just playing with tangled
1// Copyright 2022 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use itertools::Itertools as _;
16
17use crate::common::CommandOutput;
18use crate::common::TestEnvironment;
19use crate::common::TestWorkDir;
20
21#[test]
22fn test_concurrent_operation_divergence() {
23 let test_env = TestEnvironment::default();
24 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
25 let work_dir = test_env.work_dir("repo");
26
27 work_dir.run_jj(["describe", "-m", "message 1"]).success();
28 work_dir
29 .run_jj(["describe", "-m", "message 2", "--at-op", "@-"])
30 .success();
31
32 // "--at-op=@" disables op heads merging, and prints head operation ids.
33 let output = work_dir.run_jj(["op", "log", "--at-op=@"]);
34 insta::assert_snapshot!(output, @r#"
35 ------- stderr -------
36 Error: The "@" expression resolved to more than one operation
37 Hint: Try specifying one of the operations by ID: 0162305507cc, d74dff64472e
38 [EOF]
39 [exit status: 1]
40 "#);
41
42 // "op log --at-op" should work without merging the head operations
43 let output = work_dir.run_jj(["op", "log", "--at-op=d74dff64472e"]);
44 insta::assert_snapshot!(output, @r"
45 @ d74dff64472e test-username@host.example.com 2001-02-03 04:05:09.000 +07:00 - 2001-02-03 04:05:09.000 +07:00
46 │ describe commit 230dd059e1b059aefc0da06a2e5a7dbf22362f22
47 │ args: jj describe -m 'message 2' --at-op @-
48 ○ eac759b9ab75 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
49 │ add workspace 'default'
50 ○ 000000000000 root()
51 [EOF]
52 ");
53
54 // We should be informed about the concurrent modification
55 let output = work_dir.run_jj(["log", "-T", "description"]);
56 insta::assert_snapshot!(output, @r"
57 @ message 1
58 │ ○ message 2
59 ├─╯
60 ◆
61 [EOF]
62 ------- stderr -------
63 Concurrent modification detected, resolving automatically.
64 [EOF]
65 ");
66}
67
68#[test]
69fn test_concurrent_operations_auto_rebase() {
70 let test_env = TestEnvironment::default();
71 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
72 let work_dir = test_env.work_dir("repo");
73
74 work_dir.write_file("file", "contents");
75 work_dir.run_jj(["describe", "-m", "initial"]).success();
76 let output = work_dir.run_jj(["op", "log"]);
77 insta::assert_snapshot!(output, @r"
78 @ c62ace5c0522 test-username@host.example.com 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
79 │ describe commit 4e8f9d2be039994f589b4e57ac5e9488703e604d
80 │ args: jj describe -m initial
81 ○ 82d32fc68fc3 test-username@host.example.com 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
82 │ snapshot working copy
83 │ args: jj describe -m initial
84 ○ eac759b9ab75 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
85 │ add workspace 'default'
86 ○ 000000000000 root()
87 [EOF]
88 ");
89 let op_id_hex = output.stdout.raw()[3..15].to_string();
90
91 work_dir.run_jj(["describe", "-m", "rewritten"]).success();
92 work_dir
93 .run_jj(["new", "--at-op", &op_id_hex, "-m", "new child"])
94 .success();
95
96 // We should be informed about the concurrent modification
97 let output = get_log_output(&work_dir);
98 insta::assert_snapshot!(output, @r"
99 ○ db141860e12c2d5591c56fde4fc99caf71cec418 new child
100 @ 07c3641e495cce57ea4ca789123b52f421c57aa2 rewritten
101 ◆ 0000000000000000000000000000000000000000
102 [EOF]
103 ------- stderr -------
104 Concurrent modification detected, resolving automatically.
105 Rebased 1 descendant commits onto commits rewritten by other operation
106 [EOF]
107 ");
108}
109
110#[test]
111fn test_concurrent_operations_wc_modified() {
112 let test_env = TestEnvironment::default();
113 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
114 let work_dir = test_env.work_dir("repo");
115
116 work_dir.write_file("file", "contents\n");
117 work_dir.run_jj(["describe", "-m", "initial"]).success();
118 let output = work_dir.run_jj(["op", "log"]).success();
119 let op_id_hex = output.stdout.raw()[3..15].to_string();
120
121 work_dir
122 .run_jj(["new", "--at-op", &op_id_hex, "-m", "new child1"])
123 .success();
124 work_dir
125 .run_jj(["new", "--at-op", &op_id_hex, "-m", "new child2"])
126 .success();
127 work_dir.write_file("file", "modified\n");
128
129 // We should be informed about the concurrent modification
130 let output = get_log_output(&work_dir);
131 insta::assert_snapshot!(output, @r"
132 @ 4eadcf3df11f46ef3d825c776496221cc8303053 new child1
133 │ ○ 68119f1643b7e3c301c5f7c2b6c9bf4ccba87379 new child2
134 ├─╯
135 ○ 2ff7ae858a3a11837fdf9d1a76be295ef53f1bb3 initial
136 ◆ 0000000000000000000000000000000000000000
137 [EOF]
138 ------- stderr -------
139 Concurrent modification detected, resolving automatically.
140 [EOF]
141 ");
142 let output = work_dir.run_jj(["diff", "--git"]);
143 insta::assert_snapshot!(output, @r"
144 diff --git a/file b/file
145 index 12f00e90b6..2e0996000b 100644
146 --- a/file
147 +++ b/file
148 @@ -1,1 +1,1 @@
149 -contents
150 +modified
151 [EOF]
152 ");
153
154 // The working copy should be committed after merging the operations
155 let output = work_dir.run_jj(["op", "log", "-Tdescription"]);
156 insta::assert_snapshot!(output, @r"
157 @ snapshot working copy
158 ○ reconcile divergent operations
159 ├─╮
160 ○ │ new empty commit
161 │ ○ new empty commit
162 ├─╯
163 ○ describe commit 506f4ec3c2c62befa15fabc34ca9d4e6d7bef254
164 ○ snapshot working copy
165 ○ add workspace 'default'
166 ○
167 [EOF]
168 ");
169}
170
171#[test]
172fn test_concurrent_snapshot_wc_reloadable() {
173 let test_env = TestEnvironment::default();
174 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
175 let work_dir = test_env.work_dir("repo");
176 let op_heads_dir = work_dir
177 .root()
178 .join(".jj")
179 .join("repo")
180 .join("op_heads")
181 .join("heads");
182
183 work_dir.write_file("base", "");
184 work_dir.run_jj(["commit", "-m", "initial"]).success();
185
186 // Create new commit and checkout it.
187 work_dir.write_file("child1", "");
188 work_dir.run_jj(["commit", "-m", "new child1"]).success();
189
190 let template = r#"id ++ "\n" ++ description ++ "\n" ++ tags"#;
191 let output = work_dir.run_jj(["op", "log", "-T", template]);
192 insta::assert_snapshot!(output, @r"
193 @ ec6bf266624bbaed55833a34ae62fa95c0e9efa651b94eb28846972da645845052dcdc8580332a5628849f23f48b9e99fc728dc3fb13106df8d0666d746f8b85
194 │ commit 554d22b2c43c1c47e279430197363e8daabe2fd6
195 │ args: jj commit -m 'new child1'
196 ○ 23858df860b789e8176a73c0eb21804e3f1848f26d68b70d234c004d08980c41499b6669042bca20fbc2543c437222a084c7cd473e91c7a9a095a02bf38544ab
197 │ snapshot working copy
198 │ args: jj commit -m 'new child1'
199 ○ e1db5fa988fc66e5cc0491b00c53fb93e25e730341c850cb42e1e0db0c76d2b4065005787563301b1d292c104f381918897f7deabeb92d2532f42ce75d3fe588
200 │ commit de71e09289762a65f80bb1c3dae2a949df6bcde7
201 │ args: jj commit -m initial
202 ○ 7de878155a459b7751097222132c935f9dcbb8f69a72b0f3a9036345a963010a553dc7c92964220128679ead72b087ca3aaf4ab9e20a221d1ffa4f9e92a32193
203 │ snapshot working copy
204 │ args: jj commit -m initial
205 ○ eac759b9ab75793fd3da96e60939fb48f2cd2b2a9c1f13ffe723cf620f3005b8d3e7e923634a07ea39513e4f2f360c87b9ad5d331cf90d7a844864b83b72eba1
206 │ add workspace 'default'
207 ○ 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
208
209 [EOF]
210 ");
211 let op_log_lines = output.stdout.raw().lines().collect_vec();
212 let current_op_id = op_log_lines[0].split_once(" ").unwrap().1;
213 let previous_op_id = op_log_lines[6].split_once(" ").unwrap().1;
214
215 // Another process started from the "initial" operation, but snapshots after
216 // the "child1" checkout has been completed.
217 std::fs::rename(
218 op_heads_dir.join(current_op_id),
219 op_heads_dir.join(previous_op_id),
220 )
221 .unwrap();
222 work_dir.write_file("child2", "");
223 let output = work_dir.run_jj(["describe", "-m", "new child2"]);
224 insta::assert_snapshot!(output, @r"
225 ------- stderr -------
226 Working copy (@) now at: kkmpptxz 1795621b new child2
227 Parent commit (@-) : rlvkpnrz 86f54245 new child1
228 [EOF]
229 ");
230
231 // Since the repo can be reloaded before snapshotting, "child2" should be
232 // a child of "child1", not of "initial".
233 let template = r#"commit_id ++ " " ++ description"#;
234 let output = work_dir.run_jj(["log", "-T", template, "-s"]);
235 insta::assert_snapshot!(output, @r"
236 @ 1795621b54f4ebb435978b65d66bc0f90d8f20b6 new child2
237 │ A child2
238 ○ 86f54245e13f850f8275b5541e56da996b6a47b7 new child1
239 │ A child1
240 ○ 84f07f6bca2ffeddac84a8b09f60c6b81112375c initial
241 │ A base
242 ◆ 0000000000000000000000000000000000000000
243 [EOF]
244 ");
245}
246
247#[must_use]
248fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {
249 let template = r#"commit_id ++ " " ++ description"#;
250 work_dir.run_jj(["log", "-T", template])
251}