just playing with tangled
1// Copyright 2024 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 crate::common::CommandOutput;
16use crate::common::TestEnvironment;
17use crate::common::TestWorkDir;
18
19#[test]
20fn test_absorb_simple() {
21 let test_env = TestEnvironment::default();
22 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
23 let work_dir = test_env.work_dir("repo");
24
25 work_dir.run_jj(["describe", "-m0"]).success();
26 work_dir.write_file("file1", "");
27
28 work_dir.run_jj(["new", "-m1"]).success();
29 work_dir.write_file("file1", "1a\n1b\n");
30
31 work_dir.run_jj(["new", "-m2"]).success();
32 work_dir.write_file("file1", "1a\n1b\n2a\n2b\n");
33
34 // Empty commit
35 work_dir.run_jj(["new"]).success();
36 let output = work_dir.run_jj(["absorb"]);
37 insta::assert_snapshot!(output, @r"
38 ------- stderr -------
39 Nothing changed.
40 [EOF]
41 ");
42
43 // Insert first and last lines
44 work_dir.write_file("file1", "1X\n1a\n1b\n2a\n2b\n2Z\n");
45 let output = work_dir.run_jj(["absorb"]);
46 insta::assert_snapshot!(output, @r"
47 ------- stderr -------
48 Absorbed changes into 2 revisions:
49 zsuskuln 95568809 2
50 kkmpptxz bd7d4016 1
51 Working copy (@) now at: yqosqzyt 977269ac (empty) (no description set)
52 Parent commit (@-) : zsuskuln 95568809 2
53 [EOF]
54 ");
55
56 // Modify middle line in hunk
57 work_dir.write_file("file1", "1X\n1A\n1b\n2a\n2b\n2Z\n");
58 let output = work_dir.run_jj(["absorb"]);
59 insta::assert_snapshot!(output, @r"
60 ------- stderr -------
61 Absorbed changes into 1 revisions:
62 kkmpptxz 5810eb0f 1
63 Rebased 1 descendant commits.
64 Working copy (@) now at: vruxwmqv 48c7d8fa (empty) (no description set)
65 Parent commit (@-) : zsuskuln 8edd60a2 2
66 [EOF]
67 ");
68
69 // Remove middle line from hunk
70 work_dir.write_file("file1", "1X\n1A\n1b\n2a\n2Z\n");
71 let output = work_dir.run_jj(["absorb"]);
72 insta::assert_snapshot!(output, @r"
73 ------- stderr -------
74 Absorbed changes into 1 revisions:
75 zsuskuln dd109863 2
76 Working copy (@) now at: yostqsxw 7482f74b (empty) (no description set)
77 Parent commit (@-) : zsuskuln dd109863 2
78 [EOF]
79 ");
80
81 // Insert ambiguous line in between
82 work_dir.write_file("file1", "1X\n1A\n1b\nY\n2a\n2Z\n");
83 let output = work_dir.run_jj(["absorb"]);
84 insta::assert_snapshot!(output, @r"
85 ------- stderr -------
86 Nothing changed.
87 [EOF]
88 ");
89
90 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
91 @ yostqsxw bde51bc9 (no description set)
92 │ diff --git a/file1 b/file1
93 │ index 8653ca354d..88eb438902 100644
94 │ --- a/file1
95 │ +++ b/file1
96 │ @@ -1,5 +1,6 @@
97 │ 1X
98 │ 1A
99 │ 1b
100 │ +Y
101 │ 2a
102 │ 2Z
103 ○ zsuskuln dd109863 2
104 │ diff --git a/file1 b/file1
105 │ index ed237b5112..8653ca354d 100644
106 │ --- a/file1
107 │ +++ b/file1
108 │ @@ -1,3 +1,5 @@
109 │ 1X
110 │ 1A
111 │ 1b
112 │ +2a
113 │ +2Z
114 ○ kkmpptxz 5810eb0f 1
115 │ diff --git a/file1 b/file1
116 │ index e69de29bb2..ed237b5112 100644
117 │ --- a/file1
118 │ +++ b/file1
119 │ @@ -0,0 +1,3 @@
120 │ +1X
121 │ +1A
122 │ +1b
123 ○ qpvuntsm 6a446874 0
124 │ diff --git a/file1 b/file1
125 ~ new file mode 100644
126 index 0000000000..e69de29bb2
127 [EOF]
128 ");
129 insta::assert_snapshot!(get_evolog(&work_dir, "description(1)"), @r"
130 ○ kkmpptxz 5810eb0f 1
131 ├─╮ -- operation 5876e0f3d35d (2001-02-03 08:05:14) absorb changes into 1 commits
132 │ ○ yqosqzyt hidden 39b42898 (no description set)
133 │ │ -- operation a2c449e239df (2001-02-03 08:05:14) snapshot working copy
134 │ ○ yqosqzyt hidden 977269ac (empty) (no description set)
135 │ -- operation 4a9cb11bbdd5 (2001-02-03 08:05:13) absorb changes into 2 commits
136 ○ kkmpptxz hidden bd7d4016 1
137 ├─╮ -- operation 4a9cb11bbdd5 (2001-02-03 08:05:13) absorb changes into 2 commits
138 │ ○ mzvwutvl hidden 0b307741 (no description set)
139 │ │ -- operation 51ebffcb116e (2001-02-03 08:05:13) snapshot working copy
140 │ ○ mzvwutvl hidden f2709b4e (empty) (no description set)
141 │ -- operation b92e661fdac1 (2001-02-03 08:05:11) new empty commit
142 ○ kkmpptxz hidden 1553c5e8 1
143 │ -- operation 35926ea345b0 (2001-02-03 08:05:10) snapshot working copy
144 ○ kkmpptxz hidden eb943711 (empty) 1
145 -- operation da1318a72167 (2001-02-03 08:05:09) new empty commit
146 [EOF]
147 ");
148 insta::assert_snapshot!(get_evolog(&work_dir, "description(2)"), @r"
149 ○ zsuskuln dd109863 2
150 ├─╮ -- operation fc078244f126 (2001-02-03 08:05:15) absorb changes into 1 commits
151 │ ○ vruxwmqv hidden 761492a8 (no description set)
152 │ │ -- operation b15694dee324 (2001-02-03 08:05:15) snapshot working copy
153 │ ○ vruxwmqv hidden 48c7d8fa (empty) (no description set)
154 │ -- operation 5876e0f3d35d (2001-02-03 08:05:14) absorb changes into 1 commits
155 ○ zsuskuln hidden 8edd60a2 2
156 │ -- operation 5876e0f3d35d (2001-02-03 08:05:14) absorb changes into 1 commits
157 ○ zsuskuln hidden 95568809 2
158 ├─╮ -- operation 4a9cb11bbdd5 (2001-02-03 08:05:13) absorb changes into 2 commits
159 │ ○ mzvwutvl hidden 0b307741 (no description set)
160 │ │ -- operation 51ebffcb116e (2001-02-03 08:05:13) snapshot working copy
161 │ ○ mzvwutvl hidden f2709b4e (empty) (no description set)
162 │ -- operation b92e661fdac1 (2001-02-03 08:05:11) new empty commit
163 ○ zsuskuln hidden 36fad385 2
164 │ -- operation ab92cd8883ff (2001-02-03 08:05:11) snapshot working copy
165 ○ zsuskuln hidden 561fbce9 (empty) 2
166 -- operation 9339ac427cfc (2001-02-03 08:05:10) new empty commit
167 [EOF]
168 ");
169}
170
171#[test]
172fn test_absorb_replace_single_line_hunk() {
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
177 work_dir.run_jj(["describe", "-m1"]).success();
178 work_dir.write_file("file1", "1a\n");
179
180 work_dir.run_jj(["new", "-m2"]).success();
181 work_dir.write_file("file1", "2a\n1a\n2b\n");
182
183 // Replace single-line hunk, which produces a conflict right now. If our
184 // merge logic were based on interleaved delta, the hunk would be applied
185 // cleanly.
186 work_dir.run_jj(["new"]).success();
187 work_dir.write_file("file1", "2a\n1A\n2b\n");
188 let output = work_dir.run_jj(["absorb"]);
189 insta::assert_snapshot!(output, @r###"
190 ------- stderr -------
191 Absorbed changes into 1 revisions:
192 qpvuntsm 5bdb5ca1 (conflict) 1
193 Rebased 1 descendant commits.
194 Working copy (@) now at: mzvwutvl 804fe9d9 (empty) (no description set)
195 Parent commit (@-) : kkmpptxz 6068e8fc 2
196 New conflicts appeared in 1 commits:
197 qpvuntsm 5bdb5ca1 (conflict) 1
198 Hint: To resolve the conflicts, start by creating a commit on top of
199 the conflicted commit:
200 jj new qpvuntsm
201 Then use `jj resolve`, or edit the conflict markers in the file directly.
202 Once the conflicts are resolved, you can inspect the result with `jj diff`.
203 Then run `jj squash` to move the resolution into the conflicted commit.
204 [EOF]
205 "###);
206
207 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
208 @ mzvwutvl 804fe9d9 (empty) (no description set)
209 ○ kkmpptxz 6068e8fc 2
210 │ diff --git a/file1 b/file1
211 │ index 0000000000..2f87e8e465 100644
212 │ --- a/file1
213 │ +++ b/file1
214 │ @@ -1,10 +1,3 @@
215 │ -<<<<<<< Conflict 1 of 1
216 │ -%%%%%%% Changes from base to side #1
217 │ --2a
218 │ - 1a
219 │ --2b
220 │ -+++++++ Contents of side #2
221 │ 2a
222 │ 1A
223 │ 2b
224 │ ->>>>>>> Conflict 1 of 1 ends
225 × qpvuntsm 5bdb5ca1 (conflict) 1
226 │ diff --git a/file1 b/file1
227 ~ new file mode 100644
228 index 0000000000..0000000000
229 --- /dev/null
230 +++ b/file1
231 @@ -0,0 +1,10 @@
232 +<<<<<<< Conflict 1 of 1
233 +%%%%%%% Changes from base to side #1
234 +-2a
235 + 1a
236 +-2b
237 ++++++++ Contents of side #2
238 +2a
239 +1A
240 +2b
241 +>>>>>>> Conflict 1 of 1 ends
242 [EOF]
243 ");
244}
245
246#[test]
247fn test_absorb_merge() {
248 let test_env = TestEnvironment::default();
249 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
250 let work_dir = test_env.work_dir("repo");
251
252 work_dir.run_jj(["describe", "-m0"]).success();
253 work_dir.write_file("file1", "0a\n");
254
255 work_dir.run_jj(["new", "-m1"]).success();
256 work_dir.write_file("file1", "1a\n1b\n0a\n");
257
258 work_dir.run_jj(["new", "-m2", "description(0)"]).success();
259 work_dir.write_file("file1", "0a\n2a\n2b\n");
260
261 let output = work_dir.run_jj(["new", "-m3", "description(1)", "description(2)"]);
262 insta::assert_snapshot!(output, @r"
263 ------- stderr -------
264 Working copy (@) now at: mzvwutvl 42875bf7 (empty) 3
265 Parent commit (@-) : kkmpptxz 9c66f62f 1
266 Parent commit (@-) : zsuskuln 6a3dcbcf 2
267 Added 0 files, modified 1 files, removed 0 files
268 [EOF]
269 ");
270
271 // Modify first and last lines, absorb from merge
272 work_dir.write_file("file1", "1A\n1b\n0a\n2a\n2B\n");
273 let output = work_dir.run_jj(["absorb"]);
274 insta::assert_snapshot!(output, @r"
275 ------- stderr -------
276 Absorbed changes into 2 revisions:
277 zsuskuln a6fde7ea 2
278 kkmpptxz 00ecc958 1
279 Rebased 1 descendant commits.
280 Working copy (@) now at: mzvwutvl 30499858 (empty) 3
281 Parent commit (@-) : kkmpptxz 00ecc958 1
282 Parent commit (@-) : zsuskuln a6fde7ea 2
283 [EOF]
284 ");
285
286 // Add hunk to merge revision
287 work_dir.write_file("file2", "3a\n");
288
289 // Absorb into merge
290 work_dir.run_jj(["new"]).success();
291 work_dir.write_file("file2", "3A\n");
292 let output = work_dir.run_jj(["absorb"]);
293 insta::assert_snapshot!(output, @r"
294 ------- stderr -------
295 Absorbed changes into 1 revisions:
296 mzvwutvl faf778a4 3
297 Working copy (@) now at: vruxwmqv cec519a1 (empty) (no description set)
298 Parent commit (@-) : mzvwutvl faf778a4 3
299 [EOF]
300 ");
301
302 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
303 @ vruxwmqv cec519a1 (empty) (no description set)
304 ○ mzvwutvl faf778a4 3
305 ├─╮ diff --git a/file2 b/file2
306 │ │ new file mode 100644
307 │ │ index 0000000000..44442d2d7b
308 │ │ --- /dev/null
309 │ │ +++ b/file2
310 │ │ @@ -0,0 +1,1 @@
311 │ │ +3A
312 │ ○ zsuskuln a6fde7ea 2
313 │ │ diff --git a/file1 b/file1
314 │ │ index eb6e8821f1..4907935b9f 100644
315 │ │ --- a/file1
316 │ │ +++ b/file1
317 │ │ @@ -1,1 +1,3 @@
318 │ │ 0a
319 │ │ +2a
320 │ │ +2B
321 ○ │ kkmpptxz 00ecc958 1
322 ├─╯ diff --git a/file1 b/file1
323 │ index eb6e8821f1..902dd8ef13 100644
324 │ --- a/file1
325 │ +++ b/file1
326 │ @@ -1,1 +1,3 @@
327 │ +1A
328 │ +1b
329 │ 0a
330 ○ qpvuntsm d4f07be5 0
331 │ diff --git a/file1 b/file1
332 ~ new file mode 100644
333 index 0000000000..eb6e8821f1
334 --- /dev/null
335 +++ b/file1
336 @@ -0,0 +1,1 @@
337 +0a
338 [EOF]
339 ");
340}
341
342#[test]
343fn test_absorb_discardable_merge_with_descendant() {
344 let test_env = TestEnvironment::default();
345 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
346 let work_dir = test_env.work_dir("repo");
347
348 work_dir.run_jj(["describe", "-m0"]).success();
349 work_dir.write_file("file1", "0a\n");
350
351 work_dir.run_jj(["new", "-m1"]).success();
352 work_dir.write_file("file1", "1a\n1b\n0a\n");
353
354 work_dir.run_jj(["new", "-m2", "description(0)"]).success();
355 work_dir.write_file("file1", "0a\n2a\n2b\n");
356
357 let output = work_dir.run_jj(["new", "description(1)", "description(2)"]);
358 insta::assert_snapshot!(output, @r"
359 ------- stderr -------
360 Working copy (@) now at: mzvwutvl ad00b91a (empty) (no description set)
361 Parent commit (@-) : kkmpptxz 9c66f62f 1
362 Parent commit (@-) : zsuskuln 6a3dcbcf 2
363 Added 0 files, modified 1 files, removed 0 files
364 [EOF]
365 ");
366
367 // Modify first and last lines in the merge commit
368 work_dir.write_file("file1", "1A\n1b\n0a\n2a\n2B\n");
369 // Add new commit on top
370 work_dir.run_jj(["new", "-m3"]).success();
371 work_dir.write_file("file2", "3a\n");
372 // Then absorb the merge commit
373 let output = work_dir.run_jj(["absorb", "--from=@-"]);
374 insta::assert_snapshot!(output, @r"
375 ------- stderr -------
376 Absorbed changes into 2 revisions:
377 zsuskuln a6cd8e87 2
378 kkmpptxz 98b7d214 1
379 Rebased 1 descendant commits.
380 Working copy (@) now at: royxmykx df946e9b 3
381 Parent commit (@-) : kkmpptxz 98b7d214 1
382 Parent commit (@-) : zsuskuln a6cd8e87 2
383 [EOF]
384 ");
385
386 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
387 @ royxmykx df946e9b 3
388 ├─╮ diff --git a/file2 b/file2
389 │ │ new file mode 100644
390 │ │ index 0000000000..31cd755d20
391 │ │ --- /dev/null
392 │ │ +++ b/file2
393 │ │ @@ -0,0 +1,1 @@
394 │ │ +3a
395 │ ○ zsuskuln a6cd8e87 2
396 │ │ diff --git a/file1 b/file1
397 │ │ index eb6e8821f1..4907935b9f 100644
398 │ │ --- a/file1
399 │ │ +++ b/file1
400 │ │ @@ -1,1 +1,3 @@
401 │ │ 0a
402 │ │ +2a
403 │ │ +2B
404 ○ │ kkmpptxz 98b7d214 1
405 ├─╯ diff --git a/file1 b/file1
406 │ index eb6e8821f1..902dd8ef13 100644
407 │ --- a/file1
408 │ +++ b/file1
409 │ @@ -1,1 +1,3 @@
410 │ +1A
411 │ +1b
412 │ 0a
413 ○ qpvuntsm d4f07be5 0
414 │ diff --git a/file1 b/file1
415 ~ new file mode 100644
416 index 0000000000..eb6e8821f1
417 --- /dev/null
418 +++ b/file1
419 @@ -0,0 +1,1 @@
420 +0a
421 [EOF]
422 ");
423}
424
425#[test]
426fn test_absorb_conflict() {
427 let test_env = TestEnvironment::default();
428 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
429 let work_dir = test_env.work_dir("repo");
430
431 work_dir.run_jj(["describe", "-m1"]).success();
432 work_dir.write_file("file1", "1a\n1b\n");
433
434 work_dir.run_jj(["new", "root()"]).success();
435 work_dir.write_file("file1", "2a\n2b\n");
436 let output = work_dir.run_jj(["rebase", "-r@", "-ddescription(1)"]);
437 insta::assert_snapshot!(output, @r###"
438 ------- stderr -------
439 Rebased 1 commits to destination
440 Working copy (@) now at: kkmpptxz 66d44b8c (conflict) (no description set)
441 Parent commit (@-) : qpvuntsm e35bcaff 1
442 Added 0 files, modified 1 files, removed 0 files
443 Warning: There are unresolved conflicts at these paths:
444 file1 2-sided conflict
445 New conflicts appeared in 1 commits:
446 kkmpptxz 66d44b8c (conflict) (no description set)
447 Hint: To resolve the conflicts, start by creating a commit on top of
448 the conflicted commit:
449 jj new kkmpptxz
450 Then use `jj resolve`, or edit the conflict markers in the file directly.
451 Once the conflicts are resolved, you can inspect the result with `jj diff`.
452 Then run `jj squash` to move the resolution into the conflicted commit.
453 [EOF]
454 "###);
455
456 let conflict_content = work_dir.read_file("file1");
457 insta::assert_snapshot!(conflict_content, @r"
458 <<<<<<< Conflict 1 of 1
459 %%%%%%% Changes from base to side #1
460 +1a
461 +1b
462 +++++++ Contents of side #2
463 2a
464 2b
465 >>>>>>> Conflict 1 of 1 ends
466 ");
467
468 // Cannot absorb from conflict
469 let output = work_dir.run_jj(["absorb"]);
470 insta::assert_snapshot!(output, @r"
471 ------- stderr -------
472 Warning: Skipping file1: Is a conflict
473 Nothing changed.
474 [EOF]
475 ");
476
477 // Cannot absorb from resolved conflict
478 work_dir.run_jj(["new"]).success();
479 work_dir.write_file("file1", "1A\n1b\n2a\n2B\n");
480 let output = work_dir.run_jj(["absorb"]);
481 insta::assert_snapshot!(output, @r"
482 ------- stderr -------
483 Warning: Skipping file1: Is a conflict
484 Nothing changed.
485 [EOF]
486 ");
487}
488
489#[test]
490fn test_absorb_deleted_file() {
491 let test_env = TestEnvironment::default();
492 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
493 let work_dir = test_env.work_dir("repo");
494
495 work_dir.run_jj(["describe", "-m1"]).success();
496 work_dir.write_file("file1", "1a\n");
497 work_dir.write_file("file2", "1a\n");
498 work_dir.write_file("file3", "");
499
500 work_dir.run_jj(["new"]).success();
501 work_dir.remove_file("file1");
502 work_dir.write_file("file2", ""); // emptied
503 work_dir.remove_file("file3"); // no content change
504
505 // Since the destinations are chosen based on content diffs, file3 cannot be
506 // absorbed.
507 let output = work_dir.run_jj(["absorb"]);
508 insta::assert_snapshot!(output, @r"
509 ------- stderr -------
510 Absorbed changes into 1 revisions:
511 qpvuntsm 38af7fd3 1
512 Rebased 1 descendant commits.
513 Working copy (@) now at: kkmpptxz efd883f6 (no description set)
514 Parent commit (@-) : qpvuntsm 38af7fd3 1
515 Remaining changes:
516 D file3
517 [EOF]
518 ");
519
520 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
521 @ kkmpptxz efd883f6 (no description set)
522 │ diff --git a/file3 b/file3
523 │ deleted file mode 100644
524 │ index e69de29bb2..0000000000
525 ○ qpvuntsm 38af7fd3 1
526 │ diff --git a/file2 b/file2
527 ~ new file mode 100644
528 index 0000000000..e69de29bb2
529 diff --git a/file3 b/file3
530 new file mode 100644
531 index 0000000000..e69de29bb2
532 [EOF]
533 ");
534}
535
536#[test]
537fn test_absorb_deleted_file_with_multiple_hunks() {
538 let test_env = TestEnvironment::default();
539 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
540 let work_dir = test_env.work_dir("repo");
541
542 work_dir.run_jj(["describe", "-m1"]).success();
543 work_dir.write_file("file1", "1a\n1b\n");
544 work_dir.write_file("file2", "1a\n");
545
546 work_dir.run_jj(["new", "-m2"]).success();
547 work_dir.write_file("file1", "1a\n");
548 work_dir.write_file("file2", "1a\n1b\n");
549
550 // These changes produce conflicts because
551 // - for file1, "1a\n" is deleted from the commit 1,
552 // - for file2, two consecutive hunks are deleted.
553 //
554 // Since file2 change is split to two separate hunks, the file deletion
555 // cannot be propagated. If we implement merging based on interleaved delta,
556 // the file2 change will apply cleanly. The file1 change might be split into
557 // "1a\n" deletion at the commit 1 and file deletion at the commit 2, but
558 // I'm not sure if that's intuitive.
559 work_dir.run_jj(["new"]).success();
560 work_dir.remove_file("file1");
561 work_dir.remove_file("file2");
562 let output = work_dir.run_jj(["absorb"]);
563 insta::assert_snapshot!(output, @r###"
564 ------- stderr -------
565 Absorbed changes into 2 revisions:
566 kkmpptxz 8407ab95 (conflict) 2
567 qpvuntsm f1473264 (conflict) 1
568 Rebased 1 descendant commits.
569 Working copy (@) now at: zsuskuln b56f0c39 (no description set)
570 Parent commit (@-) : kkmpptxz 8407ab95 (conflict) 2
571 New conflicts appeared in 2 commits:
572 kkmpptxz 8407ab95 (conflict) 2
573 qpvuntsm f1473264 (conflict) 1
574 Hint: To resolve the conflicts, start by creating a commit on top of
575 the first conflicted commit:
576 jj new qpvuntsm
577 Then use `jj resolve`, or edit the conflict markers in the file directly.
578 Once the conflicts are resolved, you can inspect the result with `jj diff`.
579 Then run `jj squash` to move the resolution into the conflicted commit.
580 Remaining changes:
581 D file2
582 [EOF]
583 "###);
584
585 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
586 @ zsuskuln b56f0c39 (no description set)
587 │ diff --git a/file2 b/file2
588 │ deleted file mode 100644
589 │ index 0000000000..0000000000
590 │ --- a/file2
591 │ +++ /dev/null
592 │ @@ -1,7 +0,0 @@
593 │ -<<<<<<< Conflict 1 of 1
594 │ -%%%%%%% Changes from base to side #1
595 │ --1a
596 │ - 1b
597 │ -+++++++ Contents of side #2
598 │ -1a
599 │ ->>>>>>> Conflict 1 of 1 ends
600 × kkmpptxz 8407ab95 (conflict) 2
601 │ diff --git a/file1 b/file1
602 │ deleted file mode 100644
603 │ index 0000000000..0000000000
604 │ --- a/file1
605 │ +++ /dev/null
606 │ @@ -1,6 +0,0 @@
607 │ -<<<<<<< Conflict 1 of 1
608 │ -%%%%%%% Changes from base to side #1
609 │ - 1a
610 │ -+1b
611 │ -+++++++ Contents of side #2
612 │ ->>>>>>> Conflict 1 of 1 ends
613 │ diff --git a/file2 b/file2
614 │ --- a/file2
615 │ +++ b/file2
616 │ @@ -1,7 +1,7 @@
617 │ <<<<<<< Conflict 1 of 1
618 │ %%%%%%% Changes from base to side #1
619 │ - 1a
620 │ --1b
621 │ +-1a
622 │ + 1b
623 │ +++++++ Contents of side #2
624 │ -1b
625 │ +1a
626 │ >>>>>>> Conflict 1 of 1 ends
627 × qpvuntsm f1473264 (conflict) 1
628 │ diff --git a/file1 b/file1
629 ~ new file mode 100644
630 index 0000000000..0000000000
631 --- /dev/null
632 +++ b/file1
633 @@ -0,0 +1,6 @@
634 +<<<<<<< Conflict 1 of 1
635 +%%%%%%% Changes from base to side #1
636 + 1a
637 ++1b
638 ++++++++ Contents of side #2
639 +>>>>>>> Conflict 1 of 1 ends
640 diff --git a/file2 b/file2
641 new file mode 100644
642 index 0000000000..0000000000
643 --- /dev/null
644 +++ b/file2
645 @@ -0,0 +1,7 @@
646 +<<<<<<< Conflict 1 of 1
647 +%%%%%%% Changes from base to side #1
648 + 1a
649 +-1b
650 ++++++++ Contents of side #2
651 +1b
652 +>>>>>>> Conflict 1 of 1 ends
653 [EOF]
654 ");
655}
656
657#[test]
658fn test_absorb_file_mode() {
659 let test_env = TestEnvironment::default();
660 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
661 let work_dir = test_env.work_dir("repo");
662
663 work_dir.run_jj(["describe", "-m1"]).success();
664 work_dir.write_file("file1", "1a\n");
665 work_dir.run_jj(["file", "chmod", "x", "file1"]).success();
666
667 // Modify content and mode
668 work_dir.run_jj(["new"]).success();
669 work_dir.write_file("file1", "1A\n");
670 work_dir.run_jj(["file", "chmod", "n", "file1"]).success();
671
672 // Mode change shouldn't be absorbed
673 let output = work_dir.run_jj(["absorb"]);
674 insta::assert_snapshot!(output, @r"
675 ------- stderr -------
676 Absorbed changes into 1 revisions:
677 qpvuntsm 2a0c7f1d 1
678 Rebased 1 descendant commits.
679 Working copy (@) now at: zsuskuln 8ca9761d (no description set)
680 Parent commit (@-) : qpvuntsm 2a0c7f1d 1
681 Remaining changes:
682 M file1
683 [EOF]
684 ");
685
686 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
687 @ zsuskuln 8ca9761d (no description set)
688 │ diff --git a/file1 b/file1
689 │ old mode 100755
690 │ new mode 100644
691 ○ qpvuntsm 2a0c7f1d 1
692 │ diff --git a/file1 b/file1
693 ~ new file mode 100755
694 index 0000000000..268de3f3ec
695 --- /dev/null
696 +++ b/file1
697 @@ -0,0 +1,1 @@
698 +1A
699 [EOF]
700 ");
701}
702
703#[test]
704fn test_absorb_from_into() {
705 let test_env = TestEnvironment::default();
706 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
707 let work_dir = test_env.work_dir("repo");
708
709 work_dir.run_jj(["new", "-m1"]).success();
710 work_dir.write_file("file1", "1a\n1b\n1c\n");
711
712 work_dir.run_jj(["new", "-m2"]).success();
713 work_dir.write_file("file1", "1a\n2a\n1b\n1c\n2b\n");
714
715 // Line "X" and "Z" have unambiguous adjacent line within the destinations
716 // range. Line "Y" doesn't have such line.
717 work_dir.run_jj(["new"]).success();
718 work_dir.write_file("file1", "1a\nX\n2a\n1b\nY\n1c\n2b\nZ\n");
719 let output = work_dir.run_jj(["absorb", "--into=@-"]);
720 insta::assert_snapshot!(output, @r"
721 ------- stderr -------
722 Absorbed changes into 1 revisions:
723 kkmpptxz cae507ef 2
724 Rebased 1 descendant commits.
725 Working copy (@) now at: zsuskuln f02fd9ea (no description set)
726 Parent commit (@-) : kkmpptxz cae507ef 2
727 Remaining changes:
728 M file1
729 [EOF]
730 ");
731
732 insta::assert_snapshot!(get_diffs(&work_dir, "@-::"), @r"
733 @ zsuskuln f02fd9ea (no description set)
734 │ diff --git a/file1 b/file1
735 │ index faf62af049..c2d0b12547 100644
736 │ --- a/file1
737 │ +++ b/file1
738 │ @@ -2,6 +2,7 @@
739 │ X
740 │ 2a
741 │ 1b
742 │ +Y
743 │ 1c
744 │ 2b
745 │ Z
746 ○ kkmpptxz cae507ef 2
747 │ diff --git a/file1 b/file1
748 ~ index 352e9b3794..faf62af049 100644
749 --- a/file1
750 +++ b/file1
751 @@ -1,3 +1,7 @@
752 1a
753 +X
754 +2a
755 1b
756 1c
757 +2b
758 +Z
759 [EOF]
760 ");
761
762 // Absorb all lines from the working-copy parent. An empty commit won't be
763 // discarded because "absorb" isn't a command to squash commit descriptions.
764 let output = work_dir.run_jj(["absorb", "--from=@-"]);
765 insta::assert_snapshot!(output, @r"
766 ------- stderr -------
767 Absorbed changes into 1 revisions:
768 rlvkpnrz ddaed33d 1
769 Rebased 2 descendant commits.
770 Working copy (@) now at: zsuskuln 3652e5e5 (no description set)
771 Parent commit (@-) : kkmpptxz 7f4339e7 (empty) 2
772 [EOF]
773 ");
774
775 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
776 @ zsuskuln 3652e5e5 (no description set)
777 │ diff --git a/file1 b/file1
778 │ index faf62af049..c2d0b12547 100644
779 │ --- a/file1
780 │ +++ b/file1
781 │ @@ -2,6 +2,7 @@
782 │ X
783 │ 2a
784 │ 1b
785 │ +Y
786 │ 1c
787 │ 2b
788 │ Z
789 ○ kkmpptxz 7f4339e7 (empty) 2
790 ○ rlvkpnrz ddaed33d 1
791 │ diff --git a/file1 b/file1
792 │ new file mode 100644
793 │ index 0000000000..faf62af049
794 │ --- /dev/null
795 │ +++ b/file1
796 │ @@ -0,0 +1,7 @@
797 │ +1a
798 │ +X
799 │ +2a
800 │ +1b
801 │ +1c
802 │ +2b
803 │ +Z
804 ○ qpvuntsm e8849ae1 (empty) (no description set)
805 │
806 ~
807 [EOF]
808 ");
809}
810
811#[test]
812fn test_absorb_paths() {
813 let test_env = TestEnvironment::default();
814 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
815 let work_dir = test_env.work_dir("repo");
816
817 work_dir.run_jj(["describe", "-m1"]).success();
818 work_dir.write_file("file1", "1a\n");
819 work_dir.write_file("file2", "1a\n");
820
821 // Modify both files
822 work_dir.run_jj(["new"]).success();
823 work_dir.write_file("file1", "1A\n");
824 work_dir.write_file("file2", "1A\n");
825
826 let output = work_dir.run_jj(["absorb", "unknown"]);
827 insta::assert_snapshot!(output, @r"
828 ------- stderr -------
829 Nothing changed.
830 [EOF]
831 ");
832
833 let output = work_dir.run_jj(["absorb", "file1"]);
834 insta::assert_snapshot!(output, @r"
835 ------- stderr -------
836 Absorbed changes into 1 revisions:
837 qpvuntsm ca07fabe 1
838 Rebased 1 descendant commits.
839 Working copy (@) now at: kkmpptxz 4d80ada8 (no description set)
840 Parent commit (@-) : qpvuntsm ca07fabe 1
841 Remaining changes:
842 M file2
843 [EOF]
844 ");
845
846 insta::assert_snapshot!(get_diffs(&work_dir, "mutable()"), @r"
847 @ kkmpptxz 4d80ada8 (no description set)
848 │ diff --git a/file2 b/file2
849 │ index a8994dc188..268de3f3ec 100644
850 │ --- a/file2
851 │ +++ b/file2
852 │ @@ -1,1 +1,1 @@
853 │ -1a
854 │ +1A
855 ○ qpvuntsm ca07fabe 1
856 │ diff --git a/file1 b/file1
857 ~ new file mode 100644
858 index 0000000000..268de3f3ec
859 --- /dev/null
860 +++ b/file1
861 @@ -0,0 +1,1 @@
862 +1A
863 diff --git a/file2 b/file2
864 new file mode 100644
865 index 0000000000..a8994dc188
866 --- /dev/null
867 +++ b/file2
868 @@ -0,0 +1,1 @@
869 +1a
870 [EOF]
871 ");
872}
873
874#[test]
875fn test_absorb_immutable() {
876 let test_env = TestEnvironment::default();
877 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
878 let work_dir = test_env.work_dir("repo");
879 test_env.add_config("revset-aliases.'immutable_heads()' = 'present(main)'");
880
881 work_dir.run_jj(["describe", "-m1"]).success();
882 work_dir.write_file("file1", "1a\n1b\n");
883
884 work_dir.run_jj(["new", "-m2"]).success();
885 work_dir
886 .run_jj(["bookmark", "set", "-r@-", "main"])
887 .success();
888 work_dir.write_file("file1", "1a\n1b\n2a\n2b\n");
889
890 work_dir.run_jj(["new"]).success();
891 work_dir.write_file("file1", "1A\n1b\n2a\n2B\n");
892
893 // Immutable revisions are excluded by default
894 let output = work_dir.run_jj(["absorb"]);
895 insta::assert_snapshot!(output, @r"
896 ------- stderr -------
897 Absorbed changes into 1 revisions:
898 kkmpptxz e68cc3e2 2
899 Rebased 1 descendant commits.
900 Working copy (@) now at: mzvwutvl 88443af7 (no description set)
901 Parent commit (@-) : kkmpptxz e68cc3e2 2
902 Remaining changes:
903 M file1
904 [EOF]
905 ");
906
907 // Immutable revisions shouldn't be rewritten
908 let output = work_dir.run_jj(["absorb", "--into=all()"]);
909 insta::assert_snapshot!(output, @r#"
910 ------- stderr -------
911 Error: Commit e35bcaffcb55 is immutable
912 Hint: Could not modify commit: qpvuntsm e35bcaff main | 1
913 Hint: Immutable commits are used to protect shared history.
914 Hint: For more information, see:
915 - https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
916 - `jj help -k config`, "Set of immutable commits"
917 Hint: This operation would rewrite 1 immutable commits.
918 [EOF]
919 [exit status: 1]
920 "#);
921
922 insta::assert_snapshot!(get_diffs(&work_dir, ".."), @r"
923 @ mzvwutvl 88443af7 (no description set)
924 │ diff --git a/file1 b/file1
925 │ index 75e4047831..428796ca20 100644
926 │ --- a/file1
927 │ +++ b/file1
928 │ @@ -1,4 +1,4 @@
929 │ -1a
930 │ +1A
931 │ 1b
932 │ 2a
933 │ 2B
934 ○ kkmpptxz e68cc3e2 2
935 │ diff --git a/file1 b/file1
936 │ index 8c5268f893..75e4047831 100644
937 │ --- a/file1
938 │ +++ b/file1
939 │ @@ -1,2 +1,4 @@
940 │ 1a
941 │ 1b
942 │ +2a
943 │ +2B
944 ◆ qpvuntsm e35bcaff 1
945 │ diff --git a/file1 b/file1
946 ~ new file mode 100644
947 index 0000000000..8c5268f893
948 --- /dev/null
949 +++ b/file1
950 @@ -0,0 +1,2 @@
951 +1a
952 +1b
953 [EOF]
954 ");
955}
956
957#[must_use]
958fn get_diffs(work_dir: &TestWorkDir, revision: &str) -> CommandOutput {
959 let template = r#"format_commit_summary_with_refs(self, "") ++ "\n""#;
960 work_dir.run_jj(["log", "-r", revision, "-T", template, "--git"])
961}
962
963#[must_use]
964fn get_evolog(work_dir: &TestWorkDir, revision: &str) -> CommandOutput {
965 let template = r#"format_commit_summary_with_refs(self, "") ++ "\n""#;
966 work_dir.run_jj(["evolog", "-r", revision, "-T", template])
967}