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 741263c9 conflict | conflict
76 Parent commit (@-) : zsuskuln 45537d53 a | a
77 Parent commit (@-) : royxmykx 89d1b299 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 1f8a36f7 conflict | conflict
118 Parent commit (@-) : zsuskuln 45537d53 a | a
119 Parent commit (@-) : royxmykx 89d1b299 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 0d40d2b8 conflict | (conflict) conflict
218 Parent commit (@-) : zsuskuln 45537d53 a | a
219 Parent commit (@-) : royxmykx 89d1b299 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 0d40d2b8 conflict | (conflict) conflict
225 Hint: To resolve the conflicts, start by creating a commit on top of
226 the conflicted commit:
227 jj new vruxwmqv
228 Then use `jj resolve`, or edit the conflict markers in the file directly.
229 Once the conflicts are resolved, you can inspect the result with `jj diff`.
230 Then run `jj squash` to move the resolution into the conflicted commit.
231 [EOF]
232 "###);
233 insta::assert_snapshot!(
234 std::fs::read_to_string(test_env.env_root().join("editor2")).unwrap(), @r"
235 <<<<<<< Conflict 1 of 1
236 %%%%%%% Changes from base to side #1
237 -base
238 +a
239 +++++++ Contents of side #2
240 b
241 >>>>>>> Conflict 1 of 1 ends
242 ");
243 // Note the "Modified" below
244 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
245 diff --git a/file b/file
246 --- a/file
247 +++ b/file
248 @@ -1,7 +1,7 @@
249 <<<<<<< Conflict 1 of 1
250 %%%%%%% Changes from base to side #1
251 --base
252 -+a
253 +-some
254 ++fake
255 +++++++ Contents of side #2
256 -b
257 +conflict
258 >>>>>>> Conflict 1 of 1 ends
259 [EOF]
260 ");
261 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
262 file 2-sided conflict
263 [EOF]
264 ");
265
266 // Check that if merge tool leaves conflict markers in output file but
267 // `merge-tool-edits-conflict-markers=false` or is not specified,
268 // `jj` considers the conflict resolved.
269 work_dir.run_jj(["undo"]).success();
270 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
271 std::fs::write(
272 &editor_script,
273 [
274 "dump editor3",
275 indoc! {"
276 write
277 <<<<<<<
278 %%%%%%%
279 -some
280 +fake
281 +++++++
282 conflict
283 >>>>>>>
284 "},
285 ]
286 .join("\0"),
287 )
288 .unwrap();
289 let output = work_dir.run_jj(["resolve"]);
290 insta::assert_snapshot!(output, @r"
291 ------- stderr -------
292 Resolving conflicts in: file
293 Working copy (@) now at: vruxwmqv 2cc7f5e3 conflict | conflict
294 Parent commit (@-) : zsuskuln 45537d53 a | a
295 Parent commit (@-) : royxmykx 89d1b299 b | b
296 Added 0 files, modified 1 files, removed 0 files
297 [EOF]
298 ");
299 insta::assert_snapshot!(
300 std::fs::read_to_string(test_env.env_root().join("editor3")).unwrap(), @"");
301 // Note the "Resolved" below
302 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
303 diff --git a/file b/file
304 index 0000000000..0610716cc1 100644
305 --- a/file
306 +++ b/file
307 @@ -1,7 +1,7 @@
308 -<<<<<<< Conflict 1 of 1
309 -%%%%%%% Changes from base to side #1
310 --base
311 -+a
312 -+++++++ Contents of side #2
313 -b
314 ->>>>>>> Conflict 1 of 1 ends
315 +<<<<<<<
316 +%%%%%%%
317 +-some
318 ++fake
319 ++++++++
320 +conflict
321 +>>>>>>>
322 [EOF]
323 ");
324 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
325 ------- stderr -------
326 Error: No conflicts found at this revision
327 [EOF]
328 [exit status: 2]
329 ");
330
331 // Check that merge tool can override conflict marker style setting, and that
332 // the merge tool can output Git-style conflict markers
333 work_dir.run_jj(["undo"]).success();
334 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
335 std::fs::write(
336 &editor_script,
337 [
338 "dump editor4",
339 indoc! {"
340 write
341 <<<<<<<
342 some
343 |||||||
344 fake
345 =======
346 conflict
347 >>>>>>>
348 "},
349 ]
350 .join("\0"),
351 )
352 .unwrap();
353 let output = work_dir.run_jj([
354 "resolve",
355 "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
356 "--config=merge-tools.fake-editor.conflict-marker-style=git",
357 ]);
358 insta::assert_snapshot!(output, @r###"
359 ------- stderr -------
360 Resolving conflicts in: file
361 Working copy (@) now at: vruxwmqv d5f058ec conflict | (conflict) conflict
362 Parent commit (@-) : zsuskuln 45537d53 a | a
363 Parent commit (@-) : royxmykx 89d1b299 b | b
364 Added 0 files, modified 1 files, removed 0 files
365 Warning: There are unresolved conflicts at these paths:
366 file 2-sided conflict
367 New conflicts appeared in 1 commits:
368 vruxwmqv d5f058ec conflict | (conflict) conflict
369 Hint: To resolve the conflicts, start by creating a commit on top of
370 the conflicted commit:
371 jj new vruxwmqv
372 Then use `jj resolve`, or edit the conflict markers in the file directly.
373 Once the conflicts are resolved, you can inspect the result with `jj diff`.
374 Then run `jj squash` to move the resolution into the conflicted commit.
375 [EOF]
376 "###);
377 insta::assert_snapshot!(
378 std::fs::read_to_string(test_env.env_root().join("editor4")).unwrap(), @r"
379 <<<<<<< Side #1 (Conflict 1 of 1)
380 a
381 ||||||| Base
382 base
383 =======
384 b
385 >>>>>>> Side #2 (Conflict 1 of 1 ends)
386 ");
387 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
388 diff --git a/file b/file
389 --- a/file
390 +++ b/file
391 @@ -1,7 +1,7 @@
392 <<<<<<< Conflict 1 of 1
393 %%%%%%% Changes from base to side #1
394 --base
395 -+a
396 +-fake
397 ++some
398 +++++++ Contents of side #2
399 -b
400 +conflict
401 >>>>>>> Conflict 1 of 1 ends
402 [EOF]
403 ");
404 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
405 file 2-sided conflict
406 [EOF]
407 ");
408
409 // Check that merge tool can leave conflict markers by returning exit code 1
410 // when using `merge-conflict-exit-codes = [1]`. The Git "diff3" conflict
411 // markers should also be parsed correctly.
412 work_dir.run_jj(["undo"]).success();
413 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
414 std::fs::write(
415 &editor_script,
416 [
417 "dump editor5",
418 indoc! {"
419 write
420 <<<<<<<
421 some
422 |||||||
423 fake
424 =======
425 conflict
426 >>>>>>>
427 "},
428 "fail",
429 ]
430 .join("\0"),
431 )
432 .unwrap();
433 let output = work_dir.run_jj([
434 "resolve",
435 "--config=merge-tools.fake-editor.merge-conflict-exit-codes=[1]",
436 ]);
437 insta::assert_snapshot!(output, @r###"
438 ------- stderr -------
439 Resolving conflicts in: file
440 Working copy (@) now at: vruxwmqv 6c205356 conflict | (conflict) conflict
441 Parent commit (@-) : zsuskuln 45537d53 a | a
442 Parent commit (@-) : royxmykx 89d1b299 b | b
443 Added 0 files, modified 1 files, removed 0 files
444 Warning: There are unresolved conflicts at these paths:
445 file 2-sided conflict
446 New conflicts appeared in 1 commits:
447 vruxwmqv 6c205356 conflict | (conflict) conflict
448 Hint: To resolve the conflicts, start by creating a commit on top of
449 the conflicted commit:
450 jj new vruxwmqv
451 Then use `jj resolve`, or edit the conflict markers in the file directly.
452 Once the conflicts are resolved, you can inspect the result with `jj diff`.
453 Then run `jj squash` to move the resolution into the conflicted commit.
454 [EOF]
455 "###);
456 insta::assert_snapshot!(
457 std::fs::read_to_string(test_env.env_root().join("editor5")).unwrap(), @"");
458 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
459 diff --git a/file b/file
460 --- a/file
461 +++ b/file
462 @@ -1,7 +1,7 @@
463 <<<<<<< Conflict 1 of 1
464 %%%%%%% Changes from base to side #1
465 --base
466 -+a
467 +-fake
468 ++some
469 +++++++ Contents of side #2
470 -b
471 +conflict
472 >>>>>>> Conflict 1 of 1 ends
473 [EOF]
474 ");
475 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
476 file 2-sided conflict
477 [EOF]
478 ");
479
480 // Check that an error is reported if a merge tool indicated it would leave
481 // conflict markers, but the output file didn't contain valid conflict markers.
482 work_dir.run_jj(["undo"]).success();
483 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
484 std::fs::write(
485 &editor_script,
486 [
487 indoc! {"
488 write
489 <<<<<<< this isn't diff3 style!
490 some
491 =======
492 conflict
493 >>>>>>>
494 "},
495 "fail",
496 ]
497 .join("\0"),
498 )
499 .unwrap();
500 let output = work_dir.run_jj([
501 "resolve",
502 "--config=merge-tools.fake-editor.merge-conflict-exit-codes=[1]",
503 ]);
504 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r"
505 ------- stderr -------
506 Resolving conflicts in: file
507 Error: Failed to resolve conflicts
508 Caused by: Tool exited with exit status: 1, but did not produce valid conflict markers (run with --debug to see the exact invocation)
509 [EOF]
510 [exit status: 1]
511 ");
512
513 // TODO: Check that running `jj new` and then `jj resolve -r conflict` works
514 // correctly.
515}
516
517fn check_resolve_produces_input_file(
518 test_env: &mut TestEnvironment,
519 root: impl AsRef<Path>,
520 filename: &str,
521 role: &str,
522 expected_content: &str,
523) {
524 let editor_script = test_env.set_up_fake_editor();
525 let work_dir = test_env.work_dir(root);
526 std::fs::write(editor_script, format!("expect\n{expected_content}")).unwrap();
527
528 let merge_arg_config = format!(r#"merge-tools.fake-editor.merge-args=["${role}"]"#);
529 // This error means that fake-editor exited successfully but did not modify the
530 // output file.
531 let output = work_dir.run_jj(["resolve", "--config", &merge_arg_config, filename]);
532 insta::allow_duplicates! {
533 insta::assert_snapshot!(
534 output.normalize_stderr_with(|s| s.replacen(filename, "$FILENAME", 1)), @r"
535 ------- stderr -------
536 Resolving conflicts in: $FILENAME
537 Error: Failed to resolve conflicts
538 Caused by: The output file is either unchanged or empty after the editor quit (run with --debug to see the exact invocation).
539 [EOF]
540 [exit status: 1]
541 ");
542 }
543}
544
545#[test]
546fn test_normal_conflict_input_files() {
547 let mut test_env = TestEnvironment::default();
548 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
549 let work_dir = test_env.work_dir("repo");
550
551 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
552 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
553 create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
554 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
555 // Test the setup
556 insta::assert_snapshot!(get_log_output(&work_dir), @r"
557 @ conflict
558 ├─╮
559 │ ○ b
560 ○ │ a
561 ├─╯
562 ○ base
563 ◆
564 [EOF]
565 ");
566 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
567 file 2-sided conflict
568 [EOF]
569 ");
570 insta::assert_snapshot!(work_dir.read_file("file"), @r"
571 <<<<<<< Conflict 1 of 1
572 %%%%%%% Changes from base to side #1
573 -base
574 +a
575 +++++++ Contents of side #2
576 b
577 >>>>>>> Conflict 1 of 1 ends
578 ");
579
580 check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "base\n");
581 check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
582 check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "b\n");
583}
584
585#[test]
586fn test_baseless_conflict_input_files() {
587 let mut test_env = TestEnvironment::default();
588 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
589 let work_dir = test_env.work_dir("repo");
590
591 create_commit_with_files(&work_dir, "base", &[], &[]);
592 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
593 create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
594 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
595 // Test the setup
596 insta::assert_snapshot!(get_log_output(&work_dir), @r"
597 @ conflict
598 ├─╮
599 │ ○ b
600 ○ │ a
601 ├─╯
602 ○ base
603 ◆
604 [EOF]
605 ");
606 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
607 file 2-sided conflict
608 [EOF]
609 ");
610 insta::assert_snapshot!(work_dir.read_file("file"), @r"
611 <<<<<<< Conflict 1 of 1
612 %%%%%%% Changes from base to side #1
613 +a
614 +++++++ Contents of side #2
615 b
616 >>>>>>> Conflict 1 of 1 ends
617 ");
618
619 check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "");
620 check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
621 check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "b\n");
622}
623
624#[test]
625fn test_too_many_parents() {
626 let test_env = TestEnvironment::default();
627 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
628 let work_dir = test_env.work_dir("repo");
629
630 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
631 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
632 create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
633 create_commit_with_files(&work_dir, "c", &["base"], &[("file", "c\n")]);
634 create_commit_with_files(&work_dir, "conflict", &["a", "b", "c"], &[]);
635 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
636 file 3-sided conflict
637 [EOF]
638 ");
639 // Test warning color
640 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @r"
641 file [38;5;1m3-sided[38;5;3m conflict[39m
642 [EOF]
643 ");
644
645 let output = work_dir.run_jj(["resolve"]);
646 insta::assert_snapshot!(output, @r#"
647 ------- stderr -------
648 Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
649 Error: Failed to resolve conflicts
650 Caused by: The conflict at "file" has 3 sides. At most 2 sides are supported.
651 Hint: Edit the conflict markers manually to resolve this.
652 [EOF]
653 [exit status: 1]
654 "#);
655}
656
657#[test]
658fn test_simplify_conflict_sides() {
659 let mut 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 // Creates a 4-sided conflict, with fileA and fileB having different conflicts:
664 // fileA: A - B + C - B + B - B + B
665 // fileB: A - A + A - A + B - C + D
666 create_commit_with_files(
667 &work_dir,
668 "base",
669 &[],
670 &[("fileA", "base\n"), ("fileB", "base\n")],
671 );
672 create_commit_with_files(&work_dir, "a1", &["base"], &[("fileA", "1\n")]);
673 create_commit_with_files(&work_dir, "a2", &["base"], &[("fileA", "2\n")]);
674 create_commit_with_files(&work_dir, "b1", &["base"], &[("fileB", "1\n")]);
675 create_commit_with_files(&work_dir, "b2", &["base"], &[("fileB", "2\n")]);
676 create_commit_with_files(&work_dir, "conflictA", &["a1", "a2"], &[]);
677 create_commit_with_files(&work_dir, "conflictB", &["b1", "b2"], &[]);
678 create_commit_with_files(&work_dir, "conflict", &["conflictA", "conflictB"], &[]);
679
680 // Even though the tree-level conflict is a 4-sided conflict, each file is
681 // materialized as a 2-sided conflict.
682 insta::assert_snapshot!(work_dir.run_jj(["debug", "tree"]), @r#"
683 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 })]))
684 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 })]))
685 [EOF]
686 "#);
687 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
688 fileA 2-sided conflict
689 fileB 2-sided conflict
690 [EOF]
691 ");
692 insta::assert_snapshot!(work_dir.read_file("fileA"), @r"
693 <<<<<<< Conflict 1 of 1
694 %%%%%%% Changes from base to side #1
695 -base
696 +1
697 +++++++ Contents of side #2
698 2
699 >>>>>>> Conflict 1 of 1 ends
700 ");
701 insta::assert_snapshot!(work_dir.read_file("fileB"), @r"
702 <<<<<<< Conflict 1 of 1
703 %%%%%%% Changes from base to side #1
704 -base
705 +1
706 +++++++ Contents of side #2
707 2
708 >>>>>>> Conflict 1 of 1 ends
709 ");
710
711 // Conflict should be simplified before being handled by external merge tool.
712 check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "base", "base\n");
713 check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "left", "1\n");
714 check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "right", "2\n");
715 check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "base", "base\n");
716 check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "left", "1\n");
717 check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "right", "2\n");
718
719 // Check that simplified conflicts are still parsed as conflicts after editing
720 // when `merge-tool-edits-conflict-markers=true`.
721 let editor_script = test_env.set_up_fake_editor();
722 std::fs::write(
723 editor_script,
724 indoc! {"
725 write
726 <<<<<<< Conflict 1 of 1
727 %%%%%%% Changes from base to side #1
728 -base_edited
729 +1_edited
730 +++++++ Contents of side #2
731 2_edited
732 >>>>>>> Conflict 1 of 1 ends
733 "},
734 )
735 .unwrap();
736 let work_dir = test_env.work_dir("repo");
737 let output = work_dir.run_jj([
738 "resolve",
739 "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
740 "fileB",
741 ]);
742 insta::assert_snapshot!(output, @r###"
743 ------- stderr -------
744 Resolving conflicts in: fileB
745 Working copy (@) now at: nkmrtpmo 25c5dd0b conflict | (conflict) conflict
746 Parent commit (@-) : kmkuslsw ccb05364 conflictA | (conflict) (empty) conflictA
747 Parent commit (@-) : lylxulpl d9bc60cb conflictB | (conflict) (empty) conflictB
748 Added 0 files, modified 1 files, removed 0 files
749 Warning: There are unresolved conflicts at these paths:
750 fileA 2-sided conflict
751 fileB 2-sided conflict
752 New conflicts appeared in 1 commits:
753 nkmrtpmo 25c5dd0b conflict | (conflict) conflict
754 Hint: To resolve the conflicts, start by creating a commit on top of
755 the conflicted commit:
756 jj new nkmrtpmo
757 Then use `jj resolve`, or edit the conflict markers in the file directly.
758 Once the conflicts are resolved, you can inspect the result with `jj diff`.
759 Then run `jj squash` to move the resolution into the conflicted commit.
760 [EOF]
761 "###);
762 insta::assert_snapshot!(work_dir.read_file("fileB"), @r"
763 <<<<<<< Conflict 1 of 1
764 %%%%%%% Changes from base to side #1
765 -base_edited
766 +1_edited
767 +++++++ Contents of side #2
768 2_edited
769 >>>>>>> Conflict 1 of 1 ends
770 ");
771 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
772 fileA 2-sided conflict
773 fileB 2-sided conflict
774 [EOF]
775 ");
776}
777
778#[test]
779fn test_edit_delete_conflict_input_files() {
780 let mut test_env = TestEnvironment::default();
781 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
782 let work_dir = test_env.work_dir("repo");
783
784 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
785 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
786 create_commit_with_files(&work_dir, "b", &["base"], &[]);
787 work_dir.remove_file("file");
788 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
789 // Test the setup
790 insta::assert_snapshot!(get_log_output(&work_dir), @r"
791 @ conflict
792 ├─╮
793 │ ○ b
794 ○ │ a
795 ├─╯
796 ○ base
797 ◆
798 [EOF]
799 ");
800 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
801 file 2-sided conflict including 1 deletion
802 [EOF]
803 ");
804 insta::assert_snapshot!(work_dir.read_file("file"), @r"
805 <<<<<<< Conflict 1 of 1
806 +++++++ Contents of side #1
807 a
808 %%%%%%% Changes from base to side #2
809 -base
810 >>>>>>> Conflict 1 of 1 ends
811 ");
812
813 check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "base\n");
814 check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
815 check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "");
816}
817
818#[test]
819fn test_file_vs_dir() {
820 let test_env = TestEnvironment::default();
821 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
822 let work_dir = test_env.work_dir("repo");
823
824 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
825 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
826 create_commit_with_files(&work_dir, "b", &["base"], &[]);
827 work_dir.remove_file("file");
828 work_dir.create_dir("file");
829 // Without a placeholder file, `jj` ignores an empty directory
830 work_dir.write_file("file/placeholder", "");
831 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
832 insta::assert_snapshot!(get_log_output(&work_dir), @r"
833 @ conflict
834 ├─╮
835 │ ○ b
836 ○ │ a
837 ├─╯
838 ○ base
839 ◆
840 [EOF]
841 ");
842
843 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
844 file 2-sided conflict including a directory
845 [EOF]
846 ");
847 let output = work_dir.run_jj(["resolve"]);
848 insta::assert_snapshot!(output, @r#"
849 ------- stderr -------
850 Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
851 Error: Failed to resolve conflicts
852 Caused by: Only conflicts that involve normal files (not symlinks, etc.) are supported. Conflict summary for "file":
853 Conflict:
854 Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
855 Adding file with id 78981922613b2afb6025042ff6bd878ac1994e85
856 Adding tree with id 133bb38fc4e4bf6b551f1f04db7e48f04cac2877
857 [EOF]
858 [exit status: 1]
859 "#);
860}
861
862#[test]
863fn test_description_with_dir_and_deletion() {
864 let test_env = TestEnvironment::default();
865 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
866 let work_dir = test_env.work_dir("repo");
867
868 create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
869 create_commit_with_files(&work_dir, "edit", &["base"], &[("file", "b\n")]);
870 create_commit_with_files(&work_dir, "dir", &["base"], &[]);
871 work_dir.remove_file("file");
872 work_dir.create_dir("file");
873 // Without a placeholder file, `jj` ignores an empty directory
874 work_dir.write_file("file/placeholder", "");
875 create_commit_with_files(&work_dir, "del", &["base"], &[]);
876 work_dir.remove_file("file");
877 create_commit_with_files(&work_dir, "conflict", &["edit", "dir", "del"], &[]);
878 insta::assert_snapshot!(get_log_output(&work_dir), @r"
879 @ conflict
880 ├─┬─╮
881 │ │ ○ del
882 │ ○ │ dir
883 │ ├─╯
884 ○ │ edit
885 ├─╯
886 ○ base
887 ◆
888 [EOF]
889 ");
890
891 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
892 file 3-sided conflict including 1 deletion and a directory
893 [EOF]
894 ");
895 // Test warning color. The deletion is fine, so it's not highlighted
896 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @r"
897 file [38;5;1m3-sided[38;5;3m conflict including 1 deletion and [38;5;1ma directory[39m
898 [EOF]
899 ");
900 let output = work_dir.run_jj(["resolve"]);
901 insta::assert_snapshot!(output, @r#"
902 ------- stderr -------
903 Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
904 Error: Failed to resolve conflicts
905 Caused by: Only conflicts that involve normal files (not symlinks, etc.) are supported. Conflict summary for "file":
906 Conflict:
907 Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
908 Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
909 Adding file with id 61780798228d17af2d34fce4cfbdf35556832472
910 Adding tree with id 133bb38fc4e4bf6b551f1f04db7e48f04cac2877
911 [EOF]
912 [exit status: 1]
913 "#);
914}
915
916#[test]
917fn test_resolve_conflicts_with_executable() {
918 let mut test_env = TestEnvironment::default();
919 let editor_script = test_env.set_up_fake_editor();
920 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
921 let work_dir = test_env.work_dir("repo");
922
923 // Create a conflict in "file1" where all 3 terms are executables, and create a
924 // conflict in "file2" where one side set the executable bit.
925 create_commit_with_files(
926 &work_dir,
927 "base",
928 &[],
929 &[("file1", "base1\n"), ("file2", "base2\n")],
930 );
931 work_dir.run_jj(["file", "chmod", "x", "file1"]).success();
932 create_commit_with_files(
933 &work_dir,
934 "a",
935 &["base"],
936 &[("file1", "a1\n"), ("file2", "a2\n")],
937 );
938 create_commit_with_files(
939 &work_dir,
940 "b",
941 &["base"],
942 &[("file1", "b1\n"), ("file2", "b2\n")],
943 );
944 work_dir.run_jj(["file", "chmod", "x", "file2"]).success();
945 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
946 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
947 file1 2-sided conflict including an executable
948 file2 2-sided conflict including an executable
949 [EOF]
950 ");
951 insta::assert_snapshot!(work_dir.read_file("file1"), @r"
952 <<<<<<< Conflict 1 of 1
953 %%%%%%% Changes from base to side #1
954 -base1
955 +a1
956 +++++++ Contents of side #2
957 b1
958 >>>>>>> Conflict 1 of 1 ends
959 "
960 );
961 insta::assert_snapshot!(work_dir.read_file("file2"), @r"
962 <<<<<<< Conflict 1 of 1
963 %%%%%%% Changes from base to side #1
964 -base2
965 +a2
966 +++++++ Contents of side #2
967 b2
968 >>>>>>> Conflict 1 of 1 ends
969 "
970 );
971
972 // Test resolving the conflict in "file1", which should produce an executable
973 std::fs::write(&editor_script, b"write\nresolution1\n").unwrap();
974 let output = work_dir.run_jj(["resolve", "file1"]);
975 insta::assert_snapshot!(output, @r###"
976 ------- stderr -------
977 Resolving conflicts in: file1
978 Working copy (@) now at: znkkpsqq 8ab9c54e conflict | (conflict) conflict
979 Parent commit (@-) : mzvwutvl 86f7f0e3 a | a
980 Parent commit (@-) : yqosqzyt 36361412 b | b
981 Added 0 files, modified 1 files, removed 0 files
982 Warning: There are unresolved conflicts at these paths:
983 file2 2-sided conflict including an executable
984 New conflicts appeared in 1 commits:
985 znkkpsqq 8ab9c54e conflict | (conflict) conflict
986 Hint: To resolve the conflicts, start by creating a commit on top of
987 the conflicted commit:
988 jj new znkkpsqq
989 Then use `jj resolve`, or edit the conflict markers in the file directly.
990 Once the conflicts are resolved, you can inspect the result with `jj diff`.
991 Then run `jj squash` to move the resolution into the conflicted commit.
992 [EOF]
993 "###);
994 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
995 diff --git a/file1 b/file1
996 index 0000000000..95cc18629d 100755
997 --- a/file1
998 +++ b/file1
999 @@ -1,7 +1,1 @@
1000 -<<<<<<< Conflict 1 of 1
1001 -%%%%%%% Changes from base to side #1
1002 --base1
1003 -+a1
1004 -+++++++ Contents of side #2
1005 -b1
1006 ->>>>>>> Conflict 1 of 1 ends
1007 +resolution1
1008 [EOF]
1009 ");
1010 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1011 file2 2-sided conflict including an executable
1012 [EOF]
1013 ");
1014
1015 // Test resolving the conflict in "file2", which should produce an executable
1016 work_dir.run_jj(["undo"]).success();
1017 std::fs::write(&editor_script, b"write\nresolution2\n").unwrap();
1018 let output = work_dir.run_jj(["resolve", "file2"]);
1019 insta::assert_snapshot!(output, @r###"
1020 ------- stderr -------
1021 Resolving conflicts in: file2
1022 Working copy (@) now at: znkkpsqq d47830a6 conflict | (conflict) conflict
1023 Parent commit (@-) : mzvwutvl 86f7f0e3 a | a
1024 Parent commit (@-) : yqosqzyt 36361412 b | b
1025 Added 0 files, modified 1 files, removed 0 files
1026 Warning: There are unresolved conflicts at these paths:
1027 file1 2-sided conflict including an executable
1028 New conflicts appeared in 1 commits:
1029 znkkpsqq d47830a6 conflict | (conflict) conflict
1030 Hint: To resolve the conflicts, start by creating a commit on top of
1031 the conflicted commit:
1032 jj new znkkpsqq
1033 Then use `jj resolve`, or edit the conflict markers in the file directly.
1034 Once the conflicts are resolved, you can inspect the result with `jj diff`.
1035 Then run `jj squash` to move the resolution into the conflicted commit.
1036 [EOF]
1037 "###);
1038 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1039 diff --git a/file2 b/file2
1040 index 0000000000..775f078581 100755
1041 --- a/file2
1042 +++ b/file2
1043 @@ -1,7 +1,1 @@
1044 -<<<<<<< Conflict 1 of 1
1045 -%%%%%%% Changes from base to side #1
1046 --base2
1047 -+a2
1048 -+++++++ Contents of side #2
1049 -b2
1050 ->>>>>>> Conflict 1 of 1 ends
1051 +resolution2
1052 [EOF]
1053 ");
1054 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1055 file1 2-sided conflict including an executable
1056 [EOF]
1057 ");
1058
1059 // Pick "our" contents, but merges executable bits
1060 work_dir.run_jj(["undo"]).success();
1061 let output = work_dir.run_jj(["resolve", "--tool=:ours"]);
1062 insta::assert_snapshot!(output, @r"
1063 ------- stderr -------
1064 Working copy (@) now at: znkkpsqq d902c14b conflict | conflict
1065 Parent commit (@-) : mzvwutvl 86f7f0e3 a | a
1066 Parent commit (@-) : yqosqzyt 36361412 b | b
1067 Added 0 files, modified 2 files, removed 0 files
1068 [EOF]
1069 ");
1070 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1071 diff --git a/file1 b/file1
1072 index 0000000000..da0f8ed91a 100755
1073 --- a/file1
1074 +++ b/file1
1075 @@ -1,7 +1,1 @@
1076 -<<<<<<< Conflict 1 of 1
1077 -%%%%%%% Changes from base to side #1
1078 --base1
1079 -+a1
1080 -+++++++ Contents of side #2
1081 -b1
1082 ->>>>>>> Conflict 1 of 1 ends
1083 +a1
1084 diff --git a/file2 b/file2
1085 index 0000000000..c1827f07e1 100755
1086 --- a/file2
1087 +++ b/file2
1088 @@ -1,7 +1,1 @@
1089 -<<<<<<< Conflict 1 of 1
1090 -%%%%%%% Changes from base to side #1
1091 --base2
1092 -+a2
1093 -+++++++ Contents of side #2
1094 -b2
1095 ->>>>>>> Conflict 1 of 1 ends
1096 +a2
1097 [EOF]
1098 ");
1099
1100 // Pick "their" contents, but merges executable bits
1101 work_dir.run_jj(["undo"]).success();
1102 let output = work_dir.run_jj(["resolve", "--tool=:theirs"]);
1103 insta::assert_snapshot!(output, @r"
1104 ------- stderr -------
1105 Working copy (@) now at: znkkpsqq a340ca5f conflict | conflict
1106 Parent commit (@-) : mzvwutvl 86f7f0e3 a | a
1107 Parent commit (@-) : yqosqzyt 36361412 b | b
1108 Added 0 files, modified 2 files, removed 0 files
1109 [EOF]
1110 ");
1111 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1112 diff --git a/file1 b/file1
1113 index 0000000000..c9c6af7f78 100755
1114 --- a/file1
1115 +++ b/file1
1116 @@ -1,7 +1,1 @@
1117 -<<<<<<< Conflict 1 of 1
1118 -%%%%%%% Changes from base to side #1
1119 --base1
1120 -+a1
1121 -+++++++ Contents of side #2
1122 b1
1123 ->>>>>>> Conflict 1 of 1 ends
1124 diff --git a/file2 b/file2
1125 index 0000000000..e6bfff5c1d 100755
1126 --- a/file2
1127 +++ b/file2
1128 @@ -1,7 +1,1 @@
1129 -<<<<<<< Conflict 1 of 1
1130 -%%%%%%% Changes from base to side #1
1131 --base2
1132 -+a2
1133 -+++++++ Contents of side #2
1134 b2
1135 ->>>>>>> Conflict 1 of 1 ends
1136 [EOF]
1137 ");
1138}
1139
1140#[test]
1141fn test_resolve_change_delete_executable() {
1142 let mut test_env = TestEnvironment::default();
1143 let editor_script = test_env.set_up_fake_editor();
1144 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1145 let work_dir = test_env.work_dir("repo");
1146
1147 let file_template =
1148 r#"separate(' ', path, if(conflict, "c", "-"), if(executable, "x", "-")) ++ "\n""#;
1149 let file_list = |path: &str| work_dir.run_jj(["file", "list", "-T", file_template, path]);
1150
1151 // base a b
1152 // file1: normal -> { exec, absent }
1153 // file2: exec -> { absent, normal } (with content change)
1154 // file3: absent -> { normal, exec }
1155 // file4: normal -> { normal, absent } (with content change)
1156 // file5: exec -> { absent, exec } (with content change)
1157 create_commit_with_files(
1158 &work_dir,
1159 "base",
1160 &[],
1161 &[("file1", ""), ("file2", ""), ("file4", ""), ("file5", "")],
1162 );
1163 work_dir
1164 .run_jj(["file", "chmod", "x", "file2", "file5"])
1165 .success();
1166 create_commit_with_files(
1167 &work_dir,
1168 "a",
1169 &["base"],
1170 &[("file1", ""), ("file3", ""), ("file4", "a4\n")],
1171 );
1172 work_dir.remove_file("file2");
1173 work_dir.remove_file("file5");
1174 work_dir.run_jj(["file", "chmod", "x", "file1"]).success();
1175 create_commit_with_files(
1176 &work_dir,
1177 "b",
1178 &["base"],
1179 &[("file2", "b2\n"), ("file3", ""), ("file5", "b5\n")],
1180 );
1181 work_dir.remove_file("file1");
1182 work_dir.remove_file("file4");
1183 work_dir.run_jj(["file", "chmod", "n", "file2"]).success();
1184 work_dir.run_jj(["file", "chmod", "x", "file3"]).success();
1185 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
1186
1187 // Test the setup
1188 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1189 file1 2-sided conflict including 1 deletion and an executable
1190 file2 2-sided conflict including 1 deletion and an executable
1191 file3 2-sided conflict including an executable
1192 file4 2-sided conflict including 1 deletion
1193 file5 2-sided conflict including 1 deletion and an executable
1194 [EOF]
1195 ");
1196 insta::assert_snapshot!(file_list("all()"), @r"
1197 file1 c -
1198 file2 c -
1199 file3 c -
1200 file4 c -
1201 file5 c x
1202 [EOF]
1203 ");
1204 insta::assert_snapshot!(work_dir.run_jj(["log", "--git"]), @r"
1205 @ kmkuslsw test.user@example.com 2001-02-03 08:05:18 conflict 7a7ac759 conflict
1206 ├─╮ (empty) conflict
1207 │ ○ vruxwmqv test.user@example.com 2001-02-03 08:05:17 b 888b6cc3
1208 │ │ b
1209 │ │ diff --git a/file1 b/file1
1210 │ │ deleted file mode 100644
1211 │ │ index e69de29bb2..0000000000
1212 │ │ diff --git a/file2 b/file2
1213 │ │ old mode 100755
1214 │ │ new mode 100644
1215 │ │ index e69de29bb2..e6bfff5c1d
1216 │ │ --- a/file2
1217 │ │ +++ b/file2
1218 │ │ @@ -0,0 +1,1 @@
1219 │ │ +b2
1220 │ │ diff --git a/file3 b/file3
1221 │ │ new file mode 100755
1222 │ │ index 0000000000..e69de29bb2
1223 │ │ diff --git a/file4 b/file4
1224 │ │ deleted file mode 100644
1225 │ │ index e69de29bb2..0000000000
1226 │ │ diff --git a/file5 b/file5
1227 │ │ index e69de29bb2..90a5159bf0 100755
1228 │ │ --- a/file5
1229 │ │ +++ b/file5
1230 │ │ @@ -0,0 +1,1 @@
1231 │ │ +b5
1232 ○ │ mzvwutvl test.user@example.com 2001-02-03 08:05:13 a e2d3924b
1233 ├─╯ a
1234 │ diff --git a/file1 b/file1
1235 │ old mode 100644
1236 │ new mode 100755
1237 │ diff --git a/file2 b/file2
1238 │ deleted file mode 100755
1239 │ index e69de29bb2..0000000000
1240 │ diff --git a/file3 b/file3
1241 │ new file mode 100644
1242 │ index 0000000000..e69de29bb2
1243 │ diff --git a/file4 b/file4
1244 │ index e69de29bb2..88ba23dca8 100644
1245 │ --- a/file4
1246 │ +++ b/file4
1247 │ @@ -0,0 +1,1 @@
1248 │ +a4
1249 │ diff --git a/file5 b/file5
1250 │ deleted file mode 100755
1251 │ index e69de29bb2..0000000000
1252 ○ rlvkpnrz test.user@example.com 2001-02-03 08:05:10 base f747aa1f
1253 │ base
1254 │ diff --git a/file1 b/file1
1255 │ new file mode 100644
1256 │ index 0000000000..e69de29bb2
1257 │ diff --git a/file2 b/file2
1258 │ new file mode 100755
1259 │ index 0000000000..e69de29bb2
1260 │ diff --git a/file4 b/file4
1261 │ new file mode 100644
1262 │ index 0000000000..e69de29bb2
1263 │ diff --git a/file5 b/file5
1264 │ new file mode 100755
1265 │ index 0000000000..e69de29bb2
1266 ◆ zzzzzzzz root() 00000000
1267 [EOF]
1268 ");
1269
1270 // Exec bit conflict can be resolved by chmod
1271 let output = work_dir.run_jj(["resolve", "file1"]);
1272 insta::assert_snapshot!(output, @r#"
1273 ------- stderr -------
1274 Error: Failed to resolve conflicts
1275 Caused by: "file1" has conflicts in executable bit
1276 Conflict:
1277 Removing file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
1278 Adding executable file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
1279 Hint: Use `jj file chmod` to update the executable bit.
1280 [EOF]
1281 [exit status: 1]
1282 "#);
1283 let output = work_dir.run_jj(["file", "chmod", "--quiet", "x", "file1"]);
1284 insta::assert_snapshot!(output, @"");
1285
1286 // Exec bit conflict can be resolved by chmod, then content conflict
1287 let output = work_dir.run_jj(["resolve", "file2"]);
1288 insta::assert_snapshot!(output, @r#"
1289 ------- stderr -------
1290 Error: Failed to resolve conflicts
1291 Caused by: "file2" has conflicts in executable bit
1292 Conflict:
1293 Removing executable file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
1294 Adding file with id e6bfff5c1d0f0ecd501552b43a1e13d8008abc31
1295 Hint: Use `jj file chmod` to update the executable bit.
1296 [EOF]
1297 [exit status: 1]
1298 "#);
1299 let output = work_dir.run_jj(["file", "chmod", "--quiet", "n", "file2"]);
1300 insta::assert_snapshot!(output, @"");
1301 std::fs::write(&editor_script, "write\nresolved\n").unwrap();
1302 let output = work_dir.run_jj(["resolve", "file2"]);
1303 insta::assert_snapshot!(output, @r"
1304 ------- stderr -------
1305 Resolving conflicts in: file2
1306 Working copy (@) now at: kmkuslsw 1323520e conflict | (conflict) conflict
1307 Parent commit (@-) : mzvwutvl e2d3924b a | a
1308 Parent commit (@-) : vruxwmqv 888b6cc3 b | b
1309 Added 0 files, modified 1 files, removed 0 files
1310 Warning: There are unresolved conflicts at these paths:
1311 file3 2-sided conflict including an executable
1312 file4 2-sided conflict including 1 deletion
1313 file5 2-sided conflict including 1 deletion and an executable
1314 [EOF]
1315 ");
1316
1317 // Exec bit conflict can be resolved by chmod
1318 let output = work_dir.run_jj(["resolve", "file3"]);
1319 insta::assert_snapshot!(output, @r#"
1320 ------- stderr -------
1321 Error: Failed to resolve conflicts
1322 Caused by: "file3" has conflicts in executable bit
1323 Conflict:
1324 Adding file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
1325 Adding executable file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
1326 Hint: Use `jj file chmod` to update the executable bit.
1327 [EOF]
1328 [exit status: 1]
1329 "#);
1330 let output = work_dir.run_jj(["file", "chmod", "--quiet", "x", "file3"]);
1331 insta::assert_snapshot!(output, @"");
1332
1333 // Take modified content, the executable bit should be kept as "-"
1334 let output = work_dir.run_jj(["resolve", "file4", "--tool=:ours"]);
1335 insta::assert_snapshot!(output, @r"
1336 ------- stderr -------
1337 Working copy (@) now at: kmkuslsw 630e8689 conflict | (conflict) conflict
1338 Parent commit (@-) : mzvwutvl e2d3924b a | a
1339 Parent commit (@-) : vruxwmqv 888b6cc3 b | b
1340 Added 0 files, modified 1 files, removed 0 files
1341 Warning: There are unresolved conflicts at these paths:
1342 file5 2-sided conflict including 1 deletion and an executable
1343 [EOF]
1344 ");
1345
1346 // Take modified content, the executable bit should be kept as "x"
1347 let output = work_dir.run_jj(["resolve", "file5", "--tool=:theirs"]);
1348 insta::assert_snapshot!(output, @r"
1349 ------- stderr -------
1350 Working copy (@) now at: kmkuslsw 7337267a conflict | conflict
1351 Parent commit (@-) : mzvwutvl e2d3924b a | a
1352 Parent commit (@-) : vruxwmqv 888b6cc3 b | b
1353 Added 0 files, modified 1 files, removed 0 files
1354 Existing conflicts were resolved or abandoned from 1 commits.
1355 [EOF]
1356 ");
1357
1358 insta::assert_snapshot!(file_list("all()"), @r"
1359 file2 - -
1360 file3 - x
1361 file4 - -
1362 file5 - x
1363 [EOF]
1364 ");
1365}
1366
1367#[test]
1368fn test_resolve_long_conflict_markers() {
1369 let mut test_env = TestEnvironment::default();
1370 let editor_script = test_env.set_up_fake_editor();
1371 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1372 let work_dir = test_env.work_dir("repo");
1373
1374 // Makes it easier to read the diffs between conflicts
1375 test_env.add_config("ui.conflict-marker-style = 'snapshot'");
1376
1377 // Create a conflict which requires long conflict markers to be materialized
1378 create_commit_with_files(&work_dir, "base", &[], &[("file", "======= base\n")]);
1379 create_commit_with_files(&work_dir, "a", &["base"], &[("file", "<<<<<<< a\n")]);
1380 create_commit_with_files(&work_dir, "b", &["base"], &[("file", ">>>>>>> b\n")]);
1381 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
1382 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1383 file 2-sided conflict
1384 [EOF]
1385 ");
1386 insta::assert_snapshot!(work_dir.read_file("file"), @r"
1387 <<<<<<<<<<< Conflict 1 of 1
1388 +++++++++++ Contents of side #1
1389 <<<<<<< a
1390 ----------- Contents of base
1391 ======= base
1392 +++++++++++ Contents of side #2
1393 >>>>>>> b
1394 >>>>>>>>>>> Conflict 1 of 1 ends
1395 "
1396 );
1397 // Allow signaling that conflict markers were produced even if not editing
1398 // conflict markers materialized in the output file
1399 test_env.add_config("merge-tools.fake-editor.merge-conflict-exit-codes = [1]");
1400
1401 // By default, conflict markers of length 7 or longer are parsed for
1402 // compatibility with Git merge tools
1403 std::fs::write(
1404 &editor_script,
1405 indoc! {b"
1406 write
1407 <<<<<<<
1408 A
1409 |||||||
1410 BASE
1411 =======
1412 B
1413 >>>>>>>
1414 \0fail
1415 "},
1416 )
1417 .unwrap();
1418 let output = work_dir.run_jj(["resolve"]);
1419 insta::assert_snapshot!(output, @r###"
1420 ------- stderr -------
1421 Resolving conflicts in: file
1422 Working copy (@) now at: vruxwmqv 1e254ee3 conflict | (conflict) conflict
1423 Parent commit (@-) : zsuskuln 10d994ef a | a
1424 Parent commit (@-) : royxmykx 7f215575 b | b
1425 Added 0 files, modified 1 files, removed 0 files
1426 Warning: There are unresolved conflicts at these paths:
1427 file 2-sided conflict
1428 New conflicts appeared in 1 commits:
1429 vruxwmqv 1e254ee3 conflict | (conflict) conflict
1430 Hint: To resolve the conflicts, start by creating a commit on top of
1431 the conflicted commit:
1432 jj new vruxwmqv
1433 Then use `jj resolve`, or edit the conflict markers in the file directly.
1434 Once the conflicts are resolved, you can inspect the result with `jj diff`.
1435 Then run `jj squash` to move the resolution into the conflicted commit.
1436 [EOF]
1437 "###);
1438 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1439 diff --git a/file b/file
1440 --- a/file
1441 +++ b/file
1442 @@ -1,8 +1,8 @@
1443 -<<<<<<<<<<< Conflict 1 of 1
1444 -+++++++++++ Contents of side #1
1445 -<<<<<<< a
1446 ------------ Contents of base
1447 -======= base
1448 -+++++++++++ Contents of side #2
1449 ->>>>>>> b
1450 ->>>>>>>>>>> Conflict 1 of 1 ends
1451 +<<<<<<< Conflict 1 of 1
1452 ++++++++ Contents of side #1
1453 +A
1454 +------- Contents of base
1455 +BASE
1456 ++++++++ Contents of side #2
1457 +B
1458 +>>>>>>> Conflict 1 of 1 ends
1459 [EOF]
1460 ");
1461 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1462 file 2-sided conflict
1463 [EOF]
1464 ");
1465
1466 // If the merge tool edits the output file with materialized markers, the
1467 // markers must match the length of the materialized markers to be parsed
1468 work_dir.run_jj(["undo"]).success();
1469 std::fs::write(
1470 &editor_script,
1471 indoc! {b"
1472 dump editor
1473 \0write
1474 <<<<<<<<<<<
1475 <<<<<<< A
1476 |||||||||||
1477 ======= BASE
1478 ===========
1479 >>>>>>> B
1480 >>>>>>>>>>>
1481 \0fail
1482 "},
1483 )
1484 .unwrap();
1485 let output = work_dir.run_jj([
1486 "resolve",
1487 "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
1488 ]);
1489 insta::assert_snapshot!(output, @r###"
1490 ------- stderr -------
1491 Resolving conflicts in: file
1492 Working copy (@) now at: vruxwmqv 2481a401 conflict | (conflict) conflict
1493 Parent commit (@-) : zsuskuln 10d994ef a | a
1494 Parent commit (@-) : royxmykx 7f215575 b | b
1495 Added 0 files, modified 1 files, removed 0 files
1496 Warning: There are unresolved conflicts at these paths:
1497 file 2-sided conflict
1498 New conflicts appeared in 1 commits:
1499 vruxwmqv 2481a401 conflict | (conflict) conflict
1500 Hint: To resolve the conflicts, start by creating a commit on top of
1501 the conflicted commit:
1502 jj new vruxwmqv
1503 Then use `jj resolve`, or edit the conflict markers in the file directly.
1504 Once the conflicts are resolved, you can inspect the result with `jj diff`.
1505 Then run `jj squash` to move the resolution into the conflicted commit.
1506 [EOF]
1507 "###);
1508 insta::assert_snapshot!(
1509 std::fs::read_to_string(test_env.env_root().join("editor")).unwrap(), @r"
1510 <<<<<<<<<<< Conflict 1 of 1
1511 +++++++++++ Contents of side #1
1512 <<<<<<< a
1513 ----------- Contents of base
1514 ======= base
1515 +++++++++++ Contents of side #2
1516 >>>>>>> b
1517 >>>>>>>>>>> Conflict 1 of 1 ends
1518 ");
1519 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1520 diff --git a/file b/file
1521 --- a/file
1522 +++ b/file
1523 @@ -1,8 +1,8 @@
1524 <<<<<<<<<<< Conflict 1 of 1
1525 +++++++++++ Contents of side #1
1526 -<<<<<<< a
1527 +<<<<<<< A
1528 ----------- Contents of base
1529 -======= base
1530 +======= BASE
1531 +++++++++++ Contents of side #2
1532 ->>>>>>> b
1533 +>>>>>>> B
1534 >>>>>>>>>>> Conflict 1 of 1 ends
1535 [EOF]
1536 ");
1537 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1538 file 2-sided conflict
1539 [EOF]
1540 ");
1541
1542 // If the merge tool accepts the marker length as an argument, then the conflict
1543 // markers should be at least as long as "$marker_length"
1544 work_dir.run_jj(["undo"]).success();
1545 std::fs::write(
1546 &editor_script,
1547 indoc! {b"
1548 expect-arg 0
1549 11\0write
1550 <<<<<<<<<<<
1551 <<<<<<< A
1552 |||||||||||
1553 ======= BASE
1554 ===========
1555 >>>>>>> B
1556 >>>>>>>>>>>
1557 \0fail
1558 "},
1559 )
1560 .unwrap();
1561 let output = work_dir.run_jj([
1562 "resolve",
1563 r#"--config=merge-tools.fake-editor.merge-args=["$output", "$marker_length"]"#,
1564 ]);
1565 insta::assert_snapshot!(output, @r###"
1566 ------- stderr -------
1567 Resolving conflicts in: file
1568 Working copy (@) now at: vruxwmqv 2cf0bfd3 conflict | (conflict) conflict
1569 Parent commit (@-) : zsuskuln 10d994ef a | a
1570 Parent commit (@-) : royxmykx 7f215575 b | b
1571 Added 0 files, modified 1 files, removed 0 files
1572 Warning: There are unresolved conflicts at these paths:
1573 file 2-sided conflict
1574 New conflicts appeared in 1 commits:
1575 vruxwmqv 2cf0bfd3 conflict | (conflict) conflict
1576 Hint: To resolve the conflicts, start by creating a commit on top of
1577 the conflicted commit:
1578 jj new vruxwmqv
1579 Then use `jj resolve`, or edit the conflict markers in the file directly.
1580 Once the conflicts are resolved, you can inspect the result with `jj diff`.
1581 Then run `jj squash` to move the resolution into the conflicted commit.
1582 [EOF]
1583 "###);
1584 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1585 diff --git a/file b/file
1586 --- a/file
1587 +++ b/file
1588 @@ -1,8 +1,8 @@
1589 <<<<<<<<<<< Conflict 1 of 1
1590 +++++++++++ Contents of side #1
1591 -<<<<<<< a
1592 +<<<<<<< A
1593 ----------- Contents of base
1594 -======= base
1595 +======= BASE
1596 +++++++++++ Contents of side #2
1597 ->>>>>>> b
1598 +>>>>>>> B
1599 >>>>>>>>>>> Conflict 1 of 1 ends
1600 [EOF]
1601 ");
1602 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1603 file 2-sided conflict
1604 [EOF]
1605 ");
1606}
1607
1608#[test]
1609fn test_multiple_conflicts() {
1610 let mut test_env = TestEnvironment::default();
1611 let editor_script = test_env.set_up_fake_editor();
1612 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1613 let work_dir = test_env.work_dir("repo");
1614
1615 create_commit_with_files(
1616 &work_dir,
1617 "base",
1618 &[],
1619 &[
1620 (
1621 "this_file_has_a_very_long_name_to_test_padding",
1622 "first base\n",
1623 ),
1624 ("another_file", "second base\n"),
1625 ],
1626 );
1627 create_commit_with_files(
1628 &work_dir,
1629 "a",
1630 &["base"],
1631 &[
1632 (
1633 "this_file_has_a_very_long_name_to_test_padding",
1634 "first a\n",
1635 ),
1636 ("another_file", "second a\n"),
1637 ],
1638 );
1639 create_commit_with_files(
1640 &work_dir,
1641 "b",
1642 &["base"],
1643 &[
1644 (
1645 "this_file_has_a_very_long_name_to_test_padding",
1646 "first b\n",
1647 ),
1648 ("another_file", "second b\n"),
1649 ],
1650 );
1651 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
1652 // Test the setup
1653 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1654 @ conflict
1655 ├─╮
1656 │ ○ b
1657 ○ │ a
1658 ├─╯
1659 ○ base
1660 ◆
1661 [EOF]
1662 ");
1663 insta::assert_snapshot!(
1664 work_dir.read_file("this_file_has_a_very_long_name_to_test_padding"), @r"
1665 <<<<<<< Conflict 1 of 1
1666 %%%%%%% Changes from base to side #1
1667 -first base
1668 +first a
1669 +++++++ Contents of side #2
1670 first b
1671 >>>>>>> Conflict 1 of 1 ends
1672 ");
1673 insta::assert_snapshot!(work_dir.read_file("another_file"), @r"
1674 <<<<<<< Conflict 1 of 1
1675 %%%%%%% Changes from base to side #1
1676 -second base
1677 +second a
1678 +++++++ Contents of side #2
1679 second b
1680 >>>>>>> Conflict 1 of 1 ends
1681 ");
1682 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1683 another_file 2-sided conflict
1684 this_file_has_a_very_long_name_to_test_padding 2-sided conflict
1685 [EOF]
1686 ");
1687 // Test colors
1688 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @r"
1689 another_file [38;5;3m2-sided conflict[39m
1690 this_file_has_a_very_long_name_to_test_padding [38;5;3m2-sided conflict[39m
1691 [EOF]
1692 ");
1693
1694 // Check that we can manually pick which of the conflicts to resolve first
1695 std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n").unwrap();
1696 let output = work_dir.run_jj(["resolve", "another_file"]);
1697 insta::assert_snapshot!(output, @r###"
1698 ------- stderr -------
1699 Resolving conflicts in: another_file
1700 Working copy (@) now at: vruxwmqv d3584f6e conflict | (conflict) conflict
1701 Parent commit (@-) : zsuskuln 2c821f70 a | a
1702 Parent commit (@-) : royxmykx 4c2029de b | b
1703 Added 0 files, modified 1 files, removed 0 files
1704 Warning: There are unresolved conflicts at these paths:
1705 this_file_has_a_very_long_name_to_test_padding 2-sided conflict
1706 New conflicts appeared in 1 commits:
1707 vruxwmqv d3584f6e conflict | (conflict) conflict
1708 Hint: To resolve the conflicts, start by creating a commit on top of
1709 the conflicted commit:
1710 jj new vruxwmqv
1711 Then use `jj resolve`, or edit the conflict markers in the file directly.
1712 Once the conflicts are resolved, you can inspect the result with `jj diff`.
1713 Then run `jj squash` to move the resolution into the conflicted commit.
1714 [EOF]
1715 "###);
1716 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1717 diff --git a/another_file b/another_file
1718 index 0000000000..a9fcc7d486 100644
1719 --- a/another_file
1720 +++ b/another_file
1721 @@ -1,7 +1,1 @@
1722 -<<<<<<< Conflict 1 of 1
1723 -%%%%%%% Changes from base to side #1
1724 --second base
1725 -+second a
1726 -+++++++ Contents of side #2
1727 -second b
1728 ->>>>>>> Conflict 1 of 1 ends
1729 +resolution another_file
1730 [EOF]
1731 ");
1732 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1733 this_file_has_a_very_long_name_to_test_padding 2-sided conflict
1734 [EOF]
1735 ");
1736
1737 // Repeat the above with the `--quiet` option.
1738 work_dir.run_jj(["undo"]).success();
1739 std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n").unwrap();
1740 let output = work_dir.run_jj(["resolve", "--quiet", "another_file"]);
1741 insta::assert_snapshot!(output, @"");
1742
1743 // Without a path, `jj resolve` should call the merge tool multiple times
1744 work_dir.run_jj(["undo"]).success();
1745 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
1746 std::fs::write(
1747 &editor_script,
1748 [
1749 "expect\n",
1750 "write\nfirst resolution for auto-chosen file\n",
1751 "next invocation\n",
1752 "expect\n",
1753 "write\nsecond resolution for auto-chosen file\n",
1754 ]
1755 .join("\0"),
1756 )
1757 .unwrap();
1758 work_dir.run_jj(["resolve"]).success();
1759 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1760 diff --git a/another_file b/another_file
1761 index 0000000000..7903e1c1c7 100644
1762 --- a/another_file
1763 +++ b/another_file
1764 @@ -1,7 +1,1 @@
1765 -<<<<<<< Conflict 1 of 1
1766 -%%%%%%% Changes from base to side #1
1767 --second base
1768 -+second a
1769 -+++++++ Contents of side #2
1770 -second b
1771 ->>>>>>> Conflict 1 of 1 ends
1772 +first resolution for auto-chosen file
1773 diff --git a/this_file_has_a_very_long_name_to_test_padding b/this_file_has_a_very_long_name_to_test_padding
1774 index 0000000000..f8c72adf17 100644
1775 --- a/this_file_has_a_very_long_name_to_test_padding
1776 +++ b/this_file_has_a_very_long_name_to_test_padding
1777 @@ -1,7 +1,1 @@
1778 -<<<<<<< Conflict 1 of 1
1779 -%%%%%%% Changes from base to side #1
1780 --first base
1781 -+first a
1782 -+++++++ Contents of side #2
1783 -first b
1784 ->>>>>>> Conflict 1 of 1 ends
1785 +second resolution for auto-chosen file
1786 [EOF]
1787 ");
1788
1789 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1790 ------- stderr -------
1791 Error: No conflicts found at this revision
1792 [EOF]
1793 [exit status: 2]
1794 ");
1795 insta::assert_snapshot!(work_dir.run_jj(["resolve"]), @r"
1796 ------- stderr -------
1797 Error: No conflicts found at this revision
1798 [EOF]
1799 [exit status: 2]
1800 ");
1801}
1802
1803#[test]
1804fn test_multiple_conflicts_with_error() {
1805 let mut test_env = TestEnvironment::default();
1806 let editor_script = test_env.set_up_fake_editor();
1807 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1808 let work_dir = test_env.work_dir("repo");
1809
1810 // Create two conflicted files, and one non-conflicted file
1811 create_commit_with_files(
1812 &work_dir,
1813 "base",
1814 &[],
1815 &[
1816 ("file1", "base1\n"),
1817 ("file2", "base2\n"),
1818 ("file3", "base3\n"),
1819 ],
1820 );
1821 create_commit_with_files(
1822 &work_dir,
1823 "a",
1824 &["base"],
1825 &[("file1", "a1\n"), ("file2", "a2\n")],
1826 );
1827 create_commit_with_files(
1828 &work_dir,
1829 "b",
1830 &["base"],
1831 &[("file1", "b1\n"), ("file2", "b2\n")],
1832 );
1833 create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
1834 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1835 file1 2-sided conflict
1836 file2 2-sided conflict
1837 [EOF]
1838 ");
1839 insta::assert_snapshot!(work_dir.read_file("file1"), @r"
1840 <<<<<<< Conflict 1 of 1
1841 %%%%%%% Changes from base to side #1
1842 -base1
1843 +a1
1844 +++++++ Contents of side #2
1845 b1
1846 >>>>>>> Conflict 1 of 1 ends
1847 "
1848 );
1849 insta::assert_snapshot!(work_dir.read_file("file2"), @r"
1850 <<<<<<< Conflict 1 of 1
1851 %%%%%%% Changes from base to side #1
1852 -base2
1853 +a2
1854 +++++++ Contents of side #2
1855 b2
1856 >>>>>>> Conflict 1 of 1 ends
1857 "
1858 );
1859
1860 // Test resolving one conflict, then exiting without resolving the second one
1861 std::fs::write(
1862 &editor_script,
1863 ["write\nresolution1\n", "next invocation\n"].join("\0"),
1864 )
1865 .unwrap();
1866 let output = work_dir.run_jj(["resolve"]);
1867 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r###"
1868 ------- stderr -------
1869 Resolving conflicts in: file1
1870 Resolving conflicts in: file2
1871 Working copy (@) now at: vruxwmqv 98296abe conflict | (conflict) conflict
1872 Parent commit (@-) : zsuskuln 6c31698c a | a
1873 Parent commit (@-) : royxmykx ba0a5538 b | b
1874 Added 0 files, modified 1 files, removed 0 files
1875 Warning: There are unresolved conflicts at these paths:
1876 file2 2-sided conflict
1877 New conflicts appeared in 1 commits:
1878 vruxwmqv 98296abe conflict | (conflict) conflict
1879 Hint: To resolve the conflicts, start by creating a commit on top of
1880 the conflicted commit:
1881 jj new vruxwmqv
1882 Then use `jj resolve`, or edit the conflict markers in the file directly.
1883 Once the conflicts are resolved, you can inspect the result with `jj diff`.
1884 Then run `jj squash` to move the resolution into the conflicted commit.
1885 Error: Stopped due to error after resolving 1 conflicts
1886 Caused by: The output file is either unchanged or empty after the editor quit (run with --debug to see the exact invocation).
1887 [EOF]
1888 [exit status: 1]
1889 "###);
1890 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1891 diff --git a/file1 b/file1
1892 index 0000000000..95cc18629d 100644
1893 --- a/file1
1894 +++ b/file1
1895 @@ -1,7 +1,1 @@
1896 -<<<<<<< Conflict 1 of 1
1897 -%%%%%%% Changes from base to side #1
1898 --base1
1899 -+a1
1900 -+++++++ Contents of side #2
1901 -b1
1902 ->>>>>>> Conflict 1 of 1 ends
1903 +resolution1
1904 [EOF]
1905 ");
1906 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1907 file2 2-sided conflict
1908 [EOF]
1909 ");
1910
1911 // Test resolving one conflict, then failing during the second resolution
1912 work_dir.run_jj(["undo"]).success();
1913 std::fs::write(
1914 &editor_script,
1915 ["write\nresolution1\n", "next invocation\n", "fail"].join("\0"),
1916 )
1917 .unwrap();
1918 let output = work_dir.run_jj(["resolve"]);
1919 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r###"
1920 ------- stderr -------
1921 Resolving conflicts in: file1
1922 Resolving conflicts in: file2
1923 Working copy (@) now at: vruxwmqv 7daa6406 conflict | (conflict) conflict
1924 Parent commit (@-) : zsuskuln 6c31698c a | a
1925 Parent commit (@-) : royxmykx ba0a5538 b | b
1926 Added 0 files, modified 1 files, removed 0 files
1927 Warning: There are unresolved conflicts at these paths:
1928 file2 2-sided conflict
1929 New conflicts appeared in 1 commits:
1930 vruxwmqv 7daa6406 conflict | (conflict) conflict
1931 Hint: To resolve the conflicts, start by creating a commit on top of
1932 the conflicted commit:
1933 jj new vruxwmqv
1934 Then use `jj resolve`, or edit the conflict markers in the file directly.
1935 Once the conflicts are resolved, you can inspect the result with `jj diff`.
1936 Then run `jj squash` to move the resolution into the conflicted commit.
1937 Error: Stopped due to error after resolving 1 conflicts
1938 Caused by: Tool exited with exit status: 1 (run with --debug to see the exact invocation)
1939 [EOF]
1940 [exit status: 1]
1941 "###);
1942 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r"
1943 diff --git a/file1 b/file1
1944 index 0000000000..95cc18629d 100644
1945 --- a/file1
1946 +++ b/file1
1947 @@ -1,7 +1,1 @@
1948 -<<<<<<< Conflict 1 of 1
1949 -%%%%%%% Changes from base to side #1
1950 --base1
1951 -+a1
1952 -+++++++ Contents of side #2
1953 -b1
1954 ->>>>>>> Conflict 1 of 1 ends
1955 +resolution1
1956 [EOF]
1957 ");
1958 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1959 file2 2-sided conflict
1960 [EOF]
1961 ");
1962
1963 // Test immediately failing to resolve any conflict
1964 work_dir.run_jj(["undo"]).success();
1965 std::fs::write(&editor_script, "fail").unwrap();
1966 let output = work_dir.run_jj(["resolve"]);
1967 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r"
1968 ------- stderr -------
1969 Resolving conflicts in: file1
1970 Error: Failed to resolve conflicts
1971 Caused by: Tool exited with exit status: 1 (run with --debug to see the exact invocation)
1972 [EOF]
1973 [exit status: 1]
1974 ");
1975 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
1976 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
1977 file1 2-sided conflict
1978 file2 2-sided conflict
1979 [EOF]
1980 ");
1981}
1982
1983#[test]
1984fn test_resolve_with_contents_of_side() {
1985 let test_env = TestEnvironment::default();
1986 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1987 let work_dir = test_env.work_dir("repo");
1988
1989 create_commit_with_files(
1990 &work_dir,
1991 "base",
1992 &[],
1993 &[("file", "base\n"), ("other", "base\n")],
1994 );
1995 create_commit_with_files(
1996 &work_dir,
1997 "a",
1998 &["base"],
1999 &[("file", "a\n"), ("other", "base\n")],
2000 );
2001 create_commit_with_files(
2002 &work_dir,
2003 "b",
2004 &["base"],
2005 &[("file", "base\n"), ("other", "left\n")],
2006 );
2007 create_commit_with_files(
2008 &work_dir,
2009 "c",
2010 &["base"],
2011 &[("file", "b\n"), ("other", "right\n")],
2012 );
2013 create_commit_with_files(&work_dir, "conflict", &["a", "b", "c"], &[]);
2014 // Test the setup
2015 insta::assert_snapshot!(get_log_output(&work_dir), @r"
2016 @ conflict
2017 ├─┬─╮
2018 │ │ ○ c
2019 │ ○ │ b
2020 │ ├─╯
2021 ○ │ a
2022 ├─╯
2023 ○ base
2024 ◆
2025 [EOF]
2026 ");
2027 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r"
2028 file 2-sided conflict
2029 other 2-sided conflict
2030 [EOF]
2031 ");
2032 insta::assert_snapshot!(work_dir.read_file("file"), @r"
2033 <<<<<<< Conflict 1 of 1
2034 %%%%%%% Changes from base to side #1
2035 -base
2036 +a
2037 +++++++ Contents of side #2
2038 b
2039 >>>>>>> Conflict 1 of 1 ends
2040 ");
2041 insta::assert_snapshot!(work_dir.read_file("other"), @r"
2042 <<<<<<< Conflict 1 of 1
2043 %%%%%%% Changes from base to side #1
2044 -base
2045 +left
2046 +++++++ Contents of side #2
2047 right
2048 >>>>>>> Conflict 1 of 1 ends
2049 ");
2050
2051 // Check that ":ours" merge tool works correctly
2052 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
2053 let output = work_dir.run_jj(["resolve", "--tool", ":ours"]);
2054 insta::assert_snapshot!(output, @r"
2055 ------- stderr -------
2056 Working copy (@) now at: znkkpsqq 5410a03a conflict | conflict
2057 Parent commit (@-) : zsuskuln 72dced6e a | a
2058 Parent commit (@-) : royxmykx e5747f42 b | b
2059 Parent commit (@-) : vruxwmqv dd35236a c | c
2060 Added 0 files, modified 2 files, removed 0 files
2061 [EOF]
2062 ");
2063 insta::assert_snapshot!(work_dir.read_file("file"), @"a");
2064 insta::assert_snapshot!(work_dir.read_file("other"), @"left");
2065 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r#"
2066 ------- stderr -------
2067 Error: No conflicts found at this revision
2068 [EOF]
2069 [exit status: 2]
2070 "#);
2071
2072 // Check that ":theirs" merge tool works correctly
2073 work_dir.run_jj(["undo"]).success();
2074 insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
2075 let output = work_dir.run_jj(["resolve", "--tool", ":theirs"]);
2076 insta::assert_snapshot!(output, @r"
2077 ------- stderr -------
2078 Working copy (@) now at: znkkpsqq c07b2e9e conflict | conflict
2079 Parent commit (@-) : zsuskuln 72dced6e a | a
2080 Parent commit (@-) : royxmykx e5747f42 b | b
2081 Parent commit (@-) : vruxwmqv dd35236a c | c
2082 Added 0 files, modified 2 files, removed 0 files
2083 [EOF]
2084 ");
2085 insta::assert_snapshot!(work_dir.read_file("file"), @"b");
2086 insta::assert_snapshot!(work_dir.read_file("other"), @"right");
2087 insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @r#"
2088 ------- stderr -------
2089 Error: No conflicts found at this revision
2090 [EOF]
2091 [exit status: 2]
2092 "#);
2093}