jj/cli/tests/test_concurrent_operations.rs
Martin von Zweigbergk 6e302bb3a2 op_store: add a virtual root operation, similar to root commit
It seems obvious in hindsight to have a virtual root operation just
like we have a virtual root commit. It removes the same kind of
problems by making sure there's always a common ancestor (or multiple)
between any two commits.

I think the reason I didn't add a root operation from the beginning
was that there used to be a mandatory working-copy commit in the view
(this was before support for multiple workspaces).

Perhaps we should remove the "initialize repo" operation now. The only
difference between their view objects is that the "initialize repo"
operation adds the root commit as a head. We could add that to the
root operation, but then the root operation's value depends on the
commit backend.
2024-01-14 10:15:14 -08:00

248 lines
10 KiB
Rust

// 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 itertools::Itertools as _;
use crate::common::TestEnvironment;
pub mod common;
#[test]
fn test_concurrent_operation_divergence() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "message 1"]);
test_env.jj_cmd_ok(
&repo_path,
&["describe", "-m", "message 2", "--at-op", "@-"],
);
// "op log" doesn't merge the concurrent operations
let stdout = test_env.jj_cmd_success(&repo_path, &["op", "log"]);
insta::assert_snapshot!(stdout, @r###"
◉ 31e3dc1f7c87 test-username@host.example.com 2001-02-03 04:05:09.000 +07:00 - 2001-02-03 04:05:09.000 +07:00
│ describe commit 230dd059e1b059aefc0da06a2e5a7dbf22362f22
│ args: jj describe -m 'message 2' --at-op @-
│ ◉ e914ad151dae test-username@host.example.com 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
├─╯ describe commit 230dd059e1b059aefc0da06a2e5a7dbf22362f22
│ args: jj describe -m 'message 1'
◉ 27143b59c690 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'
◉ 0e8aee02e242 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
│ initialize repo
◉ 000000000000 @ 1970-01-01 00:00:00.000 +00:00 - 1970-01-01 00:00:00.000 +00:00
"###);
// We should be informed about the concurrent modification
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-T", "description"]);
insta::assert_snapshot!(stdout, @r###"
◉ message 2
│ @ message 1
├─╯
"###);
insta::assert_snapshot!(stderr, @r###"
Concurrent modification detected, resolving automatically.
"###);
}
#[test]
fn test_concurrent_operations_auto_rebase() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
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, &["describe", "-m", "initial"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["op", "log"]);
insta::assert_snapshot!(stdout, @r###"
@ 621f22ef65b4 test-username@host.example.com 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
│ describe commit 123ed18e4c4c0d77428df41112bc02ffc83fb935
│ args: jj describe -m initial
◉ 17cb042ae103 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 describe -m initial
◉ 27143b59c690 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'
◉ 0e8aee02e242 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
│ initialize repo
◉ 000000000000 @ 1970-01-01 00:00:00.000 +00:00 - 1970-01-01 00:00:00.000 +00:00
"###);
let op_id_hex = stdout[3..15].to_string();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "rewritten"]);
test_env.jj_cmd_ok(
&repo_path,
&["new", "--at-op", &op_id_hex, "-m", "new child"],
);
// We should be informed about the concurrent modification
let (stdout, stderr) = get_log_output_with_stderr(&test_env, &repo_path);
insta::assert_snapshot!(stdout, @r###"
◉ 3f06323826b4a293a9ee6d24cc0e07ad2961b5d5 new child
@ d91437157468ec86bbbc9e6a14a60d3e8d1790ac rewritten
◉ 0000000000000000000000000000000000000000
"###);
insta::assert_snapshot!(stderr, @r###"
Concurrent modification detected, resolving automatically.
Rebased 1 descendant commits onto commits rewritten by other operation
"###);
}
#[test]
fn test_concurrent_operations_wc_modified() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file"), "contents\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "initial"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["op", "log"]);
let op_id_hex = stdout[3..15].to_string();
test_env.jj_cmd_ok(
&repo_path,
&["new", "--at-op", &op_id_hex, "-m", "new child1"],
);
test_env.jj_cmd_ok(
&repo_path,
&["new", "--at-op", &op_id_hex, "-m", "new child2"],
);
std::fs::write(repo_path.join("file"), "modified\n").unwrap();
// We should be informed about the concurrent modification
let (stdout, stderr) = get_log_output_with_stderr(&test_env, &repo_path);
insta::assert_snapshot!(stdout, @r###"
@ 4eb0610031b7cd148ff9f729a673a3f815033170 new child1
│ ◉ 4b20e61d23ee7d7c4d5e61e11e97c26e716f9c30 new child2
├─╯
◉ 52c893bf3cd201e215b23e084e8a871244ca14d5 initial
◉ 0000000000000000000000000000000000000000
"###);
insta::assert_snapshot!(stderr, @r###"
Concurrent modification detected, resolving automatically.
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--git"]);
insta::assert_snapshot!(stdout, @r###"
diff --git a/file b/file
index 12f00e90b6...2e0996000b 100644
--- a/file
+++ b/file
@@ -1,1 +1,1 @@
-contents
+modified
"###);
// The working copy should be committed after merging the operations
let stdout = test_env.jj_cmd_success(&repo_path, &["op", "log", "-Tdescription"]);
insta::assert_snapshot!(stdout, @r###"
@ snapshot working copy
◉ resolve concurrent operations
├─╮
◉ │ new empty commit
│ ◉ new empty commit
├─╯
◉ describe commit cf911c223d3e24e001fc8264d6dbf0610804fc40
◉ snapshot working copy
◉ add workspace 'default'
◉ initialize repo
"###);
}
#[test]
fn test_concurrent_snapshot_wc_reloadable() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let op_heads_dir = repo_path
.join(".jj")
.join("repo")
.join("op_heads")
.join("heads");
std::fs::write(repo_path.join("base"), "").unwrap();
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "initial"]);
// Create new commit and checkout it.
std::fs::write(repo_path.join("child1"), "").unwrap();
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "new child1"]);
let template = r#"id ++ "\n" ++ description ++ "\n" ++ tags"#;
let op_log_stdout = test_env.jj_cmd_success(&repo_path, &["op", "log", "-T", template]);
insta::assert_snapshot!(op_log_stdout, @r###"
@ d726ace5612d3c99c793f61d5e86583f430fec332859af62a962f08c93cfac1459884371badcdaccbfd7b065c7df191d68fb35f6d537d1c0cc7ed18c70d6f1e5
│ commit 323b414dd255b51375d7f4392b7b2641ffe4289f
│ args: jj commit -m 'new child1'
◉ 5de87db156fa59f084be7beafb2e8921976cef54061d19671f1406f0f4bc3ad114e6bfee8d2e88e8d2614a49652b8f88071c48a9d78ff951dc81fcac8eb592f3
│ snapshot working copy
│ args: jj commit -m 'new child1'
◉ 1145f7c8a11bf57eb9bf7ee241a72c19e0fa536a35c099d9e72ad25a4475293f48e1f29eab90e31fa8770566199bf384dcb4f44227635c20d1e21ae2b9700303
│ commit 3d918700494a9895696e955b85fa05eb0d314cc6
│ args: jj commit -m initial
◉ a10989671b09d46b636fa4dee86182a170f2b6a9d127a785e8d1388b0680affcc4aeed8bba1face7f3fc1637b7f9327f6ac7f4d9384f468333805a215363ccbd
│ snapshot working copy
│ args: jj commit -m initial
◉ 27143b59c6904046f6be83ad6fe145d819944f9abbd7247ea9c57848d1d2c678ea8265598a156fe8aeef31d24d958bf6cfa0c2eb3afef40bdae2c5e98d73d0ee
│ add workspace 'default'
◉ 0e8aee02e24230c99d6d90d469c582a60fdb2ae8329341bbdb09f4a0beceba1ce7c84fc9ba6c7657d6d275b392b89b825502475ad2501be1ddebd4a09b07668c
│ initialize repo
◉ 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
"###);
let op_log_lines = op_log_stdout.lines().collect_vec();
let current_op_id = op_log_lines[0].split_once(" ").unwrap().1;
let previous_op_id = op_log_lines[6].split_once(" ").unwrap().1;
// Another process started from the "initial" operation, but snapshots after
// the "child1" checkout has been completed.
std::fs::rename(
op_heads_dir.join(current_op_id),
op_heads_dir.join(previous_op_id),
)
.unwrap();
std::fs::write(repo_path.join("child2"), "").unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "new child2"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Working copy now at: kkmpptxz 4011424e new child2
Parent commit : rlvkpnrz e08863ee new child1
"###);
// Since the repo can be reloaded before snapshotting, "child2" should be
// a child of "child1", not of "initial".
let template = r#"commit_id ++ " " ++ description"#;
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", template, "-s"]);
insta::assert_snapshot!(stdout, @r###"
@ 4011424ea0a210a914f869ea3c47d76931598d1d new child2
│ A child2
◉ e08863ee7a0df688755d3d3126498afdf4f580ad new child1
│ A child1
◉ 79989e62f8331e69a803058b57bacc264405cb65 initial
│ A base
◉ 0000000000000000000000000000000000000000
"###);
}
fn get_log_output_with_stderr(test_env: &TestEnvironment, cwd: &Path) -> (String, String) {
let template = r#"commit_id ++ " " ++ description"#;
test_env.jj_cmd_ok(cwd, &["log", "-T", template])
}