jj/cli/tests/test_workspaces.rs

1274 lines
49 KiB
Rust
Raw Normal View History

// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::path::Path;
use test_case::test_case;
use crate::common::TestEnvironment;
/// Test adding a second workspace
#[test]
fn test_workspaces_add_second_workspace() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
std::fs::write(main_path.join("file"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "initial"]);
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz 8183d0fc (empty) (no description set)
"###);
let (stdout, stderr) = test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
insta::assert_snapshot!(stdout.replace('\\', "/"), @"");
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Created workspace in "../secondary"
Working copy now at: rzvqmyuk 5ed2222c (empty) (no description set)
Parent commit : qpvuntsm 751b12b7 initial
Added 1 files, modified 0 files, removed 0 files
"###);
// Can see the working-copy commit in each workspace in the log output. The "@"
// node in the graph indicates the current workspace's working-copy commit.
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 8183d0fcaa4c default@
5ed2222c28e2 second@
751b12b7b981
000000000000
");
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path), @r###"
@ 5ed2222c28e2 second@
8183d0fcaa4c default@
751b12b7b981
000000000000
"###);
// Both workspaces show up when we list them
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz 8183d0fc (empty) (no description set)
second: rzvqmyuk 5ed2222c (empty) (no description set)
"###);
}
/// Test how sparse patterns are inherited
#[test]
fn test_workspaces_sparse_patterns() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "ws1"]);
let ws1_path = test_env.env_root().join("ws1");
let ws2_path = test_env.env_root().join("ws2");
let ws3_path = test_env.env_root().join("ws3");
let ws4_path = test_env.env_root().join("ws4");
let ws5_path = test_env.env_root().join("ws5");
let ws6_path = test_env.env_root().join("ws6");
test_env.jj_cmd_ok(&ws1_path, &["sparse", "set", "--clear", "--add=foo"]);
test_env.jj_cmd_ok(&ws1_path, &["workspace", "add", "../ws2"]);
let stdout = test_env.jj_cmd_success(&ws2_path, &["sparse", "list"]);
insta::assert_snapshot!(stdout, @r###"
foo
"###);
test_env.jj_cmd_ok(&ws2_path, &["sparse", "set", "--add=bar"]);
test_env.jj_cmd_ok(&ws2_path, &["workspace", "add", "../ws3"]);
let stdout = test_env.jj_cmd_success(&ws3_path, &["sparse", "list"]);
insta::assert_snapshot!(stdout, @r###"
bar
foo
"###);
// --sparse-patterns behavior
test_env.jj_cmd_ok(
&ws3_path,
&["workspace", "add", "--sparse-patterns=copy", "../ws4"],
);
let stdout = test_env.jj_cmd_success(&ws4_path, &["sparse", "list"]);
insta::assert_snapshot!(stdout, @r###"
bar
foo
"###);
test_env.jj_cmd_ok(
&ws3_path,
&["workspace", "add", "--sparse-patterns=full", "../ws5"],
);
let stdout = test_env.jj_cmd_success(&ws5_path, &["sparse", "list"]);
insta::assert_snapshot!(stdout, @r###"
.
"###);
test_env.jj_cmd_ok(
&ws3_path,
&["workspace", "add", "--sparse-patterns=empty", "../ws6"],
);
let stdout = test_env.jj_cmd_success(&ws6_path, &["sparse", "list"]);
insta::assert_snapshot!(stdout, @"");
}
/// Test adding a second workspace while the current workspace is editing a
/// merge
#[test]
fn test_workspaces_add_second_workspace_on_merge() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
test_env.jj_cmd_ok(&main_path, &["describe", "-m=left"]);
test_env.jj_cmd_ok(&main_path, &["new", "@-", "-m=right"]);
test_env.jj_cmd_ok(&main_path, &["new", "all:@-+", "-m=merge"]);
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: zsuskuln 35e47bff (empty) merge
"###);
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
// The new workspace's working-copy commit shares all parents with the old one.
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 35e47bff781e default@
7013a493bd09 second@
444b77e99d43
1694f2ddf8ec
000000000000
");
}
/// Test that --ignore-working-copy is respected
#[test]
fn test_workspaces_add_ignore_working_copy() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
// TODO: maybe better to error out early?
let stderr = test_env.jj_cmd_failure(
&main_path,
&["workspace", "add", "--ignore-working-copy", "../secondary"],
);
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Created workspace in "../secondary"
Error: This command must be able to update the working copy.
Hint: Don't use --ignore-working-copy.
"###);
}
/// Test that --at-op is respected
#[test]
fn test_workspaces_add_at_operation() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
std::fs::write(main_path.join("file1"), "").unwrap();
let (_stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["commit", "-m1"]);
insta::assert_snapshot!(stderr, @r###"
Working copy now at: rlvkpnrz 18d8b994 (empty) (no description set)
Parent commit : qpvuntsm 3364a7ed 1
"###);
std::fs::write(main_path.join("file2"), "").unwrap();
let (_stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["commit", "-m2"]);
insta::assert_snapshot!(stderr, @r###"
Working copy now at: kkmpptxz 2e7dc5ab (empty) (no description set)
Parent commit : rlvkpnrz 0dbaa19a 2
"###);
// --at-op should disable snapshot in the main workspace, but the newly
// created workspace should still be writable.
std::fs::write(main_path.join("file3"), "").unwrap();
let (_stdout, stderr) = test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--at-op=@-", "../secondary"],
);
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Created workspace in "../secondary"
Working copy now at: rzvqmyuk a4d1cbc9 (empty) (no description set)
Parent commit : qpvuntsm 3364a7ed 1
Added 1 files, modified 0 files, removed 0 files
"###);
let secondary_path = test_env.env_root().join("secondary");
// New snapshot can be taken in the secondary workspace.
std::fs::write(secondary_path.join("file4"), "").unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["status"]);
insta::assert_snapshot!(stdout, @r###"
Working copy changes:
A file4
Working copy : rzvqmyuk 2ba74f85 (no description set)
Parent commit: qpvuntsm 3364a7ed 1
"###);
insta::assert_snapshot!(stderr, @r###"
Concurrent modification detected, resolving automatically.
"###);
let stdout = test_env.jj_cmd_success(&secondary_path, &["op", "log", "-Tdescription"]);
insta::assert_snapshot!(stdout, @r#"
@ snapshot working copy
reconcile divergent operations
commit cd06097124e3e5860867e35c2bb105902c28ea38
create initial working-copy commit in workspace secondary
add workspace 'secondary'
snapshot working copy
commit 1c867a0762e30de4591890ea208849f793742c1b
snapshot working copy
add workspace 'default'
"#);
}
/// Test adding a workspace, but at a specific revision using '-r'
#[test]
fn test_workspaces_add_workspace_at_revision() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
std::fs::write(main_path.join("file-1"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "first"]);
std::fs::write(main_path.join("file-2"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "second"]);
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: kkmpptxz dadeedb4 (empty) (no description set)
"###);
let (_, stderr) = test_env.jj_cmd_ok(
&main_path,
&[
"workspace",
"add",
"--name",
"second",
"../secondary",
"-r",
"@--",
],
);
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Created workspace in "../secondary"
Working copy now at: zxsnswpr e374e74a (empty) (no description set)
Parent commit : qpvuntsm f6097c2f first
Added 1 files, modified 0 files, removed 0 files
"###);
// Can see the working-copy commit in each workspace in the log output. The "@"
// node in the graph indicates the current workspace's working-copy commit.
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ dadeedb493e8 default@
c420244c6398
e374e74aa0c8 second@
f6097c2f7cac
000000000000
");
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path), @r###"
@ e374e74aa0c8 second@
dadeedb493e8 default@
c420244c6398
f6097c2f7cac
000000000000
"###);
}
/// Test multiple `-r` flags to `workspace add` to create a workspace
/// working-copy commit with multiple parents.
#[test]
fn test_workspaces_add_workspace_multiple_revisions() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
std::fs::write(main_path.join("file-1"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "first"]);
test_env.jj_cmd_ok(&main_path, &["new", "-r", "root()"]);
std::fs::write(main_path.join("file-2"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "second"]);
test_env.jj_cmd_ok(&main_path, &["new", "-r", "root()"]);
std::fs::write(main_path.join("file-3"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "third"]);
test_env.jj_cmd_ok(&main_path, &["new", "-r", "root()"]);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
@ 5b36783cd11c
6c843d62ca29
544cd61f2d26
f6097c2f7cac
000000000000
"###);
let (_, stderr) = test_env.jj_cmd_ok(
&main_path,
&[
"workspace",
"add",
"--name=merge",
"../merged",
"-r=description(third)",
"-r=description(second)",
"-r=description(first)",
],
);
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Created workspace in "../merged"
Working copy now at: wmwvqwsz f4fa64f4 (empty) (no description set)
Parent commit : mzvwutvl 6c843d62 third
Parent commit : kkmpptxz 544cd61f second
Parent commit : qpvuntsm f6097c2f first
Added 3 files, modified 0 files, removed 0 files
"###);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 5b36783cd11c default@
f4fa64f40944 merge@
f6097c2f7cac
544cd61f2d26
6c843d62ca29
000000000000
");
}
#[test]
fn test_workspaces_add_workspace_from_subdir() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let subdir_path = main_path.join("subdir");
let secondary_path = test_env.env_root().join("secondary");
std::fs::create_dir(&subdir_path).unwrap();
std::fs::write(subdir_path.join("file"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "initial"]);
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz e1038e77 (empty) (no description set)
"###);
// Create workspace while in sub-directory of current workspace
let (stdout, stderr) =
test_env.jj_cmd_ok(&subdir_path, &["workspace", "add", "../../secondary"]);
insta::assert_snapshot!(stdout.replace('\\', "/"), @"");
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Created workspace in "../../secondary"
Working copy now at: rzvqmyuk 7ad84461 (empty) (no description set)
Parent commit : qpvuntsm a3a43d9e initial
Added 1 files, modified 0 files, removed 0 files
"###);
// Both workspaces show up when we list them
let stdout = test_env.jj_cmd_success(&secondary_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz e1038e77 (empty) (no description set)
secondary: rzvqmyuk 7ad84461 (empty) (no description set)
"###);
}
#[test]
fn test_workspaces_add_workspace_in_current_workspace() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
std::fs::write(main_path.join("file"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "initial"]);
// Try to create workspace using name instead of path
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["workspace", "add", "secondary"]);
insta::assert_snapshot!(stdout.replace('\\', "/"), @"");
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Created workspace in "secondary"
Warning: Workspace created inside current directory. If this was unintentional, delete the "secondary" directory and run `jj workspace forget secondary` to remove it.
Working copy now at: pmmvwywv 0a77a39d (empty) (no description set)
Parent commit : qpvuntsm 751b12b7 initial
Added 1 files, modified 0 files, removed 0 files
"###);
// Workspace created despite warning
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz 46d9ba8b (no description set)
secondary: pmmvwywv 0a77a39d (empty) (no description set)
"###);
// Use explicit path instead (no warning)
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["workspace", "add", "./third"]);
insta::assert_snapshot!(stdout.replace('\\', "/"), @"");
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Created workspace in "third"
Working copy now at: zxsnswpr 64746d4b (empty) (no description set)
Parent commit : qpvuntsm 751b12b7 initial
Added 1 files, modified 0 files, removed 0 files
"###);
// Both workspaces created
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz 477c647f (no description set)
secondary: pmmvwywv 0a77a39d (empty) (no description set)
third: zxsnswpr 64746d4b (empty) (no description set)
"###);
// Can see files from the other workspaces in main workspace, since they are
// child directories and will therefore be snapshotted
let stdout = test_env.jj_cmd_success(&main_path, &["file", "list"]);
insta::assert_snapshot!(stdout.replace('\\', "/"), @r###"
file
secondary/file
third/file
"###);
}
/// Test making changes to the working copy in a workspace as it gets rewritten
/// from another workspace
#[test]
fn test_workspaces_conflicting_edits() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
std::fs::write(main_path.join("file"), "contents\n").unwrap();
test_env.jj_cmd_ok(&main_path, &["new"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../secondary"]);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 06b57f44a3ca default@
3224de8ae048 secondary@
506f4ec3c2c6
000000000000
");
// Make changes in both working copies
std::fs::write(main_path.join("file"), "changed in main\n").unwrap();
std::fs::write(secondary_path.join("file"), "changed in second\n").unwrap();
// Squash the changes from the main workspace into the initial commit (before
// running any command in the secondary workspace
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["squash"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 descendant commits
Working copy now at: mzvwutvl a58c9a9b (empty) (no description set)
Parent commit : qpvuntsm d4124476 (no description set)
"###);
// The secondary workspace's working-copy commit was updated
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
@ a58c9a9b19ce default@
e82cd4ee8faa secondary@
d41244767d45
000000000000
"###);
let stderr = test_env.jj_cmd_failure(&secondary_path, &["st"]);
insta::assert_snapshot!(stderr, @r##"
Error: The working copy is stale (not updated since operation c81af45155a2).
Hint: Run `jj workspace update-stale` to update it.
See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy for more information.
"##);
// Same error on second run, and from another command
let stderr = test_env.jj_cmd_failure(&secondary_path, &["log"]);
insta::assert_snapshot!(stderr, @r##"
Error: The working copy is stale (not updated since operation c81af45155a2).
Hint: Run `jj workspace update-stale` to update it.
See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy for more information.
"##);
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["workspace", "update-stale"]);
// It was detected that the working copy is now stale.
// Since there was an uncommitted change in the working copy, it should
// have been committed first (causing divergence)
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Concurrent modification detected, resolving automatically.
Rebased 1 descendant commits onto commits rewritten by other operation
Working copy now at: pmmvwywv?? e82cd4ee (empty) (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit e82cd4ee8faa
"###);
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path),
@r"
@ e82cd4ee8faa secondary@ (divergent)
× a28c85ce128b (divergent)
a58c9a9b19ce default@
d41244767d45
000000000000
");
// The stale working copy should have been resolved by the previous command
let stdout = get_log_output(&test_env, &secondary_path);
assert!(!stdout.starts_with("The working copy is stale"));
insta::assert_snapshot!(stdout, @r"
@ e82cd4ee8faa secondary@ (divergent)
× a28c85ce128b (divergent)
a58c9a9b19ce default@
d41244767d45
000000000000
");
}
/// Test a clean working copy that gets rewritten from another workspace
#[test]
fn test_workspaces_updated_by_other() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
std::fs::write(main_path.join("file"), "contents\n").unwrap();
test_env.jj_cmd_ok(&main_path, &["new"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../secondary"]);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 06b57f44a3ca default@
3224de8ae048 secondary@
506f4ec3c2c6
000000000000
");
// Rewrite the check-out commit in one workspace.
std::fs::write(main_path.join("file"), "changed in main\n").unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["squash"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 descendant commits
Working copy now at: mzvwutvl a58c9a9b (empty) (no description set)
Parent commit : qpvuntsm d4124476 (no description set)
"###);
// The secondary workspace's working-copy commit was updated.
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
@ a58c9a9b19ce default@
e82cd4ee8faa secondary@
d41244767d45
000000000000
"###);
let stderr = test_env.jj_cmd_failure(&secondary_path, &["st"]);
insta::assert_snapshot!(stderr, @r##"
Error: The working copy is stale (not updated since operation c81af45155a2).
Hint: Run `jj workspace update-stale` to update it.
See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy for more information.
"##);
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["workspace", "update-stale"]);
// It was detected that the working copy is now stale, but clean. So no
// divergent commit should be created.
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Working copy now at: pmmvwywv e82cd4ee (empty) (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit e82cd4ee8faa
"###);
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path),
@r"
@ e82cd4ee8faa secondary@
a58c9a9b19ce default@
d41244767d45
000000000000
");
}
/// Test a clean working copy that gets rewritten from another workspace
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
#[test]
fn test_workspaces_updated_by_other_automatic() {
let test_env = TestEnvironment::default();
test_env.add_config("[snapshot]\nauto-update-stale = true\n");
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
std::fs::write(main_path.join("file"), "contents\n").unwrap();
test_env.jj_cmd_ok(&main_path, &["new"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../secondary"]);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 06b57f44a3ca default@
3224de8ae048 secondary@
506f4ec3c2c6
000000000000
");
// Rewrite the check-out commit in one workspace.
std::fs::write(main_path.join("file"), "changed in main\n").unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["squash"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 descendant commits
Working copy now at: mzvwutvl a58c9a9b (empty) (no description set)
Parent commit : qpvuntsm d4124476 (no description set)
"###);
// The secondary workspace's working-copy commit was updated.
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
@ a58c9a9b19ce default@
e82cd4ee8faa secondary@
d41244767d45
000000000000
"###);
// The first working copy gets automatically updated.
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["st"]);
insta::assert_snapshot!(stdout, @r###"
The working copy is clean
Working copy : pmmvwywv e82cd4ee (empty) (no description set)
Parent commit: qpvuntsm d4124476 (no description set)
"###);
insta::assert_snapshot!(stderr, @r###"
Working copy now at: pmmvwywv e82cd4ee (empty) (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit e82cd4ee8faa
"###);
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path),
@r"
@ e82cd4ee8faa secondary@
a58c9a9b19ce default@
d41244767d45
000000000000
");
}
#[test_case(false; "manual")]
#[test_case(true; "automatic")]
fn test_workspaces_current_op_discarded_by_other(automatic: bool) {
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
let test_env = TestEnvironment::default();
if automatic {
test_env.add_config("[snapshot]\nauto-update-stale = true\n");
}
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
// Use the local backend because GitBackend::gc() depends on the git CLI.
test_env.jj_cmd_ok(
test_env.env_root(),
&["init", "main", "--config-toml=ui.allow-init-native=true"],
);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
std::fs::write(main_path.join("modified"), "base\n").unwrap();
std::fs::write(main_path.join("deleted"), "base\n").unwrap();
std::fs::write(main_path.join("sparse"), "base\n").unwrap();
test_env.jj_cmd_ok(&main_path, &["new"]);
std::fs::write(main_path.join("modified"), "main\n").unwrap();
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
test_env.jj_cmd_ok(&main_path, &["new"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../secondary"]);
// Make unsnapshotted writes in the secondary working copy
test_env.jj_cmd_ok(
&secondary_path,
&[
"sparse",
"set",
"--clear",
"--add=modified",
"--add=deleted",
"--add=added",
],
);
std::fs::write(secondary_path.join("modified"), "secondary\n").unwrap();
std::fs::remove_file(secondary_path.join("deleted")).unwrap();
std::fs::write(secondary_path.join("added"), "secondary\n").unwrap();
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
// Create an op by abandoning the parent commit. Importantly, that commit also
// changes the target tree in the secondary workspace.
test_env.jj_cmd_ok(&main_path, &["abandon", "@-"]);
let stdout = test_env.jj_cmd_success(
&main_path,
&[
"operation",
"log",
"--template",
r#"id.short(10) ++ " " ++ description"#,
],
);
insta::allow_duplicates! {
insta::assert_snapshot!(stdout, @r#"
@ 757bc1140b abandon commit 20dd439c4bd12c6ad56c187ac490bd0141804618f638dc5c4dc92ff9aecba20f152b23160db9dcf61beb31a5cb14091d9def5a36d11c9599cc4d2e5689236af1
8d4abed655 create initial working-copy commit in workspace secondary
3de27432e5 add workspace 'secondary'
bcf69de808 new empty commit
a36b99a15c snapshot working copy
ddf023d319 new empty commit
829c93f6a3 snapshot working copy
2557266dd2 add workspace 'default'
0000000000
"#);
}
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
// Abandon ops, including the one the secondary workspace is currently on.
test_env.jj_cmd_ok(&main_path, &["operation", "abandon", "..@-"]);
test_env.jj_cmd_ok(&main_path, &["util", "gc", "--expire=now"]);
insta::allow_duplicates! {
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 6c051bd1ccd5 default@
96b31dafdc41 secondary@
7c5b25a4fc8f
000000000000
");
}
if automatic {
// Run a no-op command to set the randomness seed for commit hashes.
test_env.jj_cmd_success(&secondary_path, &["help"]);
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["st"]);
insta::assert_snapshot!(stdout, @r###"
Working copy changes:
A added
D deleted
M modified
Working copy : kmkuslsw 15df8cb5 RECOVERY COMMIT FROM `jj workspace update-stale`
Parent commit: rzvqmyuk 96b31daf (empty) (no description set)
"###);
insta::assert_snapshot!(stderr, @r###"
Failed to read working copy's current operation; attempting recovery. Error message from read attempt: Object 8d4abed655badb70b1bab62aa87136619dbc3c8015a8ce8dfb7abfeca4e2f36c713d8f84e070a0613907a6cee7e1cc05323fe1205a319b93fe978f11a060c33c of type operation not found
Created and checked out recovery commit 76d0126b3e5c
"###);
} else {
let stderr = test_env.jj_cmd_failure(&secondary_path, &["st"]);
insta::assert_snapshot!(stderr, @r###"
Error: Could not read working copy's operation.
Hint: Run `jj workspace update-stale` to recover.
See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy for more information.
"###);
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["workspace", "update-stale"]);
insta::assert_snapshot!(stderr, @r###"
Failed to read working copy's current operation; attempting recovery. Error message from read attempt: Object 8d4abed655badb70b1bab62aa87136619dbc3c8015a8ce8dfb7abfeca4e2f36c713d8f84e070a0613907a6cee7e1cc05323fe1205a319b93fe978f11a060c33c of type operation not found
Created and checked out recovery commit 76d0126b3e5c
"###);
insta::assert_snapshot!(stdout, @"");
}
insta::allow_duplicates! {
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 6c051bd1ccd5 default@
15df8cb57d3f secondary@
96b31dafdc41
7c5b25a4fc8f
000000000000
");
}
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
// The sparse patterns should remain
let stdout = test_env.jj_cmd_success(&secondary_path, &["sparse", "list"]);
insta::allow_duplicates! {
insta::assert_snapshot!(stdout, @r###"
added
deleted
modified
"###);
}
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["st"]);
insta::allow_duplicates! {
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(stdout, @r###"
Working copy changes:
A added
D deleted
M modified
Working copy : kmkuslsw 15df8cb5 RECOVERY COMMIT FROM `jj workspace update-stale`
Parent commit: rzvqmyuk 96b31daf (empty) (no description set)
"###);
// The modified file should have the same contents it had before (not reset to
// the base contents)
insta::assert_snapshot!(std::fs::read_to_string(secondary_path.join("modified")).unwrap(), @r###"
secondary
"###);
}
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["evolog"]);
insta::allow_duplicates! {
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(stdout, @r###"
@ kmkuslsw test.user@example.com 2001-02-03 08:05:18 secondary@ 15df8cb5
RECOVERY COMMIT FROM `jj workspace update-stale`
kmkuslsw hidden test.user@example.com 2001-02-03 08:05:18 76d0126b
(empty) RECOVERY COMMIT FROM `jj workspace update-stale`
"###);
}
workspace: recover from missing operation If the operation corresponding to a workspace is missing for some reason (the specific situation in the test in this commit is that an operation was abandoned and garbage-collected from another workspace), currently, jj fails with a 255 error code. Teach jj a way to recover from this situation. When jj detects such a situation, it prints a message and stops operation, similar to when a workspace is stale. The message tells the user what command to run. When that command is run, jj loads the repo at the @ operation (instead of the operation of the workspace), creates a new commit on the @ commit with an empty tree, and then proceeds as usual - in particular, including the auto-snapshotting of the working tree, which creates another commit that obsoletes the newly created commit. There are several design points I considered. 1) Whether the recovery should be automatic, or (as in this commit) manual in that the user should be prompted to run a command. The user might prefer to recover in another way (e.g. by simply deleting the workspace) and this situation is (hopefully) rare enough that I think it's better to prompt the user. 2) Which command the user should be prompted to run (and thus, which command should be taught to perform the recovery). I chose "workspace update-stale" because the circumstances are very similar to it: it's symptom is that the regular jj operation is blocked somewhere at the beginning, and "workspace update-stale" already does some special work before the blockage (this commit adds more of such special work). But it might be better for something more explicitly named, or even a sequence of commands (e.g. "create a new operation that becomes @ that no workspace points to", "low-level command that makes a workspace point to the operation @") but I can see how this can be unnecessarily confusing for the user. 3) How we recover. I can think of several ways: a) Always create a commit, and allow the automatic snapshotting to create another commit that obsoletes this commit. b) Create a commit but somehow teach the automatic snapshotting to replace the created commit in-place (so it has no predecessor, as viewed in "obslog"). c) Do either a) or b), with the added improvement that if there is no diff between the newly created commit and the former @, to behave as if no new commit was created (@ remains as the former @). I chose a) since it was the simplest and most easily reasoned about, which I think is the best way to go when recovering from a rare situation.
2024-02-03 05:26:23 +00:00
}
#[test]
fn test_workspaces_update_stale_noop() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["workspace", "update-stale"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @"Attempted recovery, but the working copy is not stale");
let stderr = test_env.jj_cmd_failure(
&main_path,
&["workspace", "update-stale", "--ignore-working-copy"],
);
insta::assert_snapshot!(stderr, @r###"
Error: This command must be able to update the working copy.
Hint: Don't use --ignore-working-copy.
"###);
let stdout = test_env.jj_cmd_success(&main_path, &["op", "log", "-Tdescription"]);
insta::assert_snapshot!(stdout, @r#"
@ add workspace 'default'
"#);
}
/// Test "update-stale" in a dirty, but not stale working copy.
#[test]
fn test_workspaces_update_stale_snapshot() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
std::fs::write(main_path.join("file"), "changed in main\n").unwrap();
test_env.jj_cmd_ok(&main_path, &["new"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../secondary"]);
// Record new operation in one workspace.
test_env.jj_cmd_ok(&main_path, &["new"]);
// Snapshot the other working copy, which unfortunately results in concurrent
// operations, but should be resolved cleanly.
std::fs::write(secondary_path.join("file"), "changed in second\n").unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&secondary_path, &["workspace", "update-stale"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Concurrent modification detected, resolving automatically.
Attempted recovery, but the working copy is not stale
"###);
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path), @r###"
@ e672fd8fefac secondary@
ea37b073f5ab default@
b13c81dedc64
e6e9989f1179
000000000000
"###);
}
/// Test forgetting workspaces
#[test]
fn test_workspaces_forget() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
std::fs::write(main_path.join("file"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["new"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../secondary"]);
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["workspace", "forget"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @"");
// When listing workspaces, only the secondary workspace shows up
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
secondary: pmmvwywv 18463f43 (empty) (no description set)
"###);
// `jj status` tells us that there's no working copy here
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["st"]);
insta::assert_snapshot!(stdout, @r###"
No working copy
"###);
insta::assert_snapshot!(stderr, @"");
// The old working copy doesn't get an "@" in the log output
// TODO: It seems useful to still have the "secondary@" marker here even though
// there's only one workspace. We should show it when the command is not run
// from that workspace.
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
18463f438cc9
4e8f9d2be039
000000000000
"###);
// Revision "@" cannot be used
let stderr = test_env.jj_cmd_failure(&main_path, &["log", "-r", "@"]);
insta::assert_snapshot!(stderr, @r###"
Error: Workspace "default" doesn't have a working-copy commit
"###);
// Try to add back the workspace
// TODO: We should make this just add it back instead of failing
let stderr = test_env.jj_cmd_failure(&main_path, &["workspace", "add", "."]);
insta::assert_snapshot!(stderr, @r###"
Error: Workspace already exists
"###);
// Add a third workspace...
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../third"]);
// ... and then forget it, and the secondary workspace too
let (stdout, stderr) =
test_env.jj_cmd_ok(&main_path, &["workspace", "forget", "secondary", "third"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @"");
// No workspaces left
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @"");
}
#[test]
fn test_workspaces_forget_multi_transaction() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
std::fs::write(main_path.join("file"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["new"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../second"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../third"]);
// there should be three workspaces
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz 909d51b1 (empty) (no description set)
second: pmmvwywv 18463f43 (empty) (no description set)
third: rzvqmyuk cc383fa2 (empty) (no description set)
"###);
// delete two at once, in a single tx
test_env.jj_cmd_ok(&main_path, &["workspace", "forget", "second", "third"]);
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz 909d51b1 (empty) (no description set)
"###);
// the op log should have multiple workspaces forgotten in a single tx
let stdout = test_env.jj_cmd_success(&main_path, &["op", "log", "--limit", "1"]);
insta::assert_snapshot!(stdout, @r#"
@ 60b2b5a71a84 test-username@host.example.com 2001-02-03 04:05:12.000 +07:00 - 2001-02-03 04:05:12.000 +07:00
forget workspaces second, third
args: jj workspace forget second third
"#);
// now, undo, and that should restore both workspaces
test_env.jj_cmd_ok(&main_path, &["op", "undo"]);
// finally, there should be three workspaces at the end
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: rlvkpnrz 909d51b1 (empty) (no description set)
second: pmmvwywv 18463f43 (empty) (no description set)
third: rzvqmyuk cc383fa2 (empty) (no description set)
"###);
}
#[test]
fn test_workspaces_forget_abandon_commits() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
std::fs::write(main_path.join("file"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../second"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../third"]);
test_env.jj_cmd_ok(&main_path, &["workspace", "add", "../fourth"]);
let third_path = test_env.env_root().join("third");
test_env.jj_cmd_ok(&third_path, &["edit", "second@"]);
let fourth_path = test_env.env_root().join("fourth");
test_env.jj_cmd_ok(&fourth_path, &["edit", "second@"]);
// there should be four workspaces, three of which are at the same empty commit
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: qpvuntsm 4e8f9d2b (no description set)
fourth: uuqppmxq 57d63245 (empty) (no description set)
second: uuqppmxq 57d63245 (empty) (no description set)
third: uuqppmxq 57d63245 (empty) (no description set)
"###);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 4e8f9d2be039 default@
57d63245a308 fourth@ second@ third@
000000000000
");
// delete the default workspace (should not abandon commit since not empty)
test_env.jj_cmd_success(&main_path, &["workspace", "forget", "default"]);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
57d63245a308 fourth@ second@ third@
4e8f9d2be039
000000000000
"###);
// delete the second workspace (should not abandon commit since other workspaces
// still have commit checked out)
test_env.jj_cmd_success(&main_path, &["workspace", "forget", "second"]);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
57d63245a308 fourth@ third@
4e8f9d2be039
000000000000
"###);
// delete the last 2 workspaces (commit should be abandoned now even though
// forgotten in same tx)
test_env.jj_cmd_success(&main_path, &["workspace", "forget", "third", "fourth"]);
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
4e8f9d2be039
000000000000
"###);
}
/// Test context of commit summary template
#[test]
fn test_list_workspaces_template() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
test_env.add_config(
r#"
templates.commit_summary = """commit_id.short() ++ " " ++ description.first_line() ++
if(current_working_copy, " (current)")"""
"#,
);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
std::fs::write(main_path.join("file"), "contents").unwrap();
test_env.jj_cmd_ok(&main_path, &["commit", "-m", "initial"]);
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
// "current_working_copy" should point to the workspace we operate on
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: 8183d0fcaa4c (current)
second: 0a77a39d7d6f
"###);
let stdout = test_env.jj_cmd_success(&secondary_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: 8183d0fcaa4c
second: 0a77a39d7d6f (current)
"###);
}
/// Test getting the workspace root from primary and secondary workspaces
#[test]
fn test_workspaces_root() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let secondary_path = test_env.env_root().join("secondary");
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "root"]);
insta::assert_snapshot!(stdout, @r###"
$TEST_ENV/main
"###);
let main_subdir_path = main_path.join("subdir");
std::fs::create_dir(&main_subdir_path).unwrap();
let stdout = test_env.jj_cmd_success(&main_subdir_path, &["workspace", "root"]);
insta::assert_snapshot!(stdout, @r###"
$TEST_ENV/main
"###);
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "secondary", "../secondary"],
);
let stdout = test_env.jj_cmd_success(&secondary_path, &["workspace", "root"]);
insta::assert_snapshot!(stdout, @r###"
$TEST_ENV/secondary
"###);
let secondary_subdir_path = secondary_path.join("subdir");
std::fs::create_dir(&secondary_subdir_path).unwrap();
let stdout = test_env.jj_cmd_success(&secondary_subdir_path, &["workspace", "root"]);
insta::assert_snapshot!(stdout, @r###"
$TEST_ENV/secondary
"###);
}
#[test]
fn test_debug_snapshot() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file"), "contents").unwrap();
test_env.jj_cmd_ok(&repo_path, &["debug", "snapshot"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["op", "log"]);
insta::assert_snapshot!(stdout, @r#"
@ c55ebc67e3db test-username@host.example.com 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
snapshot working copy
args: jj debug snapshot
eac759b9ab75 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
add workspace 'default'
000000000000 root()
"#);
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "initial"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["op", "log"]);
insta::assert_snapshot!(stdout, @r#"
@ c9a40b951848 test-username@host.example.com 2001-02-03 04:05:10.000 +07:00 - 2001-02-03 04:05:10.000 +07:00
describe commit 4e8f9d2be039994f589b4e57ac5e9488703e604d
args: jj describe -m initial
c55ebc67e3db test-username@host.example.com 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
snapshot working copy
args: jj debug snapshot
eac759b9ab75 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
add workspace 'default'
000000000000 root()
"#);
}
#[test]
fn test_workspaces_rename_nothing_changed() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["workspace", "rename", "default"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Nothing changed.
"###);
}
#[test]
fn test_workspaces_rename_new_workspace_name_already_used() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
let stderr = test_env.jj_cmd_failure(&main_path, &["workspace", "rename", "second"]);
insta::assert_snapshot!(stderr, @r###"
Error: Failed to rename a workspace
Caused by: Workspace second already exists
"###);
}
#[test]
fn test_workspaces_rename_forgotten_workspace() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
test_env.jj_cmd_ok(&main_path, &["workspace", "forget", "second"]);
let secondary_path = test_env.env_root().join("secondary");
let stderr = test_env.jj_cmd_failure(&secondary_path, &["workspace", "rename", "third"]);
insta::assert_snapshot!(stderr, @r###"
Error: The current workspace 'second' is not tracked in the repo.
"###);
}
#[test]
fn test_workspaces_rename_workspace() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
let secondary_path = test_env.env_root().join("secondary");
// Both workspaces show up when we list them
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: qpvuntsm 230dd059 (empty) (no description set)
second: uuqppmxq 57d63245 (empty) (no description set)
"###);
let stdout = test_env.jj_cmd_success(&secondary_path, &["workspace", "rename", "third"]);
insta::assert_snapshot!(stdout, @"");
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: qpvuntsm 230dd059 (empty) (no description set)
third: uuqppmxq 57d63245 (empty) (no description set)
"###);
// Can see the working-copy commit in each workspace in the log output.
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
@ 230dd059e1b0 default@
57d63245a308 third@
000000000000
");
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path), @r###"
@ 57d63245a308 third@
230dd059e1b0 default@
000000000000
"###);
}
fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String {
let template = r#"
separate(" ",
commit_id.short(),
working_copies,
if(divergent, "(divergent)"),
)
"#;
test_env.jj_cmd_success(cwd, &["log", "-T", template, "-r", "all()"])
}