Files
jj-vcs-jj/cli/tests/test_workspaces.rs
Martin von Zweigbergk 227a135b91 workspace: test auto-update-stale with dirty working copy
I was looking into a bug at work where someone had `auto-update-stale`
enabled and I wasn't sure if any mutating command run when that config
is enabled gets run before or after reconciling concurrent
operations. It turns out it's after reconciling (which I think is
correct). This patch adds a test to show that.
2026-05-07 14:08:36 +00:00

2088 lines
71 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 test_case::test_case;
use testutils::TestResult;
use testutils::git;
use crate::common::CommandOutput;
use crate::common::TestEnvironment;
use crate::common::TestWorkDir;
#[test]
fn test_workspaces_invalid_name() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let main_dir = test_env.work_dir("repo");
// refuse to create, directory not created
let output = main_dir.run_jj(["workspace", "add", "--name", "", "../secondary"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: New workspace name cannot be empty
[EOF]
[exit status: 1]
");
assert!(!test_env.env_root().join("secondary").exists());
// refuse to rename
let output = main_dir.run_jj(["workspace", "rename", ""]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: New workspace name cannot be empty
[EOF]
[exit status: 1]
");
}
/// Test adding a second and a third workspace
#[test]
fn test_workspaces_add_second_and_third_workspace() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("file", "contents");
main_dir.run_jj(["commit", "-m", "initial"]).success();
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 504e3d8c (empty) (no description set)
[EOF]
");
let output = main_dir.run_jj(["workspace", "add", "--name", "second", "../secondary"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../secondary"
Working copy (@) now at: rzvqmyuk bcc858e1 (empty) (no description set)
Parent commit (@-) : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
// 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(&main_dir), @r#"
@ 504e3d8c1bcd default@
│ ○ bcc858e1d93f second@
├─╯
○ 7b22a8cbe888 "initial"
◆ 000000000000
[EOF]
"#);
insta::assert_snapshot!(get_log_output(&secondary_dir), @r#"
@ bcc858e1d93f second@
│ ○ 504e3d8c1bcd default@
├─╯
○ 7b22a8cbe888 "initial"
◆ 000000000000
[EOF]
"#);
// Both workspaces show up when we list them
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 504e3d8c (empty) (no description set)
second: rzvqmyuk bcc858e1 (empty) (no description set)
[EOF]
");
// Check that a workspace can be created in an existing empty directory
main_dir.create_dir("../third");
let output = main_dir.run_jj(["workspace", "add", "--name", "third", "../third"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../third"
Working copy (@) now at: nuwvvtmy d55e769c (empty) (no description set)
Parent commit (@-) : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
// Duplicate names are not allowed, directory not created
let output = main_dir.run_jj(["workspace", "add", "--name", "third", "../tertiary"]);
insta::assert_snapshot!(output.normalize_backslash(), @"
------- stderr -------
Error: Workspace named 'third' already exists
[EOF]
[exit status: 1]
");
assert!(!test_env.env_root().join("tertiary").exists());
}
#[test]
fn test_workspaces_add_with_message() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.write_file("file", "contents");
main_dir.run_jj(["commit", "-m", "initial"]).success();
let output = main_dir.run_jj([
"workspace",
"add",
"--name",
"second",
"-m",
"add second workspace",
"../secondary",
]);
// Check that the newly created workspace has a description for the commit
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../secondary"
Working copy (@) now at: pmmvwywv 47a2d9a1 (empty) add second workspace
Parent commit (@-) : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
main_dir
.run_jj([
"workspace",
"add",
"--name",
"third",
"-m",
"first message",
"-m",
"second message",
"../tertiary",
])
.success();
// Check that multiple messages work as expected like with the "new" command
let output = main_dir.run_jj(["log", "-r", "third@", "-Tdescription", "--no-graph"]);
insta::assert_snapshot!(output.normalize_backslash(), @"
first message
second message
[EOF]
");
// Test that adding workspace with no message has no trailers
test_env.add_config(
r#"[templates]
commit_trailers = '"Signed-off-by: " ++ committer.email()'
"#,
);
let output = main_dir.run_jj(["workspace", "add", "--name", "fourth", "../quaternary"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../quaternary"
Working copy (@) now at: nppvrztz 0bfa7004 (empty) (no description set)
Parent commit (@-) : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
}
/// Test how sparse patterns are inherited
#[test]
fn test_workspaces_sparse_patterns() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "ws1"]).success();
let ws1_dir = test_env.work_dir("ws1");
let ws2_dir = test_env.work_dir("ws2");
let ws3_dir = test_env.work_dir("ws3");
let ws4_dir = test_env.work_dir("ws4");
let ws5_dir = test_env.work_dir("ws5");
let ws6_dir = test_env.work_dir("ws6");
ws1_dir
.run_jj(["sparse", "set", "--clear", "--add=foo"])
.success();
ws1_dir.run_jj(["workspace", "add", "../ws2"]).success();
let output = ws2_dir.run_jj(["sparse", "list"]);
insta::assert_snapshot!(output, @"
foo
[EOF]
");
ws2_dir.run_jj(["sparse", "set", "--add=bar"]).success();
ws2_dir.run_jj(["workspace", "add", "../ws3"]).success();
let output = ws3_dir.run_jj(["sparse", "list"]);
insta::assert_snapshot!(output, @"
bar
foo
[EOF]
");
// --sparse-patterns behavior
ws3_dir
.run_jj(["workspace", "add", "--sparse-patterns=copy", "../ws4"])
.success();
let output = ws4_dir.run_jj(["sparse", "list"]);
insta::assert_snapshot!(output, @"
bar
foo
[EOF]
");
ws3_dir
.run_jj(["workspace", "add", "--sparse-patterns=full", "../ws5"])
.success();
let output = ws5_dir.run_jj(["sparse", "list"]);
insta::assert_snapshot!(output, @"
.
[EOF]
");
ws3_dir
.run_jj(["workspace", "add", "--sparse-patterns=empty", "../ws6"])
.success();
let output = ws6_dir.run_jj(["sparse", "list"]);
insta::assert_snapshot!(output, @"");
}
/// 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.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.run_jj(["describe", "-m=left"]).success();
main_dir.run_jj(["new", "@-", "-m=right"]).success();
main_dir.run_jj(["new", "@-+", "-m=merge"]).success();
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: zsuskuln 46ed31b6 (empty) merge
[EOF]
");
main_dir
.run_jj(["workspace", "add", "--name", "second", "../secondary"])
.success();
// The new workspace's working-copy commit shares all parents with the old one.
insta::assert_snapshot!(get_log_output(&main_dir), @r#"
@ 46ed31b61ce9 default@ "merge"
├─╮
│ │ ○ d23b2d4ff55c second@
╭─┬─╯
│ ○ 3c52528f5893 "left"
○ │ a3155ab1bf5a "right"
├─╯
◆ 000000000000
[EOF]
"#);
}
/// Test that --ignore-working-copy is respected
#[test]
fn test_workspaces_add_ignore_working_copy() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
// TODO: maybe better to error out early?
let output = main_dir.run_jj(["workspace", "add", "--ignore-working-copy", "../secondary"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../secondary"
Error: This command must be able to update the working copy.
Hint: Don't use --ignore-working-copy.
[EOF]
[exit status: 1]
"#);
}
/// Test that --no-integrate-operation is respected
#[test]
fn test_workspaces_add_no_integrate_operation() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
// TODO: maybe better to error out early?
let output = main_dir.run_jj([
"workspace",
"add",
"--no-integrate-operation",
"../secondary",
]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../secondary"
Error: This command must be able to update the working copy.
Hint: Don't use --no-integrate-operation.
[EOF]
[exit status: 1]
"#);
}
/// Test that --at-op is respected
#[test]
fn test_workspaces_add_at_operation() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.write_file("file1", "");
let output = main_dir.run_jj(["commit", "-m1"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Working copy (@) now at: rlvkpnrz 59e07459 (empty) (no description set)
Parent commit (@-) : qpvuntsm 9e4b0b91 1
[EOF]
");
main_dir.write_file("file2", "");
let output = main_dir.run_jj(["commit", "-m2"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Working copy (@) now at: kkmpptxz 6e9610ac (empty) (no description set)
Parent commit (@-) : rlvkpnrz 8b7259b9 2
[EOF]
");
// --at-op should disable snapshot in the main workspace, but the newly
// created workspace should still be writable.
main_dir.write_file("file3", "");
let output = main_dir.run_jj(["workspace", "add", "--at-op=@-", "../secondary"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../secondary"
Working copy (@) now at: rzvqmyuk b8772476 (empty) (no description set)
Parent commit (@-) : qpvuntsm 9e4b0b91 1
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
let secondary_dir = test_env.work_dir("secondary");
// New snapshot can be taken in the secondary workspace.
secondary_dir.write_file("file4", "");
let output = secondary_dir.run_jj(["status"]);
insta::assert_snapshot!(output, @"
Working copy changes:
A file4
Working copy (@) : rzvqmyuk f2ff8257 (no description set)
Parent commit (@-): qpvuntsm 9e4b0b91 1
[EOF]
------- stderr -------
Concurrent modification detected, resolving automatically.
[EOF]
");
let output = secondary_dir.run_jj(["op", "log", "-Tdescription"]);
insta::assert_snapshot!(output, @"
@ snapshot working copy
○ reconcile divergent operations
├─╮
○ │ commit 9152e822279787a168ddf4cede6440a21faa00d7
│ ○ create initial working-copy commit in workspace secondary
│ ○ add workspace 'secondary'
├─╯
○ snapshot working copy
○ commit 093c3c9624b6cfe22b310586f5638792aa80e6d7
○ snapshot working copy
○ add workspace 'default'
[EOF]
");
}
/// 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.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("file-1", "contents");
main_dir.run_jj(["commit", "-m", "first"]).success();
main_dir.write_file("file-2", "contents");
main_dir.run_jj(["commit", "-m", "second"]).success();
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: kkmpptxz 5ac9178d (empty) (no description set)
[EOF]
");
let output = main_dir.run_jj([
"workspace",
"add",
"--name",
"second",
"../secondary",
"-r",
"@--",
]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../secondary"
Working copy (@) now at: zxsnswpr ea5860fb (empty) (no description set)
Parent commit (@-) : qpvuntsm 27473635 first
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
// 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(&main_dir), @r#"
@ 5ac9178da8b2 default@
○ a47d8a593529 "second"
│ ○ ea5860fbd622 second@
├─╯
○ 27473635a942 "first"
◆ 000000000000
[EOF]
"#);
insta::assert_snapshot!(get_log_output(&secondary_dir), @r#"
@ ea5860fbd622 second@
│ ○ 5ac9178da8b2 default@
│ ○ a47d8a593529 "second"
├─╯
○ 27473635a942 "first"
◆ 000000000000
[EOF]
"#);
}
/// 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.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.write_file("file-1", "contents");
main_dir.run_jj(["commit", "-m", "first"]).success();
main_dir.run_jj(["new", "-r", "root()"]).success();
main_dir.write_file("file-2", "contents");
main_dir.run_jj(["commit", "-m", "second"]).success();
main_dir.run_jj(["new", "-r", "root()"]).success();
main_dir.write_file("file-3", "contents");
main_dir.run_jj(["commit", "-m", "third"]).success();
main_dir.run_jj(["new", "-r", "root()"]).success();
insta::assert_snapshot!(get_log_output(&main_dir), @r#"
@ 8d23abddc924
│ ○ eba7f49e2358 "third"
├─╯
│ ○ 62444a45efcf "second"
├─╯
│ ○ 27473635a942 "first"
├─╯
◆ 000000000000
[EOF]
"#);
let output = main_dir.run_jj([
"workspace",
"add",
"--name=merge",
"../merged",
"-r=subject(third)",
"-r=subject(second)",
"-r=subject(first)",
]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../merged"
Working copy (@) now at: wmwvqwsz 2d7c9a2d (empty) (no description set)
Parent commit (@-) : mzvwutvl eba7f49e third
Parent commit (@-) : kkmpptxz 62444a45 second
Parent commit (@-) : qpvuntsm 27473635 first
Added 3 files, modified 0 files, removed 0 files
[EOF]
"#);
insta::assert_snapshot!(get_log_output(&main_dir), @r#"
@ 8d23abddc924 default@
│ ○ 2d7c9a2d41dc merge@
│ ├─┬─╮
│ │ │ ○ 27473635a942 "first"
├─────╯
│ │ ○ 62444a45efcf "second"
├───╯
│ ○ eba7f49e2358 "third"
├─╯
◆ 000000000000
[EOF]
"#);
}
#[test]
fn test_workspaces_add_workspace_from_subdir() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
let subdir_dir = main_dir.create_dir("subdir");
subdir_dir.write_file("file", "contents");
main_dir.run_jj(["commit", "-m", "initial"]).success();
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 0ba0ff35 (empty) (no description set)
[EOF]
");
// Create workspace while in sub-directory of current workspace
let output = subdir_dir.run_jj(["workspace", "add", "../../secondary"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../../secondary"
Working copy (@) now at: rzvqmyuk dea1be10 (empty) (no description set)
Parent commit (@-) : qpvuntsm 80b67806 initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
// Both workspaces show up when we list them
let output = secondary_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 0ba0ff35 (empty) (no description set)
secondary: rzvqmyuk dea1be10 (empty) (no description set)
[EOF]
");
}
#[test]
fn test_workspaces_add_workspace_in_current_workspace() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.write_file("file", "contents");
main_dir.run_jj(["commit", "-m", "initial"]).success();
// Try to create workspace using name instead of path
let output = main_dir.run_jj(["workspace", "add", "secondary"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
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 058f604d (empty) (no description set)
Parent commit (@-) : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
// Workspace created despite warning
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 504e3d8c (empty) (no description set)
secondary: pmmvwywv 058f604d (empty) (no description set)
[EOF]
");
// Use explicit path instead (no warning)
let output = main_dir.run_jj(["workspace", "add", "./third"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "third"
Working copy (@) now at: zxsnswpr 1c1effec (empty) (no description set)
Parent commit (@-) : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
// Both workspaces created
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 504e3d8c (empty) (no description set)
secondary: pmmvwywv 058f604d (empty) (no description set)
third: zxsnswpr 1c1effec (empty) (no description set)
[EOF]
");
let output = main_dir.run_jj(["file", "list"]);
insta::assert_snapshot!(output.normalize_backslash(), @"
file
[EOF]
");
}
#[test]
fn test_workspace_add_override_path_in_store() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.write_file("file", "contents");
main_dir.run_jj(["commit", "-m", "initial"]).success();
let output = main_dir.run_jj(["workspace", "add", "--name", "second", "../secondary"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../secondary"
Working copy (@) now at: pmmvwywv 058f604d (empty) (no description set)
Parent commit (@-) : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
// Both workspaces show up when we list them
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 504e3d8c (empty) (no description set)
second: pmmvwywv 058f604d (empty) (no description set)
[EOF]
");
// Undoing workspace addition
let output = main_dir.run_jj(["operation", "restore", "@--"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Restored to operation: 98ef745836d5 (2001-02-03 08:05:08) commit 006bd1130b84e90ab082adeabd7409270d5a86da
[EOF]
");
// Only default workspace show up when we list them
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 504e3d8c (empty) (no description set)
[EOF]
");
// Re-creating the same workspace with different path
let output = main_dir.run_jj(["workspace", "add", "--name", "second", "../tertiary"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Created workspace in "../tertiary"
Working copy (@) now at: spxsnpux 96ef6c50 (empty) (no description set)
Parent commit (@-) : qpvuntsm 7b22a8cb initial
Added 1 files, modified 0 files, removed 0 files
[EOF]
"#);
// Both workspaces show up when we list them
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz 504e3d8c (empty) (no description set)
second: spxsnpux 96ef6c50 (empty) (no description set)
[EOF]
");
// The 'second' workspace points to the new path
let output = main_dir.run_jj(["workspace", "root", "--name", "second"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/tertiary
[EOF]
");
}
/// 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.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("file", "contents\n");
main_dir.run_jj(["new"]).success();
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 393250c59e39 default@
│ ○ 547036666102 secondary@
├─╯
○ 9a462e35578a
◆ 000000000000
[EOF]
");
// Make changes in both working copies
main_dir.write_file("file", "changed in main\n");
secondary_dir.write_file("file", "changed in second\n");
// Squash the changes from the main workspace into the initial commit (before
// running any command in the secondary workspace
let output = main_dir.run_jj(["squash"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Rebased 1 descendant commits
Working copy (@) now at: mzvwutvl 3a9b690d (empty) (no description set)
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
[EOF]
");
// The secondary workspace's working-copy commit was updated
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 3a9b690d6e67 default@
│ ○ 90f3d42e0bff secondary@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
");
let output = secondary_dir.run_jj(["st"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: The working copy is stale (not updated since operation 58f8ef773e05).
Hint: Run `jj workspace update-stale` to update it.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy for more information.
[EOF]
[exit status: 1]
");
// Same error on second run, and from another command
let output = secondary_dir.run_jj(["log"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: The working copy is stale (not updated since operation 58f8ef773e05).
Hint: Run `jj workspace update-stale` to update it.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy for more information.
[EOF]
[exit status: 1]
");
// 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)
let output = secondary_dir.run_jj(["workspace", "update-stale"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Concurrent modification detected, resolving automatically.
Rebased 1 descendant commits onto commits rewritten by other operation
Working copy (@) now at: pmmvwywv/2 90f3d42e (divergent) (empty) (no description set)
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit 90f3d42e0bff
[EOF]
");
insta::assert_snapshot!(get_log_output(&secondary_dir),
@"
@ 90f3d42e0bff secondary@ (divergent)
× 8823f4273170 (divergent)
├─╯
│ ○ 3a9b690d6e67 default@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
");
// The stale working copy should have been resolved by the previous command
insta::assert_snapshot!(get_log_output(&secondary_dir), @"
@ 90f3d42e0bff secondary@ (divergent)
× 8823f4273170 (divergent)
├─╯
│ ○ 3a9b690d6e67 default@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
");
}
/// 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.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("file", "contents\n");
main_dir.run_jj(["new"]).success();
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 393250c59e39 default@
│ ○ 547036666102 secondary@
├─╯
○ 9a462e35578a
◆ 000000000000
[EOF]
");
// Rewrite the check-out commit in one workspace.
main_dir.write_file("file", "changed in main\n");
let output = main_dir.run_jj(["squash"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Rebased 1 descendant commits
Working copy (@) now at: mzvwutvl 3a9b690d (empty) (no description set)
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
[EOF]
");
// The secondary workspace's working-copy commit was updated.
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 3a9b690d6e67 default@
│ ○ 90f3d42e0bff secondary@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
");
let output = secondary_dir.run_jj(["st"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: The working copy is stale (not updated since operation 58f8ef773e05).
Hint: Run `jj workspace update-stale` to update it.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy for more information.
[EOF]
[exit status: 1]
");
// It was detected that the working copy is now stale, but clean. So no
// divergent commit should be created.
let output = secondary_dir.run_jj(["workspace", "update-stale"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Working copy (@) now at: pmmvwywv 90f3d42e (empty) (no description set)
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit 90f3d42e0bff
[EOF]
");
insta::assert_snapshot!(get_log_output(&secondary_dir),
@"
@ 90f3d42e0bff secondary@
│ ○ 3a9b690d6e67 default@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
");
}
/// Test a clean working copy that gets rewritten from another workspace
#[test]
fn test_workspaces_updated_by_other_automatic() {
let test_env = TestEnvironment::default();
test_env.add_config("snapshot.auto-update-stale = true\n");
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("file", "contents\n");
main_dir.run_jj(["new"]).success();
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 393250c59e39 default@
│ ○ 547036666102 secondary@
├─╯
○ 9a462e35578a
◆ 000000000000
[EOF]
");
// Rewrite the check-out commit in one workspace.
main_dir.write_file("file", "changed in main\n");
let output = main_dir.run_jj(["squash"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Rebased 1 descendant commits
Working copy (@) now at: mzvwutvl 3a9b690d (empty) (no description set)
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
[EOF]
");
// The secondary workspace's working-copy commit was updated.
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 3a9b690d6e67 default@
│ ○ 90f3d42e0bff secondary@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
");
// The first working copy gets automatically updated.
let output = secondary_dir.run_jj(["st"]);
insta::assert_snapshot!(output, @"
The working copy has no changes.
Working copy (@) : pmmvwywv 90f3d42e (empty) (no description set)
Parent commit (@-): qpvuntsm b853f7c8 (no description set)
[EOF]
------- stderr -------
Working copy (@) now at: pmmvwywv 90f3d42e (empty) (no description set)
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit 90f3d42e0bff
[EOF]
");
insta::assert_snapshot!(get_log_output(&secondary_dir),
@"
@ 90f3d42e0bff secondary@
│ ○ 3a9b690d6e67 default@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
");
}
/// Test a dirty working copy that gets rewritten from another workspace
#[test]
fn test_workspaces_updated_by_other_with_changes_in_working_copy_automatic() {
let test_env = TestEnvironment::default();
test_env.add_config("snapshot.auto-update-stale = true\n");
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("file", "contents\n");
main_dir.run_jj(["new"]).success();
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 393250c59e39 default@
│ ○ 547036666102 secondary@
├─╯
○ 9a462e35578a
◆ 000000000000
[EOF]
");
// Rewrite all commits from one workspace.
main_dir.write_file("file", "changed in main\n");
let output = main_dir.run_jj(["squash"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Rebased 1 descendant commits
Working copy (@) now at: mzvwutvl 3a9b690d (empty) (no description set)
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
[EOF]
");
// The secondary workspace's working-copy commit was updated.
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 3a9b690d6e67 default@
│ ○ 90f3d42e0bff secondary@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
");
// The first working copy gets automatically updated.
secondary_dir.write_file("file", "modified contents\n");
let output = secondary_dir.run_jj(["describe", "-m", "modified"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Concurrent modification detected, resolving automatically.
Rebased 1 descendant commits onto commits rewritten by other operation
Working copy (@) now at: pmmvwywv/2 90f3d42e (divergent) (empty) (no description set)
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit 90f3d42e0bff
Working copy (@) now at: pmmvwywv/0 c38323e3 (divergent) (empty) modified
Parent commit (@-) : qpvuntsm b853f7c8 (no description set)
[EOF]
");
// The snapshotting of the modified contents happens on top of the old
// operation. The `describe` operation itself happens on top the reconciled
// operation.
let output = main_dir.run_jj(["op", "log", "-Tdescription"]);
insta::assert_snapshot!(output, @"
@ describe commit 90f3d42e0bff073721e2640e32c18fb1c386d7ce
○ reconcile divergent operations
├─╮
○ │ squash commits into 9a462e35578a347e6a3951bf7a58ad7146959a8b
○ │ snapshot working copy
│ ○ snapshot working copy
├─╯
○ create initial working-copy commit in workspace secondary
○ add workspace 'secondary'
○ new empty commit
○ snapshot working copy
○ add workspace 'default'
[EOF]
");
// We get divergence between the newly described commit and the commit created
// by snapshotting (the reconciliation happened to point secondary@ to the child
// of the squashed commit rather than the snapshot commit).
insta::assert_snapshot!(get_log_output(&secondary_dir),
@r#"
@ c38323e3e6f3 secondary@ (divergent) "modified"
× 48a90f069c8c (divergent)
├─╯
│ ○ 3a9b690d6e67 default@
├─╯
○ b853f7c8b006
◆ 000000000000
[EOF]
"#);
}
#[test_case(false; "manual")]
#[test_case(true; "automatic")]
fn test_workspaces_current_op_discarded_by_other(automatic: bool) {
let test_env = TestEnvironment::default();
if automatic {
test_env.add_config("snapshot.auto-update-stale = true\n");
}
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("modified", "base\n");
main_dir.write_file("deleted", "base\n");
main_dir.write_file("sparse", "base\n");
main_dir.run_jj(["new"]).success();
main_dir.write_file("modified", "main\n");
main_dir.run_jj(["new"]).success();
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
// Make unsnapshotted writes in the secondary working copy
secondary_dir
.run_jj([
"sparse",
"set",
"--clear",
"--add=modified",
"--add=deleted",
"--add=added",
])
.success();
secondary_dir.write_file("modified", "secondary\n");
secondary_dir.remove_file("deleted");
secondary_dir.write_file("added", "secondary\n");
// Create an op by abandoning the parent commit. Importantly, that commit also
// changes the target tree in the secondary workspace.
main_dir.run_jj(["abandon", "@-"]).success();
let output = main_dir.run_jj([
"operation",
"log",
"--template",
r#"id.short(10) ++ " " ++ description"#,
]);
insta::allow_duplicates! {
insta::assert_snapshot!(output, @"
@ 2dea766382 abandon commit de90575a14d8b9198dc0930f9de4a69f846ded36
○ 0d3faebd8c create initial working-copy commit in workspace secondary
○ 200ba564cb add workspace 'secondary'
○ b34eafb924 new empty commit
○ c883929a6b snapshot working copy
○ bd7b1cfa98 new empty commit
○ 4509e20d8c snapshot working copy
○ 90267f31f9 add workspace 'default'
○ 0000000000
[EOF]
");
}
// Abandon ops, including the one the secondary workspace is currently on.
main_dir.run_jj(["operation", "abandon", "..@-"]).success();
main_dir.run_jj(["util", "gc", "--expire=now"]).success();
insta::allow_duplicates! {
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 320bc89effc9 default@
│ ○ 891f00062e10 secondary@
├─╯
○ 367415be5b44
◆ 000000000000
[EOF]
");
}
if automatic {
// Run a no-op command to set the randomness seed for commit hashes.
secondary_dir.run_jj(["help"]).success();
let output = secondary_dir.run_jj(["st"]);
insta::assert_snapshot!(output, @"
Working copy changes:
C {modified => added}
D deleted
M modified
Working copy (@) : kmkuslsw 18851b39 RECOVERY COMMIT FROM `jj workspace update-stale`
Parent commit (@-): rzvqmyuk 891f0006 (empty) (no description set)
[EOF]
------- stderr -------
Failed to read working copy's current operation; attempting recovery. Error message from read attempt: Object 0d3faebd8cf4f0e39ea3eab47f22c9cdbcdaa54d95e79a86a0dab4ebe3b0377f69e1d64fa4661913c8c6af01dc0ebb5ad7c8b2bfa3d229827b2ba756d729e0bf of type operation not found
Created and checked out recovery commit 866928d1e0fd
[EOF]
");
} else {
let output = secondary_dir.run_jj(["st"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: Could not read working copy's operation.
Hint: Run `jj workspace update-stale` to recover.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy for more information.
[EOF]
[exit status: 1]
");
let output = secondary_dir.run_jj(["workspace", "update-stale"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Failed to read working copy's current operation; attempting recovery. Error message from read attempt: Object 0d3faebd8cf4f0e39ea3eab47f22c9cdbcdaa54d95e79a86a0dab4ebe3b0377f69e1d64fa4661913c8c6af01dc0ebb5ad7c8b2bfa3d229827b2ba756d729e0bf of type operation not found
Created and checked out recovery commit 866928d1e0fd
[EOF]
");
}
insta::allow_duplicates! {
insta::assert_snapshot!(get_log_output(&main_dir), @r#"
@ 320bc89effc9 default@
│ ○ 18851b397d09 secondary@ "RECOVERY COMMIT FROM `jj workspace update-stale`"
│ ○ 891f00062e10
├─╯
○ 367415be5b44
◆ 000000000000
[EOF]
"#);
}
// The sparse patterns should remain
let output = secondary_dir.run_jj(["sparse", "list"]);
insta::allow_duplicates! {
insta::assert_snapshot!(output, @"
added
deleted
modified
[EOF]
");
}
let output = secondary_dir.run_jj(["st"]);
insta::allow_duplicates! {
insta::assert_snapshot!(output, @"
Working copy changes:
C {modified => added}
D deleted
M modified
Working copy (@) : kmkuslsw 18851b39 RECOVERY COMMIT FROM `jj workspace update-stale`
Parent commit (@-): rzvqmyuk 891f0006 (empty) (no description set)
[EOF]
");
}
insta::allow_duplicates! {
// The modified file should have the same contents it had before (not reset to
// the base contents)
insta::assert_snapshot!(secondary_dir.read_file("modified"), @"secondary");
}
let output = secondary_dir.run_jj(["evolog"]);
if automatic {
insta::assert_snapshot!(output, @"
@ kmkuslsw test.user@example.com 2001-02-03 08:05:18 secondary@ 18851b39
│ RECOVERY COMMIT FROM `jj workspace update-stale`
│ -- operation a35d39d101f4 snapshot working copy
○ kmkuslsw/1 test.user@example.com 2001-02-03 08:05:18 866928d1 (hidden)
(empty) RECOVERY COMMIT FROM `jj workspace update-stale`
-- operation 754c2986ff83 recovery commit
[EOF]
");
} else {
insta::assert_snapshot!(output, @"
@ kmkuslsw test.user@example.com 2001-02-03 08:05:18 secondary@ 18851b39
│ RECOVERY COMMIT FROM `jj workspace update-stale`
│ -- operation 3ae899f26750 snapshot working copy
○ kmkuslsw/1 test.user@example.com 2001-02-03 08:05:18 866928d1 (hidden)
(empty) RECOVERY COMMIT FROM `jj workspace update-stale`
-- operation 754c2986ff83 recovery commit
[EOF]
");
}
}
#[test]
fn test_workspaces_update_stale_noop() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let output = main_dir.run_jj(["workspace", "update-stale"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Attempted recovery, but the working copy is not stale
[EOF]
");
let output = main_dir.run_jj(["workspace", "update-stale", "--ignore-working-copy"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: This command must be able to update the working copy.
Hint: Don't use --ignore-working-copy.
[EOF]
[exit status: 1]
");
let output = main_dir.run_jj(["op", "log", "-Tdescription"]);
insta::assert_snapshot!(output, @"
@ add workspace 'default'
[EOF]
");
}
/// If the working copy was last updated to an unpublished operation, it should
/// be reported, even if the latest published operation has the same tree.
#[test]
fn test_workspaces_unpublished_operation_same_tree() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.run_jj(["desc", "-m=A"]).success();
let a_op_id = main_dir.current_operation_id();
main_dir.run_jj(["new", "-m=B"]).success();
let b_op_id = main_dir.current_operation_id();
// Make the repo forget about the B operation
main_dir.remove_file(format!(".jj/repo/op_heads/heads/{b_op_id}"));
main_dir.write_file(format!(".jj/repo/op_heads/heads/{a_op_id}"), "");
main_dir
.run_jj(["new", "-m=C", "--ignore-working-copy"])
.success();
// The working copy should be stale
let output = main_dir.run_jj(["status"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Internal error: The repo was loaded at operation 8627c7508be4, which seems to be a sibling of the working copy's operation eceacbdafd84
Hint: Run `jj op integrate eceacbdafd84` to add the working copy's operation to the operation log.
[EOF]
[exit status: 255]
");
// Test recovery by running `jj workspace update-stale` even though `jj op
// integrate` is a better solution
let output = main_dir.run_jj(["workspace", "update-stale"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Working copy (@) now at: zsuskuln 36a15ac4 (empty) C
Parent commit (@-) : qpvuntsm 8777db25 (empty) A
Updated working copy to fresh commit 36a15ac414e8
[EOF]
");
}
/// 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.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("file", "changed in main\n");
main_dir.run_jj(["new"]).success();
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
// Record new operation in one workspace.
main_dir.run_jj(["new"]).success();
// Snapshot the other working copy, which unfortunately results in concurrent
// operations, but should be resolved cleanly.
secondary_dir.write_file("file", "changed in second\n");
let output = secondary_dir.run_jj(["workspace", "update-stale"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Concurrent modification detected, resolving automatically.
Attempted recovery, but the working copy is not stale
[EOF]
");
insta::assert_snapshot!(get_log_output(&secondary_dir), @"
@ 35d779b3baea secondary@
│ ○ c9516583d53b default@
│ ○ f6ae7810ef56
├─╯
○ 7d5738ba9943
◆ 000000000000
[EOF]
");
}
/// Test that "workspace update-stale" works in colocated repos.
///
/// This is a regression test for a bug introduced in commit 7a296ca1 where
/// the reload-to-HEAD logic (added to fix a race condition) would break
/// "workspace update-stale" by reloading the repo to HEAD before snapshotting,
/// even though recovery intentionally loads at an old operation.
#[test]
fn test_colocated_workspace_update_stale() {
let test_env = TestEnvironment::default();
test_env
.run_jj_in(".", ["git", "init", "--colocate", "main"])
.success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
let git_repo = git::open(main_dir.root());
main_dir.write_file("file", "contents\n");
main_dir.run_jj(["new"]).success();
// Create new bookmarked revision from the main workspace.
main_dir
.run_jj(["new", "--no-edit", "root()", "-mold book1"])
.success();
main_dir
.run_jj(["bookmark", "set", "-rsubject('old book1')", "book1"])
.success();
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
// Rewrite the check-out commit from the secondary workspace.
// This makes the main (colocated) workspace's working copy stale.
secondary_dir.write_file("file", "changed in secondary\n");
secondary_dir.run_jj(["squash"]).success();
// Update and export the bookmark from the secondary workspace.
secondary_dir
.run_jj(["new", "--no-edit", "root()", "-mnew book1"])
.success();
secondary_dir
.run_jj([
"bookmark",
"set",
"-rsubject('new book1')",
"--allow-backwards",
"book1",
])
.success();
secondary_dir.run_jj(["git", "export"]).success();
// Create new Git ref and commit which will be imported later by "jj
// workspace update-stale".
git::add_commit(&git_repo, "refs/heads/book2", "file", b"", "book2", &[]);
insta::assert_snapshot!(get_log_output(&secondary_dir), @r#"
@ 9cb8253861b5 secondary@
│ ○ f562bf82f2da default@
├─╯
○ 30ed2f28b710
│ ○ e97ad7861f78 book1 "new book1"
├─╯
│ ○ f656b467890b "old book1"
├─╯
◆ 000000000000
[EOF]
"#);
// The main workspace's working copy is now stale.
let output = main_dir.run_jj(["st"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: The working copy is stale (not updated since operation 8ab980a3d398).
Hint: Run `jj workspace update-stale` to update it.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy for more information.
[EOF]
[exit status: 1]
");
// Before the fix, this would fail with the same "working copy is stale" error
// because the colocated repo reload logic would reload to HEAD before
// snapshotting, breaking the recovery.
let output = main_dir.run_jj(["workspace", "update-stale"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Working copy (@) now at: rlvkpnrz f562bf82 (empty) (no description set)
Parent commit (@-) : qpvuntsm 30ed2f28 (no description set)
Added 0 files, modified 1 files, removed 0 files
Updated working copy to fresh commit f562bf82f2da
Done importing changes from the underlying Git repo.
[EOF]
");
// Verify the workspace is now up-to-date. New bookmark "book2" should have
// been imported by the previous command.
let output = main_dir.run_jj(["st"]);
insta::assert_snapshot!(output, @"
The working copy has no changes.
Working copy (@) : rlvkpnrz f562bf82 (empty) (no description set)
Parent commit (@-): qpvuntsm 30ed2f28 (no description set)
[EOF]
");
// The updated bookmark "book1" shouldn't be re-imported as an external
// change. If it were, the "old book1" revision would be abandoned.
insta::assert_snapshot!(get_log_output(&main_dir), @r#"
@ f562bf82f2da default@
│ ○ 9cb8253861b5 secondary@
├─╯
○ 30ed2f28b710
│ ○ 7fe3ff3b9a60 book2 "book2"
├─╯
│ ○ e97ad7861f78 book1 "new book1"
├─╯
│ ○ f656b467890b "old book1"
├─╯
◆ 000000000000
[EOF]
"#);
}
/// Test forgetting workspaces
#[test]
fn test_workspaces_forget() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.write_file("file", "contents");
main_dir.run_jj(["new"]).success();
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
let output = main_dir.run_jj(["workspace", "forget"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Warning: The current workspace 'default' no longer exists after this operation. The working copy was left untouched.
Hint: Restore to an operation that contains the workspace (e.g. `jj undo` or `jj redo`).
[EOF]
");
// When listing workspaces, only the secondary workspace shows up
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
secondary: pmmvwywv 31da1455 (empty) (no description set)
[EOF]
");
// After forgetting the default, secondary root is still recorded, default no
// longer exists
let output = main_dir.run_jj(["workspace", "root", "--name", "secondary"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/secondary
[EOF]
");
let output = main_dir.run_jj(["workspace", "root", "--name", "default"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: No such workspace: default
[EOF]
[exit status: 1]
");
// 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(&main_dir), @"
○ 31da14559558
○ 006bd1130b84
◆ 000000000000
[EOF]
");
// Revision "@" cannot be used
let output = main_dir.run_jj(["log", "-r", "@"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: Workspace `default` doesn't have a working-copy commit
[EOF]
[exit status: 1]
");
// Try to add back the workspace
// TODO: We should make this just add it back instead of failing
let output = main_dir.run_jj(["workspace", "add", "."]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: Destination path exists and is not an empty directory
[EOF]
[exit status: 1]
");
// Add a third workspace...
main_dir.run_jj(["workspace", "add", "../third"]).success();
// ... and then forget it, a non-existent one, and the secondary workspace too
let output = main_dir.run_jj(["workspace", "forget", "secondary", "nonexistent", "third"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Warning: No such workspace: nonexistent
[EOF]
");
// No workspaces left
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"");
}
/// Test forgetting workspace created before workspace store
#[test]
fn test_workspaces_forget_from_before_workspace_store() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.remove_dir_all(".jj/repo/workspace_store");
let output = main_dir.run_jj(["workspace", "forget"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Warning: The current workspace 'default' no longer exists after this operation. The working copy was left untouched.
Hint: Restore to an operation that contains the workspace (e.g. `jj undo` or `jj redo`).
[EOF]
");
// No workspaces left
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"");
}
#[test]
fn test_workspaces_forget_nothing_changed() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let output = main_dir.run_jj(["workspace", "forget", "second", "third"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Warning: No such workspace: second
Warning: No such workspace: third
Nothing changed.
[EOF]
");
}
#[test]
fn test_workspaces_forget_multi_transaction() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.write_file("file", "contents");
main_dir.run_jj(["new"]).success();
main_dir.run_jj(["workspace", "add", "../second"]).success();
main_dir.run_jj(["workspace", "add", "../third"]).success();
// there should be three workspaces
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz f6bf8819 (empty) (no description set)
second: pmmvwywv 31da1455 (empty) (no description set)
third: rzvqmyuk bf5b5b4d (empty) (no description set)
[EOF]
");
// delete two at once, in a single tx
main_dir
.run_jj(["workspace", "forget", "second", "third", "fourth"])
.success();
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz f6bf8819 (empty) (no description set)
[EOF]
");
// the op log should have the multiple valid workspaces forgotten in a single tx
let output = main_dir.run_jj(["op", "log", "--limit", "1"]);
insta::assert_snapshot!(output, @"
@ 90493ae75198 test-username@host.example.com default@ 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 fourth
[EOF]
");
// now, undo, and that should restore both workspaces
main_dir.run_jj(["undo"]).success();
// finally, there should be three workspaces at the end
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: rlvkpnrz f6bf8819 (empty) (no description set)
second: pmmvwywv 31da1455 (empty) (no description set)
third: rzvqmyuk bf5b5b4d (empty) (no description set)
[EOF]
");
}
#[test]
fn test_workspaces_forget_abandon_commits() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.write_file("file", "contents");
main_dir.run_jj(["workspace", "add", "../second"]).success();
main_dir.run_jj(["workspace", "add", "../third"]).success();
main_dir.run_jj(["workspace", "add", "../fourth"]).success();
let third_dir = test_env.work_dir("third");
third_dir.run_jj(["edit", "second@"]).success();
let fourth_dir = test_env.work_dir("fourth");
fourth_dir.run_jj(["edit", "second@"]).success();
// there should be four workspaces, three of which are at the same empty commit
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: qpvuntsm 006bd113 (no description set)
fourth: uuqppmxq 94f41578 (empty) (no description set)
second: uuqppmxq 94f41578 (empty) (no description set)
third: uuqppmxq 94f41578 (empty) (no description set)
[EOF]
");
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ 006bd1130b84 default@
│ ○ 94f41578a9e1 fourth@ second@ third@
├─╯
◆ 000000000000
[EOF]
");
// delete the default workspace (should not abandon commit since not empty)
main_dir
.run_jj(["workspace", "forget", "default"])
.success();
insta::assert_snapshot!(get_log_output(&main_dir), @"
○ 94f41578a9e1 fourth@ second@ third@
│ ○ 006bd1130b84
├─╯
◆ 000000000000
[EOF]
");
// delete the second workspace (should not abandon commit since other workspaces
// still have commit checked out)
main_dir.run_jj(["workspace", "forget", "second"]).success();
insta::assert_snapshot!(get_log_output(&main_dir), @"
○ 94f41578a9e1 fourth@ third@
│ ○ 006bd1130b84
├─╯
◆ 000000000000
[EOF]
");
// delete the last 2 workspaces (commit should be abandoned now even though
// forgotten in same tx)
main_dir
.run_jj(["workspace", "forget", "third", "fourth"])
.success();
insta::assert_snapshot!(get_log_output(&main_dir), @"
○ 006bd1130b84
◆ 000000000000
[EOF]
");
}
/// Test context of commit summary template
#[test]
fn test_list_workspaces_template() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
test_env.add_config(
r#"
templates.workspace_list = """name ++ ": " ++ target.commit_id().short() ++ " " ++
target.description().first_line() ++
if(target.current_working_copy(), " (current)") ++ "\n""""
"#,
);
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
main_dir.write_file("file", "contents");
main_dir.run_jj(["commit", "-m", "initial"]).success();
main_dir
.run_jj(["workspace", "add", "--name", "second", "../secondary"])
.success();
// "current_working_copy" should point to the workspace we operate on
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: 504e3d8c1bcd (current)
second: 058f604dffcd
[EOF]
");
let output = secondary_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: 504e3d8c1bcd
second: 058f604dffcd (current)
[EOF]
");
// Using template option
let template = r#"name ++ ": " ++ target.commit_id().short() ++ "\n""#;
let output = main_dir.run_jj(["workspace", "list", "-T", template]);
insta::assert_snapshot!(output, @"
default: 504e3d8c1bcd
second: 058f604dffcd
[EOF]
");
}
#[test]
fn test_list_workspaces_template_root() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir
.run_jj(["workspace", "add", "--name", "second", "../secondary"])
.success();
let template = r#"name ++ ": " ++ root ++ "\n""#;
let output = main_dir.run_jj(["workspace", "list", "-T", template]);
insta::assert_snapshot!(output.normalize_backslash(), @"
default: $TEST_ENV/main
second: $TEST_ENV/secondary
[EOF]
");
}
#[test]
fn test_list_workspaces_template_root_unavailable() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir
.run_jj(["workspace", "add", "--name", "second", "../secondary"])
.success();
std::fs::remove_dir_all(test_env.env_root().join("secondary")).unwrap();
let template = r#"name ++ ": " ++ root ++ "\n""#;
let output = main_dir
.run_jj(["workspace", "list", "-T", template])
.normalize_backslash()
.normalize_stdout_with(|s| {
s.replace(
"The system cannot find the file specified.",
"No such file or directory",
)
});
insta::assert_snapshot!(output, @"
default: $TEST_ENV/main
second: <Error: Failed to resolve workspace root: second: $TEST_ENV/main/.jj/repo/../../../secondary: No such file or directory (os error 2)>
[EOF]
");
}
/// Test getting the workspace root from primary and secondary workspaces
#[test]
fn test_workspaces_root() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let secondary_dir = test_env.work_dir("secondary");
let output = main_dir.run_jj(["workspace", "root"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/main
[EOF]
");
let main_subdir_dir = main_dir.create_dir("subdir");
let output = main_subdir_dir.run_jj(["workspace", "root"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/main
[EOF]
");
main_dir
.run_jj(["workspace", "add", "--name", "secondary", "../secondary"])
.success();
// Explicitly request root of 'secondary' workspace from main workspace
let output = main_dir.run_jj(["workspace", "root", "--name", "secondary"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/secondary
[EOF]
");
let output = secondary_dir.run_jj(["workspace", "root"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/secondary
[EOF]
");
let secondary_subdir_dir = secondary_dir.create_dir("subdir");
let output = secondary_subdir_dir.run_jj(["workspace", "root"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/secondary
[EOF]
");
}
#[test]
fn test_workspaces_relative_path() -> TestResult {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
let repo_file = test_env.env_root().join("secondary/.jj/repo");
let repo_path_bytes = std::fs::read(&repo_file)?;
let stored_path = String::from_utf8(repo_path_bytes)?;
assert_eq!(stored_path, "../../main/.jj/repo");
let secondary_dir = test_env.work_dir("secondary");
let output = secondary_dir.run_jj(["status"]);
insta::assert_snapshot!(output, @"
The working copy has no changes.
Working copy (@) : uuqppmxq 94f41578 (empty) (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
[EOF]
");
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: qpvuntsm e8849ae1 (empty) (no description set)
secondary: uuqppmxq 94f41578 (empty) (no description set)
[EOF]
");
let output = secondary_dir.run_jj(["workspace", "root"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/secondary
[EOF]
");
let secondary_subdir = secondary_dir.create_dir("subdir");
let output = secondary_subdir.run_jj(["workspace", "root"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/secondary
[EOF]
");
let output = main_dir.run_jj(["workspace", "root", "--name", "secondary"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/secondary
[EOF]
");
Ok(())
}
#[test]
fn test_workspaces_root_unavailable() -> TestResult {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir
.run_jj(["workspace", "add", "../secondary"])
.success();
std::fs::remove_dir_all(test_env.env_root().join("secondary"))?;
let output = main_dir.run_jj(["workspace", "root", "--name", "secondary"]);
insta::assert_snapshot!(output.normalize_backslash().strip_stderr_last_line(), @"
------- stderr -------
Error: Cannot resolve absolute workspace path: $TEST_ENV/main/.jj/repo/../../../secondary
[EOF]
[exit status: 1]
");
Ok(())
}
#[test]
fn test_debug_snapshot() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file", "contents");
work_dir.run_jj(["debug", "snapshot"]).success();
let output = work_dir.run_jj(["op", "log"]);
insta::assert_snapshot!(output, @"
@ a60628738654 test-username@host.example.com default@ 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
○ 90267f31f904 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()
[EOF]
");
work_dir.run_jj(["describe", "-m", "initial"]).success();
let output = work_dir.run_jj(["op", "log"]);
insta::assert_snapshot!(output, @"
@ e6e34553de88 test-username@host.example.com default@ 2001-02-03 04:05:10.000 +07:00 - 2001-02-03 04:05:10.000 +07:00
│ describe commit 006bd1130b84e90ab082adeabd7409270d5a86da
│ args: jj describe -m initial
○ a60628738654 test-username@host.example.com default@ 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
○ 90267f31f904 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()
[EOF]
");
}
#[test]
fn test_workspaces_rename_nothing_changed() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
let output = main_dir.run_jj(["workspace", "rename", "default"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Nothing changed.
[EOF]
");
}
#[test]
fn test_workspaces_rename_new_workspace_name_already_used() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir
.run_jj(["workspace", "add", "--name", "second", "../secondary"])
.success();
let output = main_dir.run_jj(["workspace", "rename", "second"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: Failed to rename a workspace
Caused by: Workspace second already exists
[EOF]
[exit status: 1]
");
}
#[test]
fn test_workspaces_rename_forgotten_workspace() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir
.run_jj(["workspace", "add", "--name", "second", "../secondary"])
.success();
main_dir.run_jj(["workspace", "forget", "second"]).success();
let secondary_dir = test_env.work_dir("secondary");
let output = secondary_dir.run_jj(["workspace", "rename", "third"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: The current workspace 'second' is not tracked in the repo.
[EOF]
[exit status: 1]
");
}
#[test]
fn test_workspaces_rename_workspace() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir
.run_jj(["workspace", "add", "--name", "second", "../secondary"])
.success();
let secondary_dir = test_env.work_dir("secondary");
// Both workspaces show up when we list them
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: qpvuntsm e8849ae1 (empty) (no description set)
second: uuqppmxq 94f41578 (empty) (no description set)
[EOF]
");
let output = secondary_dir.run_jj(["workspace", "rename", "third"]);
insta::assert_snapshot!(output, @"");
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
default: qpvuntsm e8849ae1 (empty) (no description set)
third: uuqppmxq 94f41578 (empty) (no description set)
[EOF]
");
// Can see the working-copy commit in each workspace in the log output.
insta::assert_snapshot!(get_log_output(&main_dir), @"
@ e8849ae12c70 default@
│ ○ 94f41578a9e1 third@
├─╯
◆ 000000000000
[EOF]
");
insta::assert_snapshot!(get_log_output(&secondary_dir), @"
@ 94f41578a9e1 third@
│ ○ e8849ae12c70 default@
├─╯
◆ 000000000000
[EOF]
");
// The new workspace root is recorded and accessible
let output = main_dir.run_jj(["workspace", "root", "--name", "secondary"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: No such workspace: secondary
[EOF]
[exit status: 1]
");
let output = main_dir.run_jj(["workspace", "root", "--name", "third"]);
insta::assert_snapshot!(output, @"
$TEST_ENV/secondary
[EOF]
");
}
#[test]
fn test_workspaces_rename_workspace_from_before_workspace_store() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "main"]).success();
let main_dir = test_env.work_dir("main");
main_dir.remove_dir_all(".jj/repo/workspace_store");
let output = main_dir.run_jj(["workspace", "rename", "third"]);
insta::assert_snapshot!(output, @"");
let output = main_dir.run_jj(["workspace", "list"]);
insta::assert_snapshot!(output, @"
third: qpvuntsm e8849ae1 (empty) (no description set)
[EOF]
");
// The workspace root is not in the store
let output = main_dir.run_jj(["workspace", "root", "--name", "third"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: Workspace has no recorded path: third
[EOF]
[exit status: 1]
");
}
#[must_use]
fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {
let template = r#"
separate(" ",
commit_id.short(),
bookmarks,
working_copies,
if(divergent, "(divergent)"),
surround('"', '"', description.first_line()),
)
"#;
work_dir.run_jj(["log", "-T", template, "-r", "all()"])
}