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 std::path::Path;
16
17use indoc::indoc;
18
19use crate::common::create_commit_with_files;
20use crate::common::CommandOutput;
21use crate::common::TestEnvironment;
22use crate::common::TestWorkDir;
23
24#[must_use]
25fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {
26 work_dir.run_jj(["log", "-T", "bookmarks"])
27}
28
29#[test]
30fn test_resolution() {
31 let mut test_env = TestEnvironment::default();
32 let editor_script = test_env.set_up_fake_editor();
33 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
34 let work_dir = test_env.work_dir("repo");
35
36 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
37 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
38 create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
39 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
40 // Test the setup
41 insta::assert_snapshot!(get_log_output(&work_dir), @r"
42 @ conflict
43 ├─╮
44 │ ○ b
45 ○ │ a
46 ├─╯
47 ○ base
48 ◆
49 [EOF]
50 ");
51 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
52 file 2-sided conflict
53 [EOF]
54 ");
55 insta::assert_snapshot!(work_dir.read_file("file"), @r"
56 <<<<<<< Conflict 1 of 1
57 %%%%%%% Changes from base to side #1
58 -base
59 +a
60 +++++++ Contents of side #2
61 b
62 >>>>>>> Conflict 1 of 1 ends
63 ");
64
65 // Check that output file starts out empty and resolve the conflict
66 std::fs::write(
67 &editor_script,
68 ["dump editor0", "write\nresolution\n"].join("\0"),
69 )
70 .unwrap();
71 let output = work_dir.run_jj(["resolve"]);
72 insta::assert_snapshot!(output, @r"
73 ------- stderr -------
74 Resolving conflicts in: file
75 Working copy (@) now at: vruxwmqv e069f073 conflict | conflict
76 Parent commit (@-) : zsuskuln aa493daf a | a
77 Parent commit (@-) : royxmykx db6a4daf b | b
78 Added 0 files, modified 1 files, removed 0 files
79 [EOF]
80 ");
81 insta::assert_snapshot!(
82 std::fs::read_to_string(test_env.env_root().join("editor0")).unwrap(), @"");
83 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
84 diff --git a/file b/file
85 index 0000000000..88425ec521 100644
86 --- a/file
87 +++ b/file
88 @@ -1,7 +1,1 @@
89 -<<<<<<< Conflict 1 of 1
90 -%%%%%%% Changes from base to side #1
91 --base
92 -+a
93 -+++++++ Contents of side #2
94 -b
95 ->>>>>>> Conflict 1 of 1 ends
96 +resolution
97 [EOF]
98 ");
99 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
100 ------- stderr -------
101 Error: No conflicts found at this revision
102 [EOF]
103 [exit status: 2]
104 ");
105
106 // Try again with --tool=<name>
107 work_dir.run_jj(["undo"]).success();
108 std::fs::write(&editor_script, "write\nresolution\n").unwrap();
109 let output = work_dir.run_jj([
110 "resolve",
111 "--config=ui.merge-editor='false'",
112 "--tool=fake-editor",
113 ]);
114 insta::assert_snapshot!(output, @r"
115 ------- stderr -------
116 Resolving conflicts in: file
117 Working copy (@) now at: vruxwmqv 1a70c7c6 conflict | conflict
118 Parent commit (@-) : zsuskuln aa493daf a | a
119 Parent commit (@-) : royxmykx db6a4daf b | b
120 Added 0 files, modified 1 files, removed 0 files
121 [EOF]
122 ");
123 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
124 diff --git a/file b/file
125 index 0000000000..88425ec521 100644
126 --- a/file
127 +++ b/file
128 @@ -1,7 +1,1 @@
129 -<<<<<<< Conflict 1 of 1
130 -%%%%%%% Changes from base to side #1
131 --base
132 -+a
133 -+++++++ Contents of side #2
134 -b
135 ->>>>>>> Conflict 1 of 1 ends
136 +resolution
137 [EOF]
138 ");
139 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
140 ------- stderr -------
141 Error: No conflicts found at this revision
142 [EOF]
143 [exit status: 2]
144 ");
145
146 // Check that the output file starts with conflict markers if
147 // `merge-tool-edits-conflict-markers=true`
148 work_dir.run_jj(["undo"]).success();
149 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
150 std::fs::write(
151 &editor_script,
152 ["dump editor1", "write\nresolution\n"].join("\0"),
153 )
154 .unwrap();
155 work_dir
156 .run_jj([
157 "resolve",
158 "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
159 ])
160 .success();
161 insta::assert_snapshot!(
162 std::fs::read_to_string(test_env.env_root().join("editor1")).unwrap(), @r"
163 <<<<<<< Conflict 1 of 1
164 %%%%%%% Changes from base to side #1
165 -base
166 +a
167 +++++++ Contents of side #2
168 b
169 >>>>>>> Conflict 1 of 1 ends
170 ");
171 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
172 diff --git a/file b/file
173 index 0000000000..88425ec521 100644
174 --- a/file
175 +++ b/file
176 @@ -1,7 +1,1 @@
177 -<<<<<<< Conflict 1 of 1
178 -%%%%%%% Changes from base to side #1
179 --base
180 -+a
181 -+++++++ Contents of side #2
182 -b
183 ->>>>>>> Conflict 1 of 1 ends
184 +resolution
185 [EOF]
186 ");
187
188 // Check that if merge tool leaves conflict markers in output file and
189 // `merge-tool-edits-conflict-markers=true`, these markers are properly parsed.
190 work_dir.run_jj(["undo"]).success();
191 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
192 std::fs::write(
193 &editor_script,
194 [
195 "dump editor2",
196 indoc! {"
197 write
198 <<<<<<<
199 %%%%%%%
200 -some
201 +fake
202 +++++++
203 conflict
204 >>>>>>>
205 "},
206 ]
207 .join("\0"),
208 )
209 .unwrap();
210 let output = work_dir.run_jj([
211 "resolve",
212 "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
213 ]);
214 insta::assert_snapshot!(output, @r"
215 ------- stderr -------
216 Resolving conflicts in: file
217 Working copy (@) now at: vruxwmqv 608a2310 conflict | (conflict) conflict
218 Parent commit (@-) : zsuskuln aa493daf a | a
219 Parent commit (@-) : royxmykx db6a4daf b | b
220 Added 0 files, modified 1 files, removed 0 files
221 Warning: There are unresolved conflicts at these paths:
222 file 2-sided conflict
223 New conflicts appeared in 1 commits:
224 vruxwmqv 608a2310 conflict | (conflict) conflict
225 Hint: To resolve the conflicts, start by updating to it:
226 jj new vruxwmqv
227 Then use `jj resolve`, or edit the conflict markers in the file directly.
228 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
229 Then run `jj squash` to move the resolution into the conflicted commit.
230 [EOF]
231 ");
232 insta::assert_snapshot!(
233 std::fs::read_to_string(test_env.env_root().join("editor2")).unwrap(), @r"
234 <<<<<<< Conflict 1 of 1
235 %%%%%%% Changes from base to side #1
236 -base
237 +a
238 +++++++ Contents of side #2
239 b
240 >>>>>>> Conflict 1 of 1 ends
241 ");
242 // Note the "Modified" below
243 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
244 diff --git a/file b/file
245 --- a/file
246 +++ b/file
247 @@ -1,7 +1,7 @@
248 <<<<<<< Conflict 1 of 1
249 %%%%%%% Changes from base to side #1
250 --base
251 -+a
252 +-some
253 ++fake
254 +++++++ Contents of side #2
255 -b
256 +conflict
257 >>>>>>> Conflict 1 of 1 ends
258 [EOF]
259 ");
260 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
261 file 2-sided conflict
262 [EOF]
263 ");
264
265 // Check that if merge tool leaves conflict markers in output file but
266 // `merge-tool-edits-conflict-markers=false` or is not specified,
267 // `jj` considers the conflict resolved.
268 work_dir.run_jj(["undo"]).success();
269 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
270 std::fs::write(
271 &editor_script,
272 [
273 "dump editor3",
274 indoc! {"
275 write
276 <<<<<<<
277 %%%%%%%
278 -some
279 +fake
280 +++++++
281 conflict
282 >>>>>>>
283 "},
284 ]
285 .join("\0"),
286 )
287 .unwrap();
288 let output = work_dir.run_jj(["resolve"]);
289 insta::assert_snapshot!(output, @r"
290 ------- stderr -------
291 Resolving conflicts in: file
292 Working copy (@) now at: vruxwmqv 3166dfd2 conflict | conflict
293 Parent commit (@-) : zsuskuln aa493daf a | a
294 Parent commit (@-) : royxmykx db6a4daf b | b
295 Added 0 files, modified 1 files, removed 0 files
296 [EOF]
297 ");
298 insta::assert_snapshot!(
299 std::fs::read_to_string(test_env.env_root().join("editor3")).unwrap(), @"");
300 // Note the "Resolved" below
301 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
302 diff --git a/file b/file
303 index 0000000000..0610716cc1 100644
304 --- a/file
305 +++ b/file
306 @@ -1,7 +1,7 @@
307 -<<<<<<< Conflict 1 of 1
308 -%%%%%%% Changes from base to side #1
309 --base
310 -+a
311 -+++++++ Contents of side #2
312 -b
313 ->>>>>>> Conflict 1 of 1 ends
314 +<<<<<<<
315 +%%%%%%%
316 +-some
317 ++fake
318 ++++++++
319 +conflict
320 +>>>>>>>
321 [EOF]
322 ");
323 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
324 ------- stderr -------
325 Error: No conflicts found at this revision
326 [EOF]
327 [exit status: 2]
328 ");
329
330 // Check that merge tool can override conflict marker style setting, and that
331 // the merge tool can output Git-style conflict markers
332 work_dir.run_jj(["undo"]).success();
333 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
334 std::fs::write(
335 &editor_script,
336 [
337 "dump editor4",
338 indoc! {"
339 write
340 <<<<<<<
341 some
342 |||||||
343 fake
344 =======
345 conflict
346 >>>>>>>
347 "},
348 ]
349 .join("\0"),
350 )
351 .unwrap();
352 let output = work_dir.run_jj([
353 "resolve",
354 "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
355 "--config=merge-tools.fake-editor.conflict-marker-style=git",
356 ]);
357 insta::assert_snapshot!(output, @r"
358 ------- stderr -------
359 Resolving conflicts in: file
360 Working copy (@) now at: vruxwmqv 8e03fefa conflict | (conflict) conflict
361 Parent commit (@-) : zsuskuln aa493daf a | a
362 Parent commit (@-) : royxmykx db6a4daf b | b
363 Added 0 files, modified 1 files, removed 0 files
364 Warning: There are unresolved conflicts at these paths:
365 file 2-sided conflict
366 New conflicts appeared in 1 commits:
367 vruxwmqv 8e03fefa conflict | (conflict) conflict
368 Hint: To resolve the conflicts, start by updating to it:
369 jj new vruxwmqv
370 Then use `jj resolve`, or edit the conflict markers in the file directly.
371 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
372 Then run `jj squash` to move the resolution into the conflicted commit.
373 [EOF]
374 ");
375 insta::assert_snapshot!(
376 std::fs::read_to_string(test_env.env_root().join("editor4")).unwrap(), @r"
377 <<<<<<< Side #1 (Conflict 1 of 1)
378 a
379 ||||||| Base
380 base
381 =======
382 b
383 >>>>>>> Side #2 (Conflict 1 of 1 ends)
384 ");
385 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
386 diff --git a/file b/file
387 --- a/file
388 +++ b/file
389 @@ -1,7 +1,7 @@
390 <<<<<<< Conflict 1 of 1
391 %%%%%%% Changes from base to side #1
392 --base
393 -+a
394 +-fake
395 ++some
396 +++++++ Contents of side #2
397 -b
398 +conflict
399 >>>>>>> Conflict 1 of 1 ends
400 [EOF]
401 ");
402 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
403 file 2-sided conflict
404 [EOF]
405 ");
406
407 // Check that merge tool can leave conflict markers by returning exit code 1
408 // when using `merge-conflict-exit-codes = [1]`. The Git "diff3" conflict
409 // markers should also be parsed correctly.
410 work_dir.run_jj(["undo"]).success();
411 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
412 std::fs::write(
413 &editor_script,
414 [
415 "dump editor5",
416 indoc! {"
417 write
418 <<<<<<<
419 some
420 |||||||
421 fake
422 =======
423 conflict
424 >>>>>>>
425 "},
426 "fail",
427 ]
428 .join("\0"),
429 )
430 .unwrap();
431 let output = work_dir.run_jj([
432 "resolve",
433 "--config=merge-tools.fake-editor.merge-conflict-exit-codes=[1]",
434 ]);
435 insta::assert_snapshot!(output, @r"
436 ------- stderr -------
437 Resolving conflicts in: file
438 Working copy (@) now at: vruxwmqv a786ac2f conflict | (conflict) conflict
439 Parent commit (@-) : zsuskuln aa493daf a | a
440 Parent commit (@-) : royxmykx db6a4daf b | b
441 Added 0 files, modified 1 files, removed 0 files
442 Warning: There are unresolved conflicts at these paths:
443 file 2-sided conflict
444 New conflicts appeared in 1 commits:
445 vruxwmqv a786ac2f conflict | (conflict) conflict
446 Hint: To resolve the conflicts, start by updating to it:
447 jj new vruxwmqv
448 Then use `jj resolve`, or edit the conflict markers in the file directly.
449 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
450 Then run `jj squash` to move the resolution into the conflicted commit.
451 [EOF]
452 ");
453 insta::assert_snapshot!(
454 std::fs::read_to_string(test_env.env_root().join("editor5")).unwrap(), @"");
455 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
456 diff --git a/file b/file
457 --- a/file
458 +++ b/file
459 @@ -1,7 +1,7 @@
460 <<<<<<< Conflict 1 of 1
461 %%%%%%% Changes from base to side #1
462 --base
463 -+a
464 +-fake
465 ++some
466 +++++++ Contents of side #2
467 -b
468 +conflict
469 >>>>>>> Conflict 1 of 1 ends
470 [EOF]
471 ");
472 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
473 file 2-sided conflict
474 [EOF]
475 ");
476
477 // Check that an error is reported if a merge tool indicated it would leave
478 // conflict markers, but the output file didn't contain valid conflict markers.
479 work_dir.run_jj(["undo"]).success();
480 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
481 std::fs::write(
482 &editor_script,
483 [
484 indoc! {"
485 write
486 <<<<<<< this isn't diff3 style!
487 some
488 =======
489 conflict
490 >>>>>>>
491 "},
492 "fail",
493 ]
494 .join("\0"),
495 )
496 .unwrap();
497 let output = work_dir.run_jj([
498 "resolve",
499 "--config=merge-tools.fake-editor.merge-conflict-exit-codes=[1]",
500 ]);
501 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r"
502 ------- stderr -------
503 Resolving conflicts in: file
504 Error: Failed to resolve conflicts
505 Caused by: Tool exited with exit status: 1, but did not produce valid conflict markers (run with --debug to see the exact invocation)
506 [EOF]
507 [exit status: 1]
508 ");
509
510 // TODO: Check that running `jj new` and then `jj resolve -r conflict` works
511 // correctly.
512}
513
514fn check_resolve_produces_input_file(
515 test_env: &mut TestEnvironment,
516 root: impl AsRef<Path>,
517 filename: &str,
518 role: &str,
519 expected_content: &str,
520) {
521 let editor_script = test_env.set_up_fake_editor();
522 let work_dir = test_env.work_dir(root);
523 std::fs::write(editor_script, format!("expect\n{expected_content}")).unwrap();
524
525 let merge_arg_config = format!(r#"merge-tools.fake-editor.merge-args=["${role}"]"#);
526 // This error means that fake-editor exited successfully but did not modify the
527 // output file.
528 let output = work_dir.run_jj(["resolve", "--config", &merge_arg_config, filename]);
529 insta::allow_duplicates! {
530 insta::assert_snapshot!(
531 output.normalize_stderr_with(|s| s.replacen(filename, "$FILENAME", 1)), @r"
532 ------- stderr -------
533 Resolving conflicts in: $FILENAME
534 Error: Failed to resolve conflicts
535 Caused by: The output file is either unchanged or empty after the editor quit (run with --debug to see the exact invocation).
536 [EOF]
537 [exit status: 1]
538 ");
539 }
540}
541
542#[test]
543fn test_normal_conflict_input_files() {
544 let mut test_env = TestEnvironment::default();
545 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
546 let work_dir = test_env.work_dir("repo");
547
548 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
549 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
550 create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
551 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
552 // Test the setup
553 insta::assert_snapshot!(get_log_output(&work_dir), @r"
554 @ conflict
555 ├─╮
556 │ ○ b
557 ○ │ a
558 ├─╯
559 ○ base
560 ◆
561 [EOF]
562 ");
563 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
564 file 2-sided conflict
565 [EOF]
566 ");
567 insta::assert_snapshot!(work_dir.read_file("file"), @r"
568 <<<<<<< Conflict 1 of 1
569 %%%%%%% Changes from base to side #1
570 -base
571 +a
572 +++++++ Contents of side #2
573 b
574 >>>>>>> Conflict 1 of 1 ends
575 ");
576
577 check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "base\n");
578 check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
579 check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "b\n");
580}
581
582#[test]
583fn test_baseless_conflict_input_files() {
584 let mut test_env = TestEnvironment::default();
585 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
586 let work_dir = test_env.work_dir("repo");
587
588 create_commit_with_files(&work_dir, "base", &[], &[]);
589 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
590 create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
591 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
592 // Test the setup
593 insta::assert_snapshot!(get_log_output(&work_dir), @r"
594 @ conflict
595 ├─╮
596 │ ○ b
597 ○ │ a
598 ├─╯
599 ○ base
600 ◆
601 [EOF]
602 ");
603 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
604 file 2-sided conflict
605 [EOF]
606 ");
607 insta::assert_snapshot!(work_dir.read_file("file"), @r"
608 <<<<<<< Conflict 1 of 1
609 %%%%%%% Changes from base to side #1
610 +a
611 +++++++ Contents of side #2
612 b
613 >>>>>>> Conflict 1 of 1 ends
614 ");
615
616 check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "");
617 check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
618 check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "b\n");
619}
620
621#[test]
622fn test_too_many_parents() {
623 let test_env = TestEnvironment::default();
624 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
625 let work_dir = test_env.work_dir("repo");
626
627 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
628 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
629 create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
630 create_commit_with_files(&work_dir, "c", &["base"], &[("file", "c\n")]);
631 create_commit_with_files(&work_dir, "conflict", &["a", "b", "c"], &[]);
632 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
633 file 3-sided conflict
634 [EOF]
635 ");
636 // Test warning color
637 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @r"
638 file [38;5;1m3-sided[38;5;3m conflict[39m
639 [EOF]
640 ");
641
642 let output = work_dir.run_jj(["resolve"]);
643 insta::assert_snapshot!(output, @r#"
644 ------- stderr -------
645 Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
646 Error: Failed to resolve conflicts
647 Caused by: The conflict at "file" has 3 sides. At most 2 sides are supported.
648 [EOF]
649 [exit status: 1]
650 "#);
651}
652
653#[test]
654fn test_simplify_conflict_sides() {
655 let mut test_env = TestEnvironment::default();
656 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
657 let work_dir = test_env.work_dir("repo");
658
659 // Creates a 4-sided conflict, with fileA and fileB having different conflicts:
660 // fileA: A - B + C - B + B - B + B
661 // fileB: A - A + A - A + B - C + D
662 create_commit_with_files(
663 &work_dir,
664 "base",
665 &[],
666 &[("fileA", "base\n"), ("fileB", "base\n")],
667 );
668 create_commit_with_files(&work_dir, "a1", &["base"], &[("fileA", "1\n")]);
669 create_commit_with_files(&work_dir, "a2", &["base"], &[("fileA", "2\n")]);
670 create_commit_with_files(&work_dir, "b1", &["base"], &[("fileB", "1\n")]);
671 create_commit_with_files(&work_dir, "b2", &["base"], &[("fileB", "2\n")]);
672 create_commit_with_files(&work_dir, "conflictA", &["a1", "a2"], &[]);
673 create_commit_with_files(&work_dir, "conflictB", &["b1", "b2"], &[]);
674 create_commit_with_files(&work_dir, "conflict", &["conflictA", "conflictB"], &[]);
675
676 // Even though the tree-level conflict is a 4-sided conflict, each file is
677 // materialized as a 2-sided conflict.
678 insta::assert_snapshot!(work_dir.run_jj(["debug", "tree"]), @r#"
679 fileA: Ok(Conflicted([Some(File { id: FileId("d00491fd7e5bb6fa28c517a0bb32b8b506539d4d"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("0cfbf08886fca9a91cb753ec8734c84fcbe52c9f"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false })]))
680 fileB: Ok(Conflicted([Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("d00491fd7e5bb6fa28c517a0bb32b8b506539d4d"), executable: false }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false }), Some(File { id: FileId("0cfbf08886fca9a91cb753ec8734c84fcbe52c9f"), executable: false })]))
681 [EOF]
682 "#);
683 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
684 fileA 2-sided conflict
685 fileB 2-sided conflict
686 [EOF]
687 ");
688 insta::assert_snapshot!(work_dir.read_file("fileA"), @r"
689 <<<<<<< Conflict 1 of 1
690 %%%%%%% Changes from base to side #1
691 -base
692 +1
693 +++++++ Contents of side #2
694 2
695 >>>>>>> Conflict 1 of 1 ends
696 ");
697 insta::assert_snapshot!(work_dir.read_file("fileB"), @r"
698 <<<<<<< Conflict 1 of 1
699 %%%%%%% Changes from base to side #1
700 -base
701 +1
702 +++++++ Contents of side #2
703 2
704 >>>>>>> Conflict 1 of 1 ends
705 ");
706
707 // Conflict should be simplified before being handled by external merge tool.
708 check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "base", "base\n");
709 check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "left", "1\n");
710 check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "right", "2\n");
711 check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "base", "base\n");
712 check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "left", "1\n");
713 check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "right", "2\n");
714
715 // Check that simplified conflicts are still parsed as conflicts after editing
716 // when `merge-tool-edits-conflict-markers=true`.
717 let editor_script = test_env.set_up_fake_editor();
718 std::fs::write(
719 editor_script,
720 indoc! {"
721 write
722 <<<<<<< Conflict 1 of 1
723 %%%%%%% Changes from base to side #1
724 -base_edited
725 +1_edited
726 +++++++ Contents of side #2
727 2_edited
728 >>>>>>> Conflict 1 of 1 ends
729 "},
730 )
731 .unwrap();
732 let work_dir = test_env.work_dir("repo");
733 let output = work_dir.run_jj([
734 "resolve",
735 "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
736 "fileB",
737 ]);
738 insta::assert_snapshot!(output, @r"
739 ------- stderr -------
740 Resolving conflicts in: fileB
741 Working copy (@) now at: nkmrtpmo 69cc0c2d conflict | (conflict) conflict
742 Parent commit (@-) : kmkuslsw 4601566f conflictA | (conflict) (empty) conflictA
743 Parent commit (@-) : lylxulpl 6f8d8381 conflictB | (conflict) (empty) conflictB
744 Added 0 files, modified 1 files, removed 0 files
745 Warning: There are unresolved conflicts at these paths:
746 fileA 2-sided conflict
747 fileB 2-sided conflict
748 New conflicts appeared in 1 commits:
749 nkmrtpmo 69cc0c2d conflict | (conflict) conflict
750 Hint: To resolve the conflicts, start by updating to it:
751 jj new nkmrtpmo
752 Then use `jj resolve`, or edit the conflict markers in the file directly.
753 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
754 Then run `jj squash` to move the resolution into the conflicted commit.
755 [EOF]
756 ");
757 insta::assert_snapshot!(work_dir.read_file("fileB"), @r"
758 <<<<<<< Conflict 1 of 1
759 %%%%%%% Changes from base to side #1
760 -base_edited
761 +1_edited
762 +++++++ Contents of side #2
763 2_edited
764 >>>>>>> Conflict 1 of 1 ends
765 ");
766 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
767 fileA 2-sided conflict
768 fileB 2-sided conflict
769 [EOF]
770 ");
771}
772
773#[test]
774fn test_edit_delete_conflict_input_files() {
775 let mut test_env = TestEnvironment::default();
776 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
777 let work_dir = test_env.work_dir("repo");
778
779 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
780 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
781 create_commit_with_files(&work_dir, "b", &["base"], &[]);
782 work_dir.remove_file("file");
783 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
784 // Test the setup
785 insta::assert_snapshot!(get_log_output(&work_dir), @r"
786 @ conflict
787 ├─╮
788 │ ○ b
789 ○ │ a
790 ├─╯
791 ○ base
792 ◆
793 [EOF]
794 ");
795 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
796 file 2-sided conflict including 1 deletion
797 [EOF]
798 ");
799 insta::assert_snapshot!(work_dir.read_file("file"), @r"
800 <<<<<<< Conflict 1 of 1
801 +++++++ Contents of side #1
802 a
803 %%%%%%% Changes from base to side #2
804 -base
805 >>>>>>> Conflict 1 of 1 ends
806 ");
807
808 check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "base\n");
809 check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
810 check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "");
811}
812
813#[test]
814fn test_file_vs_dir() {
815 let test_env = TestEnvironment::default();
816 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
817 let work_dir = test_env.work_dir("repo");
818
819 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
820 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
821 create_commit_with_files(&work_dir, "b", &["base"], &[]);
822 work_dir.remove_file("file");
823 work_dir.create_dir("file");
824 // Without a placeholder file, `jj` ignores an empty directory
825 work_dir.write_file("file/placeholder", "");
826 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
827 insta::assert_snapshot!(get_log_output(&work_dir), @r"
828 @ conflict
829 ├─╮
830 │ ○ b
831 ○ │ a
832 ├─╯
833 ○ base
834 ◆
835 [EOF]
836 ");
837
838 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
839 file 2-sided conflict including a directory
840 [EOF]
841 ");
842 let output = work_dir.run_jj(["resolve"]);
843 insta::assert_snapshot!(output, @r#"
844 ------- stderr -------
845 Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
846 Error: Failed to resolve conflicts
847 Caused by: Only conflicts that involve normal files (not symlinks, not executable, etc.) are supported. Conflict summary for "file":
848 Conflict:
849 Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
850 Adding file with id 78981922613b2afb6025042ff6bd878ac1994e85
851 Adding tree with id 133bb38fc4e4bf6b551f1f04db7e48f04cac2877
852
853 [EOF]
854 [exit status: 1]
855 "#);
856}
857
858#[test]
859fn test_description_with_dir_and_deletion() {
860 let test_env = TestEnvironment::default();
861 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
862 let work_dir = test_env.work_dir("repo");
863
864 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
865 create_commit_with_files(&work_dir, "edit", &["base"], &[("file", "b\n")]);
866 create_commit_with_files(&work_dir, "dir", &["base"], &[]);
867 work_dir.remove_file("file");
868 work_dir.create_dir("file");
869 // Without a placeholder file, `jj` ignores an empty directory
870 work_dir.write_file("file/placeholder", "");
871 create_commit_with_files(&work_dir, "del", &["base"], &[]);
872 work_dir.remove_file("file");
873 create_commit_with_files(&work_dir, "conflict", &["edit", "dir", "del"], &[]);
874 insta::assert_snapshot!(get_log_output(&work_dir), @r"
875 @ conflict
876 ├─┬─╮
877 │ │ ○ del
878 │ ○ │ dir
879 │ ├─╯
880 ○ │ edit
881 ├─╯
882 ○ base
883 ◆
884 [EOF]
885 ");
886
887 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
888 file 3-sided conflict including 1 deletion and a directory
889 [EOF]
890 ");
891 // Test warning color. The deletion is fine, so it's not highlighted
892 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @r"
893 file [38;5;1m3-sided[38;5;3m conflict including 1 deletion and [38;5;1ma directory[39m
894 [EOF]
895 ");
896 let output = work_dir.run_jj(["resolve"]);
897 insta::assert_snapshot!(output, @r#"
898 ------- stderr -------
899 Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
900 Error: Failed to resolve conflicts
901 Caused by: Only conflicts that involve normal files (not symlinks, not executable, etc.) are supported. Conflict summary for "file":
902 Conflict:
903 Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
904 Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
905 Adding file with id 61780798228d17af2d34fce4cfbdf35556832472
906 Adding tree with id 133bb38fc4e4bf6b551f1f04db7e48f04cac2877
907
908 [EOF]
909 [exit status: 1]
910 "#);
911}
912
913#[test]
914fn test_resolve_conflicts_with_executable() {
915 let mut test_env = TestEnvironment::default();
916 let editor_script = test_env.set_up_fake_editor();
917 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
918 let work_dir = test_env.work_dir("repo");
919
920 // Create a conflict in "file1" where all 3 terms are executables, and create a
921 // conflict in "file2" where one side set the executable bit.
922 create_commit_with_files(
923 &work_dir,
924 "base",
925 &[],
926 &[("file1", "base1\n"), ("file2", "base2\n")],
927 );
928 work_dir.run_jj(["file", "chmod", "x", "file1"]).success();
929 create_commit_with_files(
930 &work_dir,
931 "a",
932 &["base"],
933 &[("file1", "a1\n"), ("file2", "a2\n")],
934 );
935 create_commit_with_files(
936 &work_dir,
937 "b",
938 &["base"],
939 &[("file1", "b1\n"), ("file2", "b2\n")],
940 );
941 work_dir.run_jj(["file", "chmod", "x", "file2"]).success();
942 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
943 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
944 file1 2-sided conflict including an executable
945 file2 2-sided conflict including an executable
946 [EOF]
947 ");
948 insta::assert_snapshot!(work_dir.read_file("file1"), @r"
949 <<<<<<< Conflict 1 of 1
950 %%%%%%% Changes from base to side #1
951 -base1
952 +a1
953 +++++++ Contents of side #2
954 b1
955 >>>>>>> Conflict 1 of 1 ends
956 "
957 );
958 insta::assert_snapshot!(work_dir.read_file("file2"), @r"
959 <<<<<<< Conflict 1 of 1
960 %%%%%%% Changes from base to side #1
961 -base2
962 +a2
963 +++++++ Contents of side #2
964 b2
965 >>>>>>> Conflict 1 of 1 ends
966 "
967 );
968
969 // Test resolving the conflict in "file1", which should produce an executable
970 std::fs::write(&editor_script, b"write\nresolution1\n").unwrap();
971 let output = work_dir.run_jj(["resolve", "file1"]);
972 insta::assert_snapshot!(output, @r"
973 ------- stderr -------
974 Resolving conflicts in: file1
975 Working copy (@) now at: znkkpsqq eb159d56 conflict | (conflict) conflict
976 Parent commit (@-) : mzvwutvl 08932848 a | a
977 Parent commit (@-) : yqosqzyt b69b3de6 b | b
978 Added 0 files, modified 1 files, removed 0 files
979 Warning: There are unresolved conflicts at these paths:
980 file2 2-sided conflict including an executable
981 New conflicts appeared in 1 commits:
982 znkkpsqq eb159d56 conflict | (conflict) conflict
983 Hint: To resolve the conflicts, start by updating to it:
984 jj new znkkpsqq
985 Then use `jj resolve`, or edit the conflict markers in the file directly.
986 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
987 Then run `jj squash` to move the resolution into the conflicted commit.
988 [EOF]
989 ");
990 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
991 diff --git a/file1 b/file1
992 index 0000000000..95cc18629d 100755
993 --- a/file1
994 +++ b/file1
995 @@ -1,7 +1,1 @@
996 -<<<<<<< Conflict 1 of 1
997 -%%%%%%% Changes from base to side #1
998 --base1
999 -+a1
1000 -+++++++ Contents of side #2
1001 -b1
1002 ->>>>>>> Conflict 1 of 1 ends
1003 +resolution1
1004 [EOF]
1005 ");
1006 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1007 file2 2-sided conflict including an executable
1008 [EOF]
1009 ");
1010
1011 // Test resolving the conflict in "file2", which should produce an executable
1012 work_dir.run_jj(["undo"]).success();
1013 std::fs::write(&editor_script, b"write\nresolution2\n").unwrap();
1014 let output = work_dir.run_jj(["resolve", "file2"]);
1015 insta::assert_snapshot!(output, @r"
1016 ------- stderr -------
1017 Resolving conflicts in: file2
1018 Working copy (@) now at: znkkpsqq 4dccbb3c conflict | (conflict) conflict
1019 Parent commit (@-) : mzvwutvl 08932848 a | a
1020 Parent commit (@-) : yqosqzyt b69b3de6 b | b
1021 Added 0 files, modified 1 files, removed 0 files
1022 Warning: There are unresolved conflicts at these paths:
1023 file1 2-sided conflict including an executable
1024 New conflicts appeared in 1 commits:
1025 znkkpsqq 4dccbb3c conflict | (conflict) conflict
1026 Hint: To resolve the conflicts, start by updating to it:
1027 jj new znkkpsqq
1028 Then use `jj resolve`, or edit the conflict markers in the file directly.
1029 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
1030 Then run `jj squash` to move the resolution into the conflicted commit.
1031 [EOF]
1032 ");
1033 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1034 diff --git a/file2 b/file2
1035 index 0000000000..775f078581 100755
1036 --- a/file2
1037 +++ b/file2
1038 @@ -1,7 +1,1 @@
1039 -<<<<<<< Conflict 1 of 1
1040 -%%%%%%% Changes from base to side #1
1041 --base2
1042 -+a2
1043 -+++++++ Contents of side #2
1044 -b2
1045 ->>>>>>> Conflict 1 of 1 ends
1046 +resolution2
1047 [EOF]
1048 ");
1049 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1050 file1 2-sided conflict including an executable
1051 [EOF]
1052 ");
1053}
1054
1055#[test]
1056fn test_resolve_long_conflict_markers() {
1057 let mut test_env = TestEnvironment::default();
1058 let editor_script = test_env.set_up_fake_editor();
1059 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1060 let work_dir = test_env.work_dir("repo");
1061
1062 // Makes it easier to read the diffs between conflicts
1063 test_env.add_config("ui.conflict-marker-style = 'snapshot'");
1064
1065 // Create a conflict which requires long conflict markers to be materialized
1066 create_commit_with_files(&work_dir, "base", &[], &[("file", "======= base\n")]);
1067 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "<<<<<<< a\n")]);
1068 create_commit_with_files(&work_dir, "b", &["base"], &[("file", ">>>>>>> b\n")]);
1069 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
1070 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1071 file 2-sided conflict
1072 [EOF]
1073 ");
1074 insta::assert_snapshot!(work_dir.read_file("file"), @r"
1075 <<<<<<<<<<< Conflict 1 of 1
1076 +++++++++++ Contents of side #1
1077 <<<<<<< a
1078 ----------- Contents of base
1079 ======= base
1080 +++++++++++ Contents of side #2
1081 >>>>>>> b
1082 >>>>>>>>>>> Conflict 1 of 1 ends
1083 "
1084 );
1085 // Allow signaling that conflict markers were produced even if not editing
1086 // conflict markers materialized in the output file
1087 test_env.add_config("merge-tools.fake-editor.merge-conflict-exit-codes = [1]");
1088
1089 // By default, conflict markers of length 7 or longer are parsed for
1090 // compatibility with Git merge tools
1091 std::fs::write(
1092 &editor_script,
1093 indoc! {b"
1094 write
1095 <<<<<<<
1096 A
1097 |||||||
1098 BASE
1099 =======
1100 B
1101 >>>>>>>
1102 \0fail
1103 "},
1104 )
1105 .unwrap();
1106 let output = work_dir.run_jj(["resolve"]);
1107 insta::assert_snapshot!(output, @r"
1108 ------- stderr -------
1109 Resolving conflicts in: file
1110 Working copy (@) now at: vruxwmqv 2b985546 conflict | (conflict) conflict
1111 Parent commit (@-) : zsuskuln 64177fd4 a | a
1112 Parent commit (@-) : royxmykx db442c1e b | b
1113 Added 0 files, modified 1 files, removed 0 files
1114 Warning: There are unresolved conflicts at these paths:
1115 file 2-sided conflict
1116 New conflicts appeared in 1 commits:
1117 vruxwmqv 2b985546 conflict | (conflict) conflict
1118 Hint: To resolve the conflicts, start by updating to it:
1119 jj new vruxwmqv
1120 Then use `jj resolve`, or edit the conflict markers in the file directly.
1121 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
1122 Then run `jj squash` to move the resolution into the conflicted commit.
1123 [EOF]
1124 ");
1125 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1126 diff --git a/file b/file
1127 --- a/file
1128 +++ b/file
1129 @@ -1,8 +1,8 @@
1130 -<<<<<<<<<<< Conflict 1 of 1
1131 -+++++++++++ Contents of side #1
1132 -<<<<<<< a
1133 ------------ Contents of base
1134 -======= base
1135 -+++++++++++ Contents of side #2
1136 ->>>>>>> b
1137 ->>>>>>>>>>> Conflict 1 of 1 ends
1138 +<<<<<<< Conflict 1 of 1
1139 ++++++++ Contents of side #1
1140 +A
1141 +------- Contents of base
1142 +BASE
1143 ++++++++ Contents of side #2
1144 +B
1145 +>>>>>>> Conflict 1 of 1 ends
1146 [EOF]
1147 ");
1148 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1149 file 2-sided conflict
1150 [EOF]
1151 ");
1152
1153 // If the merge tool edits the output file with materialized markers, the
1154 // markers must match the length of the materialized markers to be parsed
1155 work_dir.run_jj(["undo"]).success();
1156 std::fs::write(
1157 &editor_script,
1158 indoc! {b"
1159 dump editor
1160 \0write
1161 <<<<<<<<<<<
1162 <<<<<<< A
1163 |||||||||||
1164 ======= BASE
1165 ===========
1166 >>>>>>> B
1167 >>>>>>>>>>>
1168 \0fail
1169 "},
1170 )
1171 .unwrap();
1172 let output = work_dir.run_jj([
1173 "resolve",
1174 "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
1175 ]);
1176 insta::assert_snapshot!(output, @r"
1177 ------- stderr -------
1178 Resolving conflicts in: file
1179 Working copy (@) now at: vruxwmqv fac9406d conflict | (conflict) conflict
1180 Parent commit (@-) : zsuskuln 64177fd4 a | a
1181 Parent commit (@-) : royxmykx db442c1e b | b
1182 Added 0 files, modified 1 files, removed 0 files
1183 Warning: There are unresolved conflicts at these paths:
1184 file 2-sided conflict
1185 New conflicts appeared in 1 commits:
1186 vruxwmqv fac9406d conflict | (conflict) conflict
1187 Hint: To resolve the conflicts, start by updating to it:
1188 jj new vruxwmqv
1189 Then use `jj resolve`, or edit the conflict markers in the file directly.
1190 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
1191 Then run `jj squash` to move the resolution into the conflicted commit.
1192 [EOF]
1193 ");
1194 insta::assert_snapshot!(
1195 std::fs::read_to_string(test_env.env_root().join("editor")).unwrap(), @r"
1196 <<<<<<<<<<< Conflict 1 of 1
1197 +++++++++++ Contents of side #1
1198 <<<<<<< a
1199 ----------- Contents of base
1200 ======= base
1201 +++++++++++ Contents of side #2
1202 >>>>>>> b
1203 >>>>>>>>>>> Conflict 1 of 1 ends
1204 ");
1205 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1206 diff --git a/file b/file
1207 --- a/file
1208 +++ b/file
1209 @@ -1,8 +1,8 @@
1210 <<<<<<<<<<< Conflict 1 of 1
1211 +++++++++++ Contents of side #1
1212 -<<<<<<< a
1213 +<<<<<<< A
1214 ----------- Contents of base
1215 -======= base
1216 +======= BASE
1217 +++++++++++ Contents of side #2
1218 ->>>>>>> b
1219 +>>>>>>> B
1220 >>>>>>>>>>> Conflict 1 of 1 ends
1221 [EOF]
1222 ");
1223 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1224 file 2-sided conflict
1225 [EOF]
1226 ");
1227
1228 // If the merge tool accepts the marker length as an argument, then the conflict
1229 // markers should be at least as long as "$marker_length"
1230 work_dir.run_jj(["undo"]).success();
1231 std::fs::write(
1232 &editor_script,
1233 indoc! {b"
1234 expect-arg 0
1235 11\0write
1236 <<<<<<<<<<<
1237 <<<<<<< A
1238 |||||||||||
1239 ======= BASE
1240 ===========
1241 >>>>>>> B
1242 >>>>>>>>>>>
1243 \0fail
1244 "},
1245 )
1246 .unwrap();
1247 let output = work_dir.run_jj([
1248 "resolve",
1249 r#"--config=merge-tools.fake-editor.merge-args=["$output", "$marker_length"]"#,
1250 ]);
1251 insta::assert_snapshot!(output, @r"
1252 ------- stderr -------
1253 Resolving conflicts in: file
1254 Working copy (@) now at: vruxwmqv 1b29631a conflict | (conflict) conflict
1255 Parent commit (@-) : zsuskuln 64177fd4 a | a
1256 Parent commit (@-) : royxmykx db442c1e b | b
1257 Added 0 files, modified 1 files, removed 0 files
1258 Warning: There are unresolved conflicts at these paths:
1259 file 2-sided conflict
1260 New conflicts appeared in 1 commits:
1261 vruxwmqv 1b29631a conflict | (conflict) conflict
1262 Hint: To resolve the conflicts, start by updating to it:
1263 jj new vruxwmqv
1264 Then use `jj resolve`, or edit the conflict markers in the file directly.
1265 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
1266 Then run `jj squash` to move the resolution into the conflicted commit.
1267 [EOF]
1268 ");
1269 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1270 diff --git a/file b/file
1271 --- a/file
1272 +++ b/file
1273 @@ -1,8 +1,8 @@
1274 <<<<<<<<<<< Conflict 1 of 1
1275 +++++++++++ Contents of side #1
1276 -<<<<<<< a
1277 +<<<<<<< A
1278 ----------- Contents of base
1279 -======= base
1280 +======= BASE
1281 +++++++++++ Contents of side #2
1282 ->>>>>>> b
1283 +>>>>>>> B
1284 >>>>>>>>>>> Conflict 1 of 1 ends
1285 [EOF]
1286 ");
1287 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1288 file 2-sided conflict
1289 [EOF]
1290 ");
1291}
1292
1293#[test]
1294fn test_multiple_conflicts() {
1295 let mut test_env = TestEnvironment::default();
1296 let editor_script = test_env.set_up_fake_editor();
1297 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1298 let work_dir = test_env.work_dir("repo");
1299
1300 create_commit_with_files(
1301 &work_dir,
1302 "base",
1303 &[],
1304 &[
1305 (
1306 "this_file_has_a_very_long_name_to_test_padding",
1307 "first base\n",
1308 ),
1309 ("another_file", "second base\n"),
1310 ],
1311 );
1312 create_commit_with_files(
1313 &work_dir,
1314 "a",
1315 &["base"],
1316 &[
1317 (
1318 "this_file_has_a_very_long_name_to_test_padding",
1319 "first a\n",
1320 ),
1321 ("another_file", "second a\n"),
1322 ],
1323 );
1324 create_commit_with_files(
1325 &work_dir,
1326 "b",
1327 &["base"],
1328 &[
1329 (
1330 "this_file_has_a_very_long_name_to_test_padding",
1331 "first b\n",
1332 ),
1333 ("another_file", "second b\n"),
1334 ],
1335 );
1336 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
1337 // Test the setup
1338 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1339 @ conflict
1340 ├─╮
1341 │ ○ b
1342 ○ │ a
1343 ├─╯
1344 ○ base
1345 ◆
1346 [EOF]
1347 ");
1348 insta::assert_snapshot!(
1349 work_dir.read_file("this_file_has_a_very_long_name_to_test_padding"), @r"
1350 <<<<<<< Conflict 1 of 1
1351 %%%%%%% Changes from base to side #1
1352 -first base
1353 +first a
1354 +++++++ Contents of side #2
1355 first b
1356 >>>>>>> Conflict 1 of 1 ends
1357 ");
1358 insta::assert_snapshot!(work_dir.read_file("another_file"), @r"
1359 <<<<<<< Conflict 1 of 1
1360 %%%%%%% Changes from base to side #1
1361 -second base
1362 +second a
1363 +++++++ Contents of side #2
1364 second b
1365 >>>>>>> Conflict 1 of 1 ends
1366 ");
1367 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1368 another_file 2-sided conflict
1369 this_file_has_a_very_long_name_to_test_padding 2-sided conflict
1370 [EOF]
1371 ");
1372 // Test colors
1373 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @r"
1374 another_file [38;5;3m2-sided conflict[39m
1375 this_file_has_a_very_long_name_to_test_padding [38;5;3m2-sided conflict[39m
1376 [EOF]
1377 ");
1378
1379 // Check that we can manually pick which of the conflicts to resolve first
1380 std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n").unwrap();
1381 let output = work_dir.run_jj(["resolve", "another_file"]);
1382 insta::assert_snapshot!(output, @r"
1383 ------- stderr -------
1384 Resolving conflicts in: another_file
1385 Working copy (@) now at: vruxwmqv 309e981c conflict | (conflict) conflict
1386 Parent commit (@-) : zsuskuln de7553ef a | a
1387 Parent commit (@-) : royxmykx f68bc2f0 b | b
1388 Added 0 files, modified 1 files, removed 0 files
1389 Warning: There are unresolved conflicts at these paths:
1390 this_file_has_a_very_long_name_to_test_padding 2-sided conflict
1391 New conflicts appeared in 1 commits:
1392 vruxwmqv 309e981c conflict | (conflict) conflict
1393 Hint: To resolve the conflicts, start by updating to it:
1394 jj new vruxwmqv
1395 Then use `jj resolve`, or edit the conflict markers in the file directly.
1396 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
1397 Then run `jj squash` to move the resolution into the conflicted commit.
1398 [EOF]
1399 ");
1400 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1401 diff --git a/another_file b/another_file
1402 index 0000000000..a9fcc7d486 100644
1403 --- a/another_file
1404 +++ b/another_file
1405 @@ -1,7 +1,1 @@
1406 -<<<<<<< Conflict 1 of 1
1407 -%%%%%%% Changes from base to side #1
1408 --second base
1409 -+second a
1410 -+++++++ Contents of side #2
1411 -second b
1412 ->>>>>>> Conflict 1 of 1 ends
1413 +resolution another_file
1414 [EOF]
1415 ");
1416 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1417 this_file_has_a_very_long_name_to_test_padding 2-sided conflict
1418 [EOF]
1419 ");
1420
1421 // Repeat the above with the `--quiet` option.
1422 work_dir.run_jj(["undo"]).success();
1423 std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n").unwrap();
1424 let output = work_dir.run_jj(["resolve", "--quiet", "another_file"]);
1425 insta::assert_snapshot!(output, @"");
1426
1427 // Without a path, `jj resolve` should call the merge tool multiple times
1428 work_dir.run_jj(["undo"]).success();
1429 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
1430 std::fs::write(
1431 &editor_script,
1432 [
1433 "expect\n",
1434 "write\nfirst resolution for auto-chosen file\n",
1435 "next invocation\n",
1436 "expect\n",
1437 "write\nsecond resolution for auto-chosen file\n",
1438 ]
1439 .join("\0"),
1440 )
1441 .unwrap();
1442 work_dir.run_jj(["resolve"]).success();
1443 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1444 diff --git a/another_file b/another_file
1445 index 0000000000..7903e1c1c7 100644
1446 --- a/another_file
1447 +++ b/another_file
1448 @@ -1,7 +1,1 @@
1449 -<<<<<<< Conflict 1 of 1
1450 -%%%%%%% Changes from base to side #1
1451 --second base
1452 -+second a
1453 -+++++++ Contents of side #2
1454 -second b
1455 ->>>>>>> Conflict 1 of 1 ends
1456 +first resolution for auto-chosen file
1457 diff --git a/this_file_has_a_very_long_name_to_test_padding b/this_file_has_a_very_long_name_to_test_padding
1458 index 0000000000..f8c72adf17 100644
1459 --- a/this_file_has_a_very_long_name_to_test_padding
1460 +++ b/this_file_has_a_very_long_name_to_test_padding
1461 @@ -1,7 +1,1 @@
1462 -<<<<<<< Conflict 1 of 1
1463 -%%%%%%% Changes from base to side #1
1464 --first base
1465 -+first a
1466 -+++++++ Contents of side #2
1467 -first b
1468 ->>>>>>> Conflict 1 of 1 ends
1469 +second resolution for auto-chosen file
1470 [EOF]
1471 ");
1472
1473 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1474 ------- stderr -------
1475 Error: No conflicts found at this revision
1476 [EOF]
1477 [exit status: 2]
1478 ");
1479 insta::assert_snapshot!(work_dir.run_jj(["resolve"]), @r"
1480 ------- stderr -------
1481 Error: No conflicts found at this revision
1482 [EOF]
1483 [exit status: 2]
1484 ");
1485}
1486
1487#[test]
1488fn test_multiple_conflicts_with_error() {
1489 let mut test_env = TestEnvironment::default();
1490 let editor_script = test_env.set_up_fake_editor();
1491 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1492 let work_dir = test_env.work_dir("repo");
1493
1494 // Create two conflicted files, and one non-conflicted file
1495 create_commit_with_files(
1496 &work_dir,
1497 "base",
1498 &[],
1499 &[
1500 ("file1", "base1\n"),
1501 ("file2", "base2\n"),
1502 ("file3", "base3\n"),
1503 ],
1504 );
1505 create_commit_with_files(
1506 &work_dir,
1507 "a",
1508 &["base"],
1509 &[("file1", "a1\n"), ("file2", "a2\n")],
1510 );
1511 create_commit_with_files(
1512 &work_dir,
1513 "b",
1514 &["base"],
1515 &[("file1", "b1\n"), ("file2", "b2\n")],
1516 );
1517 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
1518 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1519 file1 2-sided conflict
1520 file2 2-sided conflict
1521 [EOF]
1522 ");
1523 insta::assert_snapshot!(work_dir.read_file("file1"), @r"
1524 <<<<<<< Conflict 1 of 1
1525 %%%%%%% Changes from base to side #1
1526 -base1
1527 +a1
1528 +++++++ Contents of side #2
1529 b1
1530 >>>>>>> Conflict 1 of 1 ends
1531 "
1532 );
1533 insta::assert_snapshot!(work_dir.read_file("file2"), @r"
1534 <<<<<<< Conflict 1 of 1
1535 %%%%%%% Changes from base to side #1
1536 -base2
1537 +a2
1538 +++++++ Contents of side #2
1539 b2
1540 >>>>>>> Conflict 1 of 1 ends
1541 "
1542 );
1543
1544 // Test resolving one conflict, then exiting without resolving the second one
1545 std::fs::write(
1546 &editor_script,
1547 ["write\nresolution1\n", "next invocation\n"].join("\0"),
1548 )
1549 .unwrap();
1550 let output = work_dir.run_jj(["resolve"]);
1551 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r"
1552 ------- stderr -------
1553 Resolving conflicts in: file1
1554 Resolving conflicts in: file2
1555 Working copy (@) now at: vruxwmqv d2f3f858 conflict | (conflict) conflict
1556 Parent commit (@-) : zsuskuln 9db7fdfb a | a
1557 Parent commit (@-) : royxmykx d67e26e4 b | b
1558 Added 0 files, modified 1 files, removed 0 files
1559 Warning: There are unresolved conflicts at these paths:
1560 file2 2-sided conflict
1561 New conflicts appeared in 1 commits:
1562 vruxwmqv d2f3f858 conflict | (conflict) conflict
1563 Hint: To resolve the conflicts, start by updating to it:
1564 jj new vruxwmqv
1565 Then use `jj resolve`, or edit the conflict markers in the file directly.
1566 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
1567 Then run `jj squash` to move the resolution into the conflicted commit.
1568 Error: Stopped due to error after resolving 1 conflicts
1569 Caused by: The output file is either unchanged or empty after the editor quit (run with --debug to see the exact invocation).
1570 [EOF]
1571 [exit status: 1]
1572 ");
1573 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1574 diff --git a/file1 b/file1
1575 index 0000000000..95cc18629d 100644
1576 --- a/file1
1577 +++ b/file1
1578 @@ -1,7 +1,1 @@
1579 -<<<<<<< Conflict 1 of 1
1580 -%%%%%%% Changes from base to side #1
1581 --base1
1582 -+a1
1583 -+++++++ Contents of side #2
1584 -b1
1585 ->>>>>>> Conflict 1 of 1 ends
1586 +resolution1
1587 [EOF]
1588 ");
1589 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1590 file2 2-sided conflict
1591 [EOF]
1592 ");
1593
1594 // Test resolving one conflict, then failing during the second resolution
1595 work_dir.run_jj(["undo"]).success();
1596 std::fs::write(
1597 &editor_script,
1598 ["write\nresolution1\n", "next invocation\n", "fail"].join("\0"),
1599 )
1600 .unwrap();
1601 let output = work_dir.run_jj(["resolve"]);
1602 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r"
1603 ------- stderr -------
1604 Resolving conflicts in: file1
1605 Resolving conflicts in: file2
1606 Working copy (@) now at: vruxwmqv 0a54e8ed conflict | (conflict) conflict
1607 Parent commit (@-) : zsuskuln 9db7fdfb a | a
1608 Parent commit (@-) : royxmykx d67e26e4 b | b
1609 Added 0 files, modified 1 files, removed 0 files
1610 Warning: There are unresolved conflicts at these paths:
1611 file2 2-sided conflict
1612 New conflicts appeared in 1 commits:
1613 vruxwmqv 0a54e8ed conflict | (conflict) conflict
1614 Hint: To resolve the conflicts, start by updating to it:
1615 jj new vruxwmqv
1616 Then use `jj resolve`, or edit the conflict markers in the file directly.
1617 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
1618 Then run `jj squash` to move the resolution into the conflicted commit.
1619 Error: Stopped due to error after resolving 1 conflicts
1620 Caused by: Tool exited with exit status: 1 (run with --debug to see the exact invocation)
1621 [EOF]
1622 [exit status: 1]
1623 ");
1624 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1625 diff --git a/file1 b/file1
1626 index 0000000000..95cc18629d 100644
1627 --- a/file1
1628 +++ b/file1
1629 @@ -1,7 +1,1 @@
1630 -<<<<<<< Conflict 1 of 1
1631 -%%%%%%% Changes from base to side #1
1632 --base1
1633 -+a1
1634 -+++++++ Contents of side #2
1635 -b1
1636 ->>>>>>> Conflict 1 of 1 ends
1637 +resolution1
1638 [EOF]
1639 ");
1640 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1641 file2 2-sided conflict
1642 [EOF]
1643 ");
1644
1645 // Test immediately failing to resolve any conflict
1646 work_dir.run_jj(["undo"]).success();
1647 std::fs::write(&editor_script, "fail").unwrap();
1648 let output = work_dir.run_jj(["resolve"]);
1649 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r"
1650 ------- stderr -------
1651 Resolving conflicts in: file1
1652 Error: Failed to resolve conflicts
1653 Caused by: Tool exited with exit status: 1 (run with --debug to see the exact invocation)
1654 [EOF]
1655 [exit status: 1]
1656 ");
1657 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
1658 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1659 file1 2-sided conflict
1660 file2 2-sided conflict
1661 [EOF]
1662 ");
1663}