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 indoc::indoc;
16use itertools::Itertools as _;
17
18use crate::common::create_commit;
19use crate::common::fake_diff_editor_path;
20use crate::common::to_toml_value;
21use crate::common::CommandOutput;
22use crate::common::TestEnvironment;
23use crate::common::TestWorkDir;
24
25#[test]
26fn test_diff_basic() {
27 let test_env = TestEnvironment::default();
28 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
29 let work_dir = test_env.work_dir("repo");
30
31 work_dir.write_file("file1", "foo\n");
32 work_dir.write_file("file2", "1\n2\n3\n4\n");
33 work_dir.run_jj(["new"]).success();
34 work_dir.remove_file("file1");
35 work_dir.write_file("file2", "1\n5\n3\n");
36 work_dir.write_file("file3", "foo\n");
37 work_dir.write_file("file4", "1\n2\n3\n4\n");
38
39 let output = work_dir.run_jj(["diff"]);
40 insta::assert_snapshot!(output, @r"
41 Modified regular file file2:
42 1 1: 1
43 2 2: 25
44 3 3: 3
45 4 : 4
46 Modified regular file file3 (file1 => file3):
47 Modified regular file file4 (file2 => file4):
48 [EOF]
49 ");
50
51 let output = work_dir.run_jj(["diff", "--context=0"]);
52 insta::assert_snapshot!(output, @r"
53 Modified regular file file2:
54 1 1: 1
55 2 2: 25
56 3 3: 3
57 4 : 4
58 Modified regular file file3 (file1 => file3):
59 Modified regular file file4 (file2 => file4):
60 [EOF]
61 ");
62
63 let output = work_dir.run_jj(["diff", "--color=debug"]);
64 insta::assert_snapshot!(output, @r"
65 [38;5;3m<<diff header::Modified regular file file2:>>[39m
66 [38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: 1>>
67 [38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 2>>[39m<<diff::: >>[4m[38;5;1m<<diff removed token::2>>[38;5;2m<<diff added token::5>>[24m[39m<<diff::>>
68 [38;5;1m<<diff removed line_number:: 3>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 3>>[39m<<diff::: 3>>
69 [38;5;1m<<diff removed line_number:: 4>>[39m<<diff:: : >>[4m[38;5;1m<<diff removed token::4>>[24m[39m
70 [38;5;3m<<diff header::Modified regular file file3 (file1 => file3):>>[39m
71 [38;5;3m<<diff header::Modified regular file file4 (file2 => file4):>>[39m
72 [EOF]
73 ");
74
75 let output = work_dir.run_jj(["diff", "-s"]);
76 insta::assert_snapshot!(output, @r"
77 M file2
78 R {file1 => file3}
79 C {file2 => file4}
80 [EOF]
81 ");
82
83 let output = work_dir.run_jj(["diff", "--types"]);
84 insta::assert_snapshot!(output, @r"
85 FF file2
86 FF {file1 => file3}
87 FF {file2 => file4}
88 [EOF]
89 ");
90
91 let output = work_dir.run_jj(["diff", "--types", "glob:file[12]"]);
92 insta::assert_snapshot!(output, @r"
93 F- file1
94 FF file2
95 [EOF]
96 ");
97
98 let output = work_dir.run_jj(["diff", "--git", "file1"]);
99 insta::assert_snapshot!(output, @r"
100 diff --git a/file1 b/file1
101 deleted file mode 100644
102 index 257cc5642c..0000000000
103 --- a/file1
104 +++ /dev/null
105 @@ -1,1 +0,0 @@
106 -foo
107 [EOF]
108 ");
109
110 let output = work_dir.run_jj(["diff", "--git"]);
111 insta::assert_snapshot!(output, @r"
112 diff --git a/file2 b/file2
113 index 94ebaf9001..1ffc51b472 100644
114 --- a/file2
115 +++ b/file2
116 @@ -1,4 +1,3 @@
117 1
118 -2
119 +5
120 3
121 -4
122 diff --git a/file1 b/file3
123 rename from file1
124 rename to file3
125 diff --git a/file2 b/file4
126 copy from file2
127 copy to file4
128 [EOF]
129 ");
130
131 let output = work_dir.run_jj(["diff", "--git", "--context=0"]);
132 insta::assert_snapshot!(output, @r"
133 diff --git a/file2 b/file2
134 index 94ebaf9001..1ffc51b472 100644
135 --- a/file2
136 +++ b/file2
137 @@ -2,1 +2,1 @@
138 -2
139 +5
140 @@ -4,1 +3,0 @@
141 -4
142 diff --git a/file1 b/file3
143 rename from file1
144 rename to file3
145 diff --git a/file2 b/file4
146 copy from file2
147 copy to file4
148 [EOF]
149 ");
150
151 let output = work_dir.run_jj(["diff", "--git", "--color=debug"]);
152 insta::assert_snapshot!(output, @r"
153 [1m<<diff file_header::diff --git a/file2 b/file2>>[0m
154 [1m<<diff file_header::index 94ebaf9001..1ffc51b472 100644>>[0m
155 [1m<<diff file_header::--- a/file2>>[0m
156 [1m<<diff file_header::+++ b/file2>>[0m
157 [38;5;6m<<diff hunk_header::@@ -1,4 +1,3 @@>>[39m
158 <<diff context:: 1>>
159 [38;5;1m<<diff removed::->>[4m<<diff removed token::2>>[24m<<diff removed::>>[39m
160 [38;5;2m<<diff added::+>>[4m<<diff added token::5>>[24m<<diff added::>>[39m
161 <<diff context:: 3>>
162 [38;5;1m<<diff removed::->>[4m<<diff removed token::4>>[24m[39m
163 [1m<<diff file_header::diff --git a/file1 b/file3>>[0m
164 [1m<<diff file_header::rename from file1>>[0m
165 [1m<<diff file_header::rename to file3>>[0m
166 [1m<<diff file_header::diff --git a/file2 b/file4>>[0m
167 [1m<<diff file_header::copy from file2>>[0m
168 [1m<<diff file_header::copy to file4>>[0m
169 [EOF]
170 ");
171
172 let output = work_dir.run_jj(["diff", "-s", "--git"]);
173 insta::assert_snapshot!(output, @r"
174 M file2
175 R {file1 => file3}
176 C {file2 => file4}
177 diff --git a/file2 b/file2
178 index 94ebaf9001..1ffc51b472 100644
179 --- a/file2
180 +++ b/file2
181 @@ -1,4 +1,3 @@
182 1
183 -2
184 +5
185 3
186 -4
187 diff --git a/file1 b/file3
188 rename from file1
189 rename to file3
190 diff --git a/file2 b/file4
191 copy from file2
192 copy to file4
193 [EOF]
194 ");
195
196 let output = work_dir.run_jj(["diff", "--stat"]);
197 insta::assert_snapshot!(output, @r"
198 file2 | 3 +--
199 {file1 => file3} | 0
200 {file2 => file4} | 0
201 3 files changed, 1 insertion(+), 2 deletions(-)
202 [EOF]
203 ");
204
205 // Filter by glob pattern
206 let output = work_dir.run_jj(["diff", "-s", "glob:file[12]"]);
207 insta::assert_snapshot!(output, @r"
208 D file1
209 M file2
210 [EOF]
211 ");
212
213 // Unmatched paths should generate warnings
214 let output = test_env.run_jj_in(
215 ".",
216 [
217 "diff",
218 "-Rrepo",
219 "-s",
220 "repo", // matches directory
221 "repo/file1", // deleted in to_tree, but exists in from_tree
222 "repo/x",
223 "repo/y/z",
224 ],
225 );
226 insta::assert_snapshot!(output.normalize_backslash(), @r"
227 M repo/file2
228 R repo/{file1 => file3}
229 C repo/{file2 => file4}
230 [EOF]
231 ------- stderr -------
232 Warning: No matching entries for paths: repo/x, repo/y/z
233 [EOF]
234 ");
235
236 // Unmodified paths shouldn't generate warnings
237 let output = work_dir.run_jj(["diff", "-s", "--from=@", "file2"]);
238 insta::assert_snapshot!(output, @"");
239}
240
241#[test]
242fn test_diff_empty() {
243 let test_env = TestEnvironment::default();
244 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
245 let work_dir = test_env.work_dir("repo");
246
247 work_dir.write_file("file1", "");
248 let output = work_dir.run_jj(["diff"]);
249 insta::assert_snapshot!(output, @r"
250 Added regular file file1:
251 (empty)
252 [EOF]
253 ");
254
255 work_dir.run_jj(["new"]).success();
256 work_dir.remove_file("file1");
257 let output = work_dir.run_jj(["diff"]);
258 insta::assert_snapshot!(output, @r"
259 Removed regular file file1:
260 (empty)
261 [EOF]
262 ");
263
264 let output = work_dir.run_jj(["diff", "--stat"]);
265 insta::assert_snapshot!(output, @r"
266 file1 | 0
267 1 file changed, 0 insertions(+), 0 deletions(-)
268 [EOF]
269 ");
270}
271
272#[test]
273fn test_diff_file_mode() {
274 let test_env = TestEnvironment::default();
275 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
276 let work_dir = test_env.work_dir("repo");
277
278 // Test content+mode/mode-only changes of empty/non-empty files:
279 // - file1: ("", x) -> ("2", n) empty, content+mode
280 // - file2: ("1", x) -> ("1", n) non-empty, mode-only
281 // - file3: ("1", n) -> ("2", x) non-empty, content+mode
282 // - file4: ("", n) -> ("", x) empty, mode-only
283
284 work_dir.write_file("file1", "");
285 work_dir.write_file("file2", "1\n");
286 work_dir.write_file("file3", "1\n");
287 work_dir.write_file("file4", "");
288 work_dir
289 .run_jj(["file", "chmod", "x", "file1", "file2"])
290 .success();
291
292 work_dir.run_jj(["new"]).success();
293 work_dir.write_file("file1", "2\n");
294 work_dir.write_file("file3", "2\n");
295 work_dir
296 .run_jj(["file", "chmod", "n", "file1", "file2"])
297 .success();
298 work_dir
299 .run_jj(["file", "chmod", "x", "file3", "file4"])
300 .success();
301
302 work_dir.run_jj(["new"]).success();
303 work_dir.remove_file("file1");
304 work_dir.remove_file("file2");
305 work_dir.remove_file("file3");
306 work_dir.remove_file("file4");
307
308 let output = work_dir.run_jj(["diff", "-r@--"]);
309 insta::assert_snapshot!(output, @r"
310 Added executable file file1:
311 (empty)
312 Added executable file file2:
313 1: 1
314 Added regular file file3:
315 1: 1
316 Added regular file file4:
317 (empty)
318 [EOF]
319 ");
320 let output = work_dir.run_jj(["diff", "-r@-"]);
321 insta::assert_snapshot!(output, @r"
322 Executable file became non-executable at file1:
323 1: 2
324 Executable file became non-executable at file2:
325 Non-executable file became executable at file3:
326 1 1: 12
327 Non-executable file became executable at file4:
328 [EOF]
329 ");
330 let output = work_dir.run_jj(["diff", "-r@"]);
331 insta::assert_snapshot!(output, @r"
332 Removed regular file file1:
333 1 : 2
334 Removed regular file file2:
335 1 : 1
336 Removed executable file file3:
337 1 : 2
338 Removed executable file file4:
339 (empty)
340 [EOF]
341 ");
342
343 let output = work_dir.run_jj(["diff", "-r@--", "--git"]);
344 insta::assert_snapshot!(output, @r"
345 diff --git a/file1 b/file1
346 new file mode 100755
347 index 0000000000..e69de29bb2
348 diff --git a/file2 b/file2
349 new file mode 100755
350 index 0000000000..d00491fd7e
351 --- /dev/null
352 +++ b/file2
353 @@ -0,0 +1,1 @@
354 +1
355 diff --git a/file3 b/file3
356 new file mode 100644
357 index 0000000000..d00491fd7e
358 --- /dev/null
359 +++ b/file3
360 @@ -0,0 +1,1 @@
361 +1
362 diff --git a/file4 b/file4
363 new file mode 100644
364 index 0000000000..e69de29bb2
365 [EOF]
366 ");
367 let output = work_dir.run_jj(["diff", "-r@-", "--git"]);
368 insta::assert_snapshot!(output, @r"
369 diff --git a/file1 b/file1
370 old mode 100755
371 new mode 100644
372 index e69de29bb2..0cfbf08886
373 --- a/file1
374 +++ b/file1
375 @@ -0,0 +1,1 @@
376 +2
377 diff --git a/file2 b/file2
378 old mode 100755
379 new mode 100644
380 diff --git a/file3 b/file3
381 old mode 100644
382 new mode 100755
383 index d00491fd7e..0cfbf08886
384 --- a/file3
385 +++ b/file3
386 @@ -1,1 +1,1 @@
387 -1
388 +2
389 diff --git a/file4 b/file4
390 old mode 100644
391 new mode 100755
392 [EOF]
393 ");
394 let output = work_dir.run_jj(["diff", "-r@", "--git"]);
395 insta::assert_snapshot!(output, @r"
396 diff --git a/file1 b/file1
397 deleted file mode 100644
398 index 0cfbf08886..0000000000
399 --- a/file1
400 +++ /dev/null
401 @@ -1,1 +0,0 @@
402 -2
403 diff --git a/file2 b/file2
404 deleted file mode 100644
405 index d00491fd7e..0000000000
406 --- a/file2
407 +++ /dev/null
408 @@ -1,1 +0,0 @@
409 -1
410 diff --git a/file3 b/file3
411 deleted file mode 100755
412 index 0cfbf08886..0000000000
413 --- a/file3
414 +++ /dev/null
415 @@ -1,1 +0,0 @@
416 -2
417 diff --git a/file4 b/file4
418 deleted file mode 100755
419 index e69de29bb2..0000000000
420 [EOF]
421 ");
422}
423
424#[test]
425fn test_diff_types() {
426 let test_env = TestEnvironment::default();
427 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
428 let work_dir = test_env.work_dir("repo");
429
430 let file_path = "foo";
431
432 // Missing
433 work_dir.run_jj(["new", "root()", "-m=missing"]).success();
434
435 // Normal file
436 work_dir.run_jj(["new", "root()", "-m=file"]).success();
437 work_dir.write_file(file_path, "foo");
438
439 // Conflict (add/add)
440 work_dir.run_jj(["new", "root()", "-m=conflict"]).success();
441 work_dir.write_file(file_path, "foo");
442 work_dir.run_jj(["new", "root()"]).success();
443 work_dir.write_file(file_path, "bar");
444 work_dir
445 .run_jj(["squash", r#"--into=description("conflict")"#])
446 .success();
447
448 #[cfg(unix)]
449 {
450 use std::os::unix::fs::PermissionsExt as _;
451 use std::path::PathBuf;
452
453 // Executable
454 work_dir
455 .run_jj(["new", "root()", "-m=executable"])
456 .success();
457 work_dir.write_file(file_path, "foo");
458 std::fs::set_permissions(
459 work_dir.root().join(file_path),
460 std::fs::Permissions::from_mode(0o755),
461 )
462 .unwrap();
463
464 // Symlink
465 work_dir.run_jj(["new", "root()", "-m=symlink"]).success();
466 std::os::unix::fs::symlink(PathBuf::from("."), work_dir.root().join(file_path)).unwrap();
467 }
468
469 let diff = |from: &str, to: &str| {
470 work_dir.run_jj([
471 "diff",
472 "--types",
473 &format!(r#"--from=description("{from}")"#),
474 &format!(r#"--to=description("{to}")"#),
475 ])
476 };
477 insta::assert_snapshot!(diff("missing", "file"), @r"
478 -F foo
479 [EOF]
480 ");
481 insta::assert_snapshot!(diff("file", "conflict"), @r"
482 FC foo
483 [EOF]
484 ");
485 insta::assert_snapshot!(diff("conflict", "missing"), @r"
486 C- foo
487 [EOF]
488 ");
489
490 #[cfg(unix)]
491 {
492 insta::assert_snapshot!(diff("symlink", "file"), @r"
493 LF foo
494 [EOF]
495 ");
496 insta::assert_snapshot!(diff("missing", "executable"), @r"
497 -F foo
498 [EOF]
499 ");
500 }
501}
502
503#[test]
504fn test_diff_name_only() {
505 let test_env = TestEnvironment::default();
506 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
507 let work_dir = test_env.work_dir("repo");
508
509 work_dir.run_jj(["new"]).success();
510 work_dir.write_file("deleted", "d");
511 work_dir.write_file("modified", "m");
512 insta::assert_snapshot!(work_dir.run_jj(["diff", "--name-only"]), @r"
513 deleted
514 modified
515 [EOF]
516 ");
517 work_dir.run_jj(["commit", "-mfirst"]).success();
518 work_dir.remove_file("deleted");
519 work_dir.write_file("modified", "mod");
520 work_dir.write_file("added", "add");
521 work_dir.create_dir("sub");
522 work_dir.write_file("sub/added", "sub/add");
523 insta::assert_snapshot!(work_dir.run_jj(["diff", "--name-only"]).normalize_backslash(), @r"
524 added
525 deleted
526 modified
527 sub/added
528 [EOF]
529 ");
530}
531
532#[test]
533fn test_diff_bad_args() {
534 let test_env = TestEnvironment::default();
535 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
536 let work_dir = test_env.work_dir("repo");
537
538 let output = work_dir.run_jj(["diff", "-s", "--types"]);
539 insta::assert_snapshot!(output, @r"
540 ------- stderr -------
541 error: the argument '--summary' cannot be used with '--types'
542
543 Usage: jj diff --summary [FILESETS]...
544
545 For more information, try '--help'.
546 [EOF]
547 [exit status: 2]
548 ");
549
550 let output = work_dir.run_jj(["diff", "--color-words", "--git"]);
551 insta::assert_snapshot!(output, @r"
552 ------- stderr -------
553 error: the argument '--color-words' cannot be used with '--git'
554
555 Usage: jj diff --color-words [FILESETS]...
556
557 For more information, try '--help'.
558 [EOF]
559 [exit status: 2]
560 ");
561}
562
563#[test]
564fn test_diff_relative_paths() {
565 let test_env = TestEnvironment::default();
566 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
567 let work_dir = test_env.work_dir("repo");
568
569 work_dir.create_dir_all("dir1/subdir1");
570 work_dir.create_dir("dir2");
571 work_dir.write_file("file1", "foo1\n");
572 work_dir.write_file("dir1/file2", "foo2\n");
573 work_dir.write_file("dir1/subdir1/file3", "foo3\n");
574 work_dir.write_file("dir2/file4", "foo4\n");
575 work_dir.run_jj(["new"]).success();
576 work_dir.write_file("file1", "bar1\n");
577 work_dir.write_file("dir1/file2", "bar2\n");
578 work_dir.write_file("dir1/subdir1/file3", "bar3\n");
579 work_dir.write_file("dir2/file4", "bar4\n");
580
581 let sub_dir1 = work_dir.dir("dir1");
582 let output = sub_dir1.run_jj(["diff"]);
583 #[cfg(unix)]
584 insta::assert_snapshot!(output, @r"
585 Modified regular file file2:
586 1 1: foo2bar2
587 Modified regular file subdir1/file3:
588 1 1: foo3bar3
589 Modified regular file ../dir2/file4:
590 1 1: foo4bar4
591 Modified regular file ../file1:
592 1 1: foo1bar1
593 [EOF]
594 ");
595 #[cfg(windows)]
596 insta::assert_snapshot!(output, @r"
597 Modified regular file file2:
598 1 1: foo2bar2
599 Modified regular file subdir1\file3:
600 1 1: foo3bar3
601 Modified regular file ..\dir2\file4:
602 1 1: foo4bar4
603 Modified regular file ..\file1:
604 1 1: foo1bar1
605 [EOF]
606 ");
607
608 let output = sub_dir1.run_jj(["diff", "-s"]);
609 #[cfg(unix)]
610 insta::assert_snapshot!(output, @r"
611 M file2
612 M subdir1/file3
613 M ../dir2/file4
614 M ../file1
615 [EOF]
616 ");
617 #[cfg(windows)]
618 insta::assert_snapshot!(output, @r"
619 M file2
620 M subdir1\file3
621 M ..\dir2\file4
622 M ..\file1
623 [EOF]
624 ");
625
626 let output = sub_dir1.run_jj(["diff", "--types"]);
627 #[cfg(unix)]
628 insta::assert_snapshot!(output, @r"
629 FF file2
630 FF subdir1/file3
631 FF ../dir2/file4
632 FF ../file1
633 [EOF]
634 ");
635 #[cfg(windows)]
636 insta::assert_snapshot!(output, @r"
637 FF file2
638 FF subdir1\file3
639 FF ..\dir2\file4
640 FF ..\file1
641 [EOF]
642 ");
643
644 let output = sub_dir1.run_jj(["diff", "--git"]);
645 insta::assert_snapshot!(output, @r"
646 diff --git a/dir1/file2 b/dir1/file2
647 index 54b060eee9..1fe912cdd8 100644
648 --- a/dir1/file2
649 +++ b/dir1/file2
650 @@ -1,1 +1,1 @@
651 -foo2
652 +bar2
653 diff --git a/dir1/subdir1/file3 b/dir1/subdir1/file3
654 index c1ec6c6f12..f3c8b75ec6 100644
655 --- a/dir1/subdir1/file3
656 +++ b/dir1/subdir1/file3
657 @@ -1,1 +1,1 @@
658 -foo3
659 +bar3
660 diff --git a/dir2/file4 b/dir2/file4
661 index a0016dbc4c..17375f7a12 100644
662 --- a/dir2/file4
663 +++ b/dir2/file4
664 @@ -1,1 +1,1 @@
665 -foo4
666 +bar4
667 diff --git a/file1 b/file1
668 index 1715acd6a5..05c4fe6772 100644
669 --- a/file1
670 +++ b/file1
671 @@ -1,1 +1,1 @@
672 -foo1
673 +bar1
674 [EOF]
675 ");
676
677 let output = sub_dir1.run_jj(["diff", "--stat"]);
678 #[cfg(unix)]
679 insta::assert_snapshot!(output, @r"
680 file2 | 2 +-
681 subdir1/file3 | 2 +-
682 ../dir2/file4 | 2 +-
683 ../file1 | 2 +-
684 4 files changed, 4 insertions(+), 4 deletions(-)
685 [EOF]
686 ");
687 #[cfg(windows)]
688 insta::assert_snapshot!(output, @r"
689 file2 | 2 +-
690 subdir1\file3 | 2 +-
691 ..\dir2\file4 | 2 +-
692 ..\file1 | 2 +-
693 4 files changed, 4 insertions(+), 4 deletions(-)
694 [EOF]
695 ");
696}
697
698#[test]
699fn test_diff_hunks() {
700 let test_env = TestEnvironment::default();
701 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
702 let work_dir = test_env.work_dir("repo");
703
704 // Test added, removed, inserted, and modified lines. The modified line
705 // contains unchanged words.
706 work_dir.write_file("file1", "");
707 work_dir.write_file("file2", "foo\n");
708 work_dir.write_file("file3", "foo\nbaz qux blah blah\n");
709 work_dir.run_jj(["new"]).success();
710 work_dir.write_file("file1", "foo\n");
711 work_dir.write_file("file2", "");
712 work_dir.write_file("file3", "foo\nbar\nbaz quux blah blah\n");
713
714 let output = work_dir.run_jj(["diff"]);
715 insta::assert_snapshot!(output, @r"
716 Modified regular file file1:
717 1: foo
718 Modified regular file file2:
719 1 : foo
720 Modified regular file file3:
721 1 1: foo
722 2: bar
723 2 3: baz quxquux blah blah
724 [EOF]
725 ");
726
727 let output = work_dir.run_jj(["diff", "--color=debug"]);
728 insta::assert_snapshot!(output, @r"
729 [38;5;3m<<diff header::Modified regular file file1:>>[39m
730 <<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::foo>>[24m[39m
731 [38;5;3m<<diff header::Modified regular file file2:>>[39m
732 [38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: : >>[4m[38;5;1m<<diff removed token::foo>>[24m[39m
733 [38;5;3m<<diff header::Modified regular file file3:>>[39m
734 [38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: foo>>
735 <<diff:: >>[38;5;2m<<diff added line_number:: 2>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::bar>>[24m[39m
736 [38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 3>>[39m<<diff::: baz >>[4m[38;5;1m<<diff removed token::qux>>[38;5;2m<<diff added token::quux>>[24m[39m<<diff:: blah blah>>
737 [EOF]
738 ");
739
740 let output = work_dir.run_jj(["diff", "--git"]);
741 insta::assert_snapshot!(output, @r"
742 diff --git a/file1 b/file1
743 index e69de29bb2..257cc5642c 100644
744 --- a/file1
745 +++ b/file1
746 @@ -0,0 +1,1 @@
747 +foo
748 diff --git a/file2 b/file2
749 index 257cc5642c..e69de29bb2 100644
750 --- a/file2
751 +++ b/file2
752 @@ -1,1 +0,0 @@
753 -foo
754 diff --git a/file3 b/file3
755 index 221a95a095..a543ef3892 100644
756 --- a/file3
757 +++ b/file3
758 @@ -1,2 +1,3 @@
759 foo
760 -baz qux blah blah
761 +bar
762 +baz quux blah blah
763 [EOF]
764 ");
765
766 let output = work_dir.run_jj(["diff", "--git", "--color=debug"]);
767 insta::assert_snapshot!(output, @r"
768 [1m<<diff file_header::diff --git a/file1 b/file1>>[0m
769 [1m<<diff file_header::index e69de29bb2..257cc5642c 100644>>[0m
770 [1m<<diff file_header::--- a/file1>>[0m
771 [1m<<diff file_header::+++ b/file1>>[0m
772 [38;5;6m<<diff hunk_header::@@ -0,0 +1,1 @@>>[39m
773 [38;5;2m<<diff added::+>>[4m<<diff added token::foo>>[24m[39m
774 [1m<<diff file_header::diff --git a/file2 b/file2>>[0m
775 [1m<<diff file_header::index 257cc5642c..e69de29bb2 100644>>[0m
776 [1m<<diff file_header::--- a/file2>>[0m
777 [1m<<diff file_header::+++ b/file2>>[0m
778 [38;5;6m<<diff hunk_header::@@ -1,1 +0,0 @@>>[39m
779 [38;5;1m<<diff removed::->>[4m<<diff removed token::foo>>[24m[39m
780 [1m<<diff file_header::diff --git a/file3 b/file3>>[0m
781 [1m<<diff file_header::index 221a95a095..a543ef3892 100644>>[0m
782 [1m<<diff file_header::--- a/file3>>[0m
783 [1m<<diff file_header::+++ b/file3>>[0m
784 [38;5;6m<<diff hunk_header::@@ -1,2 +1,3 @@>>[39m
785 <<diff context:: foo>>
786 [38;5;1m<<diff removed::-baz >>[4m<<diff removed token::qux>>[24m<<diff removed:: blah blah>>[39m
787 [38;5;2m<<diff added::+>>[4m<<diff added token::bar>>[24m[39m
788 [38;5;2m<<diff added::+baz >>[4m<<diff added token::quux>>[24m<<diff added:: blah blah>>[39m
789 [EOF]
790 ");
791}
792
793#[test]
794fn test_diff_color_words_inlining_threshold() {
795 let test_env = TestEnvironment::default();
796 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
797 let work_dir = test_env.work_dir("repo");
798
799 let render_diff = |max_alternation: i32, args: &[&str]| {
800 let config = format!("diff.color-words.max-inline-alternation={max_alternation}");
801 work_dir.run_jj_with(|cmd| cmd.args(["diff", "--config", &config]).args(args))
802 };
803
804 let file1_path = "file1-single-line";
805 let file2_path = "file2-multiple-lines-in-single-hunk";
806 let file3_path = "file3-changes-across-lines";
807 work_dir.write_file(
808 file1_path,
809 indoc! {"
810 == adds ==
811 a b c
812 == removes ==
813 a b c d e f g
814 == adds + removes ==
815 a b c d e
816 == adds + removes + adds ==
817 a b c d e
818 == adds + removes + adds + removes ==
819 a b c d e f g
820 "},
821 );
822 work_dir.write_file(
823 file2_path,
824 indoc! {"
825 == adds; removes; adds + removes ==
826 a b c
827 a b c d e f g
828 a b c d e
829 == adds + removes + adds; adds + removes + adds + removes ==
830 a b c d e
831 a b c d e f g
832 "},
833 );
834 work_dir.write_file(
835 file3_path,
836 indoc! {"
837 == adds ==
838 a b c
839 == removes ==
840 a b c d
841 e f g
842 == adds + removes ==
843 a b c
844 d e
845 == adds + removes + adds ==
846 a b c
847 d e
848 == adds + removes + adds + removes ==
849 a b
850 c d e f g
851 "},
852 );
853 work_dir.run_jj(["new"]).success();
854 work_dir.write_file(
855 file1_path,
856 indoc! {"
857 == adds ==
858 a X b Y Z c
859 == removes ==
860 a c f
861 == adds + removes ==
862 a X b d
863 == adds + removes + adds ==
864 a X b d Y
865 == adds + removes + adds + removes ==
866 X a Y b d Z e
867 "},
868 );
869 work_dir.write_file(
870 file2_path,
871 indoc! {"
872 == adds; removes; adds + removes ==
873 a X b Y Z c
874 a c f
875 a X b d
876 == adds + removes + adds; adds + removes + adds + removes ==
877 a X b d Y
878 X a Y b d Z e
879 "},
880 );
881 work_dir.write_file(
882 file3_path,
883 indoc! {"
884 == adds ==
885 a X b
886 Y Z c
887 == removes ==
888 a c f
889 == adds + removes ==
890 a
891 X b d
892 == adds + removes + adds ==
893 a X b d
894 Y
895 == adds + removes + adds + removes ==
896 X a Y b d
897 Z e
898 "},
899 );
900
901 // default
902 let output = work_dir.run_jj(["diff"]);
903 insta::assert_snapshot!(output, @r"
904 Modified regular file file1-single-line:
905 1 1: == adds ==
906 2 2: a X b Y Z c
907 3 3: == removes ==
908 4 4: a b c d e f g
909 5 5: == adds + removes ==
910 6 6: a X b c d e
911 7 7: == adds + removes + adds ==
912 8 8: a X b c d eY
913 9 9: == adds + removes + adds + removes ==
914 10 : a b c d e f g
915 10: X a Y b d Z e
916 Modified regular file file2-multiple-lines-in-single-hunk:
917 1 1: == adds; removes; adds + removes ==
918 2 2: a X b Y Z c
919 3 3: a b c d e f g
920 4 4: a X b c d e
921 5 5: == adds + removes + adds; adds + removes + adds + removes ==
922 6 : a b c d e
923 7 : a b c d e f g
924 6: a X b d Y
925 7: X a Y b d Z e
926 Modified regular file file3-changes-across-lines:
927 1 1: == adds ==
928 2 2: a X b
929 2 3: Y Z c
930 3 4: == removes ==
931 4 5: a b c d
932 5 5: e f g
933 6 6: == adds + removes ==
934 7 7: a
935 7 8: X b c
936 8 8: d e
937 9 9: == adds + removes + adds ==
938 10 10: a X b c
939 11 10: d e
940 11 11: Y
941 12 12: == adds + removes + adds + removes ==
942 13 : a b
943 14 : c d e f g
944 13: X a Y b d
945 14: Z e
946 [EOF]
947 ");
948
949 // -1: inline all
950 insta::assert_snapshot!(render_diff(-1, &[]), @r"
951 Modified regular file file1-single-line:
952 1 1: == adds ==
953 2 2: a X b Y Z c
954 3 3: == removes ==
955 4 4: a b c d e f g
956 5 5: == adds + removes ==
957 6 6: a X b c d e
958 7 7: == adds + removes + adds ==
959 8 8: a X b c d eY
960 9 9: == adds + removes + adds + removes ==
961 10 10: X a Y b c d Z e f g
962 Modified regular file file2-multiple-lines-in-single-hunk:
963 1 1: == adds; removes; adds + removes ==
964 2 2: a X b Y Z c
965 3 3: a b c d e f g
966 4 4: a X b c d e
967 5 5: == adds + removes + adds; adds + removes + adds + removes ==
968 6 6: a X b c d eY
969 7 7: X a Y b c d Z e f g
970 Modified regular file file3-changes-across-lines:
971 1 1: == adds ==
972 2 2: a X b
973 2 3: Y Z c
974 3 4: == removes ==
975 4 5: a b c d
976 5 5: e f g
977 6 6: == adds + removes ==
978 7 7: a
979 7 8: X b c
980 8 8: d e
981 9 9: == adds + removes + adds ==
982 10 10: a X b c
983 11 10: d e
984 11 11: Y
985 12 12: == adds + removes + adds + removes ==
986 13 13: X a Y b
987 14 13: c d
988 14 14: Z e f g
989 [EOF]
990 ");
991
992 // 0: no inlining
993 insta::assert_snapshot!(render_diff(0, &[]), @r"
994 Modified regular file file1-single-line:
995 1 1: == adds ==
996 2 : a b c
997 2: a X b Y Z c
998 3 3: == removes ==
999 4 : a b c d e f g
1000 4: a c f
1001 5 5: == adds + removes ==
1002 6 : a b c d e
1003 6: a X b d
1004 7 7: == adds + removes + adds ==
1005 8 : a b c d e
1006 8: a X b d Y
1007 9 9: == adds + removes + adds + removes ==
1008 10 : a b c d e f g
1009 10: X a Y b d Z e
1010 Modified regular file file2-multiple-lines-in-single-hunk:
1011 1 1: == adds; removes; adds + removes ==
1012 2 : a b c
1013 3 : a b c d e f g
1014 4 : a b c d e
1015 2: a X b Y Z c
1016 3: a c f
1017 4: a X b d
1018 5 5: == adds + removes + adds; adds + removes + adds + removes ==
1019 6 : a b c d e
1020 7 : a b c d e f g
1021 6: a X b d Y
1022 7: X a Y b d Z e
1023 Modified regular file file3-changes-across-lines:
1024 1 1: == adds ==
1025 2 : a b c
1026 2: a X b
1027 3: Y Z c
1028 3 4: == removes ==
1029 4 : a b c d
1030 5 : e f g
1031 5: a c f
1032 6 6: == adds + removes ==
1033 7 : a b c
1034 8 : d e
1035 7: a
1036 8: X b d
1037 9 9: == adds + removes + adds ==
1038 10 : a b c
1039 11 : d e
1040 10: a X b d
1041 11: Y
1042 12 12: == adds + removes + adds + removes ==
1043 13 : a b
1044 14 : c d e f g
1045 13: X a Y b d
1046 14: Z e
1047 [EOF]
1048 ");
1049
1050 // 1: inline adds-only or removes-only lines
1051 insta::assert_snapshot!(render_diff(1, &[]), @r"
1052 Modified regular file file1-single-line:
1053 1 1: == adds ==
1054 2 2: a X b Y Z c
1055 3 3: == removes ==
1056 4 4: a b c d e f g
1057 5 5: == adds + removes ==
1058 6 : a b c d e
1059 6: a X b d
1060 7 7: == adds + removes + adds ==
1061 8 : a b c d e
1062 8: a X b d Y
1063 9 9: == adds + removes + adds + removes ==
1064 10 : a b c d e f g
1065 10: X a Y b d Z e
1066 Modified regular file file2-multiple-lines-in-single-hunk:
1067 1 1: == adds; removes; adds + removes ==
1068 2 : a b c
1069 3 : a b c d e f g
1070 4 : a b c d e
1071 2: a X b Y Z c
1072 3: a c f
1073 4: a X b d
1074 5 5: == adds + removes + adds; adds + removes + adds + removes ==
1075 6 : a b c d e
1076 7 : a b c d e f g
1077 6: a X b d Y
1078 7: X a Y b d Z e
1079 Modified regular file file3-changes-across-lines:
1080 1 1: == adds ==
1081 2 2: a X b
1082 2 3: Y Z c
1083 3 4: == removes ==
1084 4 5: a b c d
1085 5 5: e f g
1086 6 6: == adds + removes ==
1087 7 : a b c
1088 8 : d e
1089 7: a
1090 8: X b d
1091 9 9: == adds + removes + adds ==
1092 10 : a b c
1093 11 : d e
1094 10: a X b d
1095 11: Y
1096 12 12: == adds + removes + adds + removes ==
1097 13 : a b
1098 14 : c d e f g
1099 13: X a Y b d
1100 14: Z e
1101 [EOF]
1102 ");
1103
1104 // 2: inline up to adds + removes lines
1105 insta::assert_snapshot!(render_diff(2, &[]), @r"
1106 Modified regular file file1-single-line:
1107 1 1: == adds ==
1108 2 2: a X b Y Z c
1109 3 3: == removes ==
1110 4 4: a b c d e f g
1111 5 5: == adds + removes ==
1112 6 6: a X b c d e
1113 7 7: == adds + removes + adds ==
1114 8 : a b c d e
1115 8: a X b d Y
1116 9 9: == adds + removes + adds + removes ==
1117 10 : a b c d e f g
1118 10: X a Y b d Z e
1119 Modified regular file file2-multiple-lines-in-single-hunk:
1120 1 1: == adds; removes; adds + removes ==
1121 2 2: a X b Y Z c
1122 3 3: a b c d e f g
1123 4 4: a X b c d e
1124 5 5: == adds + removes + adds; adds + removes + adds + removes ==
1125 6 : a b c d e
1126 7 : a b c d e f g
1127 6: a X b d Y
1128 7: X a Y b d Z e
1129 Modified regular file file3-changes-across-lines:
1130 1 1: == adds ==
1131 2 2: a X b
1132 2 3: Y Z c
1133 3 4: == removes ==
1134 4 5: a b c d
1135 5 5: e f g
1136 6 6: == adds + removes ==
1137 7 7: a
1138 7 8: X b c
1139 8 8: d e
1140 9 9: == adds + removes + adds ==
1141 10 : a b c
1142 11 : d e
1143 10: a X b d
1144 11: Y
1145 12 12: == adds + removes + adds + removes ==
1146 13 : a b
1147 14 : c d e f g
1148 13: X a Y b d
1149 14: Z e
1150 [EOF]
1151 ");
1152
1153 // 3: inline up to adds + removes + adds lines
1154 insta::assert_snapshot!(render_diff(3, &[]), @r"
1155 Modified regular file file1-single-line:
1156 1 1: == adds ==
1157 2 2: a X b Y Z c
1158 3 3: == removes ==
1159 4 4: a b c d e f g
1160 5 5: == adds + removes ==
1161 6 6: a X b c d e
1162 7 7: == adds + removes + adds ==
1163 8 8: a X b c d eY
1164 9 9: == adds + removes + adds + removes ==
1165 10 : a b c d e f g
1166 10: X a Y b d Z e
1167 Modified regular file file2-multiple-lines-in-single-hunk:
1168 1 1: == adds; removes; adds + removes ==
1169 2 2: a X b Y Z c
1170 3 3: a b c d e f g
1171 4 4: a X b c d e
1172 5 5: == adds + removes + adds; adds + removes + adds + removes ==
1173 6 : a b c d e
1174 7 : a b c d e f g
1175 6: a X b d Y
1176 7: X a Y b d Z e
1177 Modified regular file file3-changes-across-lines:
1178 1 1: == adds ==
1179 2 2: a X b
1180 2 3: Y Z c
1181 3 4: == removes ==
1182 4 5: a b c d
1183 5 5: e f g
1184 6 6: == adds + removes ==
1185 7 7: a
1186 7 8: X b c
1187 8 8: d e
1188 9 9: == adds + removes + adds ==
1189 10 10: a X b c
1190 11 10: d e
1191 11 11: Y
1192 12 12: == adds + removes + adds + removes ==
1193 13 : a b
1194 14 : c d e f g
1195 13: X a Y b d
1196 14: Z e
1197 [EOF]
1198 ");
1199
1200 // 4: inline up to adds + removes + adds + removes lines
1201 insta::assert_snapshot!(render_diff(4, &[]), @r"
1202 Modified regular file file1-single-line:
1203 1 1: == adds ==
1204 2 2: a X b Y Z c
1205 3 3: == removes ==
1206 4 4: a b c d e f g
1207 5 5: == adds + removes ==
1208 6 6: a X b c d e
1209 7 7: == adds + removes + adds ==
1210 8 8: a X b c d eY
1211 9 9: == adds + removes + adds + removes ==
1212 10 10: X a Y b c d Z e f g
1213 Modified regular file file2-multiple-lines-in-single-hunk:
1214 1 1: == adds; removes; adds + removes ==
1215 2 2: a X b Y Z c
1216 3 3: a b c d e f g
1217 4 4: a X b c d e
1218 5 5: == adds + removes + adds; adds + removes + adds + removes ==
1219 6 6: a X b c d eY
1220 7 7: X a Y b c d Z e f g
1221 Modified regular file file3-changes-across-lines:
1222 1 1: == adds ==
1223 2 2: a X b
1224 2 3: Y Z c
1225 3 4: == removes ==
1226 4 5: a b c d
1227 5 5: e f g
1228 6 6: == adds + removes ==
1229 7 7: a
1230 7 8: X b c
1231 8 8: d e
1232 9 9: == adds + removes + adds ==
1233 10 10: a X b c
1234 11 10: d e
1235 11 11: Y
1236 12 12: == adds + removes + adds + removes ==
1237 13 13: X a Y b
1238 14 13: c d
1239 14 14: Z e f g
1240 [EOF]
1241 ");
1242
1243 // context words in added/removed lines should be labeled as such
1244 insta::assert_snapshot!(render_diff(2, &["--color=always"]), @r"
1245 [38;5;3mModified regular file file1-single-line:[39m
1246 [38;5;1m 1[39m [38;5;2m 1[39m: == adds ==
1247 [38;5;1m 2[39m [38;5;2m 2[39m: a [4m[38;5;2mX [24m[39mb [4m[38;5;2mY Z [24m[39mc
1248 [38;5;1m 3[39m [38;5;2m 3[39m: == removes ==
1249 [38;5;1m 4[39m [38;5;2m 4[39m: a [4m[38;5;1mb [24m[39mc [4m[38;5;1md e [24m[39mf[4m[38;5;1m g[24m[39m
1250 [38;5;1m 5[39m [38;5;2m 5[39m: == adds + removes ==
1251 [38;5;1m 6[39m [38;5;2m 6[39m: a [4m[38;5;2mX [24m[39mb [4m[38;5;1mc [24m[39md[4m[38;5;1m e[24m[39m
1252 [38;5;1m 7[39m [38;5;2m 7[39m: == adds + removes + adds ==
1253 [38;5;1m 8[39m : [38;5;1ma b [4mc [24md [4me[24m[39m
1254 [38;5;2m 8[39m: [38;5;2ma [4mX [24mb d [4mY[24m[39m
1255 [38;5;1m 9[39m [38;5;2m 9[39m: == adds + removes + adds + removes ==
1256 [38;5;1m 10[39m : [38;5;1ma b [4mc [24md e[4m f g[24m[39m
1257 [38;5;2m 10[39m: [4m[38;5;2mX [24ma [4mY [24mb d [4mZ [24me[39m
1258 [38;5;3mModified regular file file2-multiple-lines-in-single-hunk:[39m
1259 [38;5;1m 1[39m [38;5;2m 1[39m: == adds; removes; adds + removes ==
1260 [38;5;1m 2[39m [38;5;2m 2[39m: a [4m[38;5;2mX [24m[39mb [4m[38;5;2mY Z [24m[39mc
1261 [38;5;1m 3[39m [38;5;2m 3[39m: a [4m[38;5;1mb [24m[39mc [4m[38;5;1md e [24m[39mf[4m[38;5;1m g[24m[39m
1262 [38;5;1m 4[39m [38;5;2m 4[39m: a [4m[38;5;2mX [24m[39mb [4m[38;5;1mc [24m[39md[4m[38;5;1m e[24m[39m
1263 [38;5;1m 5[39m [38;5;2m 5[39m: == adds + removes + adds; adds + removes + adds + removes ==
1264 [38;5;1m 6[39m : [38;5;1ma b [4mc [24md [4me[24m[39m
1265 [38;5;1m 7[39m : [38;5;1ma b [4mc [24md e[4m f g[24m[39m
1266 [38;5;2m 6[39m: [38;5;2ma [4mX [24mb d [4mY[24m[39m
1267 [38;5;2m 7[39m: [4m[38;5;2mX [24ma [4mY [24mb d [4mZ [24me[39m
1268 [38;5;3mModified regular file file3-changes-across-lines:[39m
1269 [38;5;1m 1[39m [38;5;2m 1[39m: == adds ==
1270 [38;5;1m 2[39m [38;5;2m 2[39m: a [4m[38;5;2mX [24m[39mb[4m[38;5;2m[24m[39m
1271 [38;5;1m 2[39m [38;5;2m 3[39m: [4m[38;5;2mY Z[24m[39m c
1272 [38;5;1m 3[39m [38;5;2m 4[39m: == removes ==
1273 [38;5;1m 4[39m [38;5;2m 5[39m: a [4m[38;5;1mb [24m[39mc [4m[38;5;1md[24m[39m
1274 [38;5;1m 5[39m [38;5;2m 5[39m: [4m[38;5;1me [24m[39mf[4m[38;5;1m g[24m[39m
1275 [38;5;1m 6[39m [38;5;2m 6[39m: == adds + removes ==
1276 [38;5;1m 7[39m [38;5;2m 7[39m: a[4m[38;5;2m[24m[39m
1277 [38;5;1m 7[39m [38;5;2m 8[39m: [4m[38;5;2mX[24m[39m b [4m[38;5;1mc[24m[39m
1278 [38;5;1m 8[39m [38;5;2m 8[39m: d[4m[38;5;1m e[24m[39m
1279 [38;5;1m 9[39m [38;5;2m 9[39m: == adds + removes + adds ==
1280 [38;5;1m 10[39m : [38;5;1ma b [4mc[24m[39m
1281 [38;5;1m 11[39m : [38;5;1md[4m e[24m[39m
1282 [38;5;2m 10[39m: [38;5;2ma [4mX [24mb d[4m[24m[39m
1283 [38;5;2m 11[39m: [4m[38;5;2mY[24m[39m
1284 [38;5;1m 12[39m [38;5;2m 12[39m: == adds + removes + adds + removes ==
1285 [38;5;1m 13[39m : [38;5;1ma b[4m[24m[39m
1286 [38;5;1m 14[39m : [4m[38;5;1mc[24m d e[4m f g[24m[39m
1287 [38;5;2m 13[39m: [4m[38;5;2mX [24ma [4mY [24mb d[4m[24m[39m
1288 [38;5;2m 14[39m: [4m[38;5;2mZ[24m e[39m
1289 [EOF]
1290 ");
1291 insta::assert_snapshot!(render_diff(2, &["--color=debug"]), @r"
1292 [38;5;3m<<diff header::Modified regular file file1-single-line:>>[39m
1293 [38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: == adds ==>>
1294 [38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 2>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b >>[4m[38;5;2m<<diff added token::Y Z >>[24m[39m<<diff::c>>
1295 [38;5;1m<<diff removed line_number:: 3>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 3>>[39m<<diff::: == removes ==>>
1296 [38;5;1m<<diff removed line_number:: 4>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 4>>[39m<<diff::: a >>[4m[38;5;1m<<diff removed token::b >>[24m[39m<<diff::c >>[4m[38;5;1m<<diff removed token::d e >>[24m[39m<<diff::f>>[4m[38;5;1m<<diff removed token:: g>>[24m[39m<<diff::>>
1297 [38;5;1m<<diff removed line_number:: 5>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 5>>[39m<<diff::: == adds + removes ==>>
1298 [38;5;1m<<diff removed line_number:: 6>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 6>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b >>[4m[38;5;1m<<diff removed token::c >>[24m[39m<<diff::d>>[4m[38;5;1m<<diff removed token:: e>>[24m[39m<<diff::>>
1299 [38;5;1m<<diff removed line_number:: 7>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 7>>[39m<<diff::: == adds + removes + adds ==>>
1300 [38;5;1m<<diff removed line_number:: 8>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c >>[24m<<diff removed::d >>[4m<<diff removed token::e>>[24m<<diff removed::>>[39m
1301 <<diff:: >>[38;5;2m<<diff added line_number:: 8>>[39m<<diff::: >>[38;5;2m<<diff added::a >>[4m<<diff added token::X >>[24m<<diff added::b d >>[4m<<diff added token::Y>>[24m<<diff added::>>[39m
1302 [38;5;1m<<diff removed line_number:: 9>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 9>>[39m<<diff::: == adds + removes + adds + removes ==>>
1303 [38;5;1m<<diff removed line_number:: 10>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c >>[24m<<diff removed::d e>>[4m<<diff removed token:: f g>>[24m<<diff removed::>>[39m
1304 <<diff:: >>[38;5;2m<<diff added line_number:: 10>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::X >>[24m<<diff added::a >>[4m<<diff added token::Y >>[24m<<diff added::b d >>[4m<<diff added token::Z >>[24m<<diff added::e>>[39m
1305 [38;5;3m<<diff header::Modified regular file file2-multiple-lines-in-single-hunk:>>[39m
1306 [38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: == adds; removes; adds + removes ==>>
1307 [38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 2>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b >>[4m[38;5;2m<<diff added token::Y Z >>[24m[39m<<diff::c>>
1308 [38;5;1m<<diff removed line_number:: 3>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 3>>[39m<<diff::: a >>[4m[38;5;1m<<diff removed token::b >>[24m[39m<<diff::c >>[4m[38;5;1m<<diff removed token::d e >>[24m[39m<<diff::f>>[4m[38;5;1m<<diff removed token:: g>>[24m[39m<<diff::>>
1309 [38;5;1m<<diff removed line_number:: 4>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 4>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b >>[4m[38;5;1m<<diff removed token::c >>[24m[39m<<diff::d>>[4m[38;5;1m<<diff removed token:: e>>[24m[39m<<diff::>>
1310 [38;5;1m<<diff removed line_number:: 5>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 5>>[39m<<diff::: == adds + removes + adds; adds + removes + adds + removes ==>>
1311 [38;5;1m<<diff removed line_number:: 6>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c >>[24m<<diff removed::d >>[4m<<diff removed token::e>>[24m<<diff removed::>>[39m
1312 [38;5;1m<<diff removed line_number:: 7>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c >>[24m<<diff removed::d e>>[4m<<diff removed token:: f g>>[24m<<diff removed::>>[39m
1313 <<diff:: >>[38;5;2m<<diff added line_number:: 6>>[39m<<diff::: >>[38;5;2m<<diff added::a >>[4m<<diff added token::X >>[24m<<diff added::b d >>[4m<<diff added token::Y>>[24m<<diff added::>>[39m
1314 <<diff:: >>[38;5;2m<<diff added line_number:: 7>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::X >>[24m<<diff added::a >>[4m<<diff added token::Y >>[24m<<diff added::b d >>[4m<<diff added token::Z >>[24m<<diff added::e>>[39m
1315 [38;5;3m<<diff header::Modified regular file file3-changes-across-lines:>>[39m
1316 [38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: == adds ==>>
1317 [38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 2>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b>>[4m[38;5;2m<<diff added token::>>[24m[39m
1318 [38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 3>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::Y Z>>[24m[39m<<diff:: c>>
1319 [38;5;1m<<diff removed line_number:: 3>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 4>>[39m<<diff::: == removes ==>>
1320 [38;5;1m<<diff removed line_number:: 4>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 5>>[39m<<diff::: a >>[4m[38;5;1m<<diff removed token::b >>[24m[39m<<diff::c >>[4m[38;5;1m<<diff removed token::d>>[24m[39m
1321 [38;5;1m<<diff removed line_number:: 5>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 5>>[39m<<diff::: >>[4m[38;5;1m<<diff removed token::e >>[24m[39m<<diff::f>>[4m[38;5;1m<<diff removed token:: g>>[24m[39m<<diff::>>
1322 [38;5;1m<<diff removed line_number:: 6>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 6>>[39m<<diff::: == adds + removes ==>>
1323 [38;5;1m<<diff removed line_number:: 7>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 7>>[39m<<diff::: a>>[4m[38;5;2m<<diff added token::>>[24m[39m
1324 [38;5;1m<<diff removed line_number:: 7>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 8>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::X>>[24m[39m<<diff:: b >>[4m[38;5;1m<<diff removed token::c>>[24m[39m
1325 [38;5;1m<<diff removed line_number:: 8>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 8>>[39m<<diff::: d>>[4m[38;5;1m<<diff removed token:: e>>[24m[39m<<diff::>>
1326 [38;5;1m<<diff removed line_number:: 9>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 9>>[39m<<diff::: == adds + removes + adds ==>>
1327 [38;5;1m<<diff removed line_number:: 10>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c>>[24m[39m
1328 [38;5;1m<<diff removed line_number:: 11>>[39m<<diff:: : >>[38;5;1m<<diff removed::d>>[4m<<diff removed token:: e>>[24m<<diff removed::>>[39m
1329 <<diff:: >>[38;5;2m<<diff added line_number:: 10>>[39m<<diff::: >>[38;5;2m<<diff added::a >>[4m<<diff added token::X >>[24m<<diff added::b d>>[4m<<diff added token::>>[24m[39m
1330 <<diff:: >>[38;5;2m<<diff added line_number:: 11>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::Y>>[24m<<diff added::>>[39m
1331 [38;5;1m<<diff removed line_number:: 12>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 12>>[39m<<diff::: == adds + removes + adds + removes ==>>
1332 [38;5;1m<<diff removed line_number:: 13>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b>>[4m<<diff removed token::>>[24m[39m
1333 [38;5;1m<<diff removed line_number:: 14>>[39m<<diff:: : >>[4m[38;5;1m<<diff removed token::c>>[24m<<diff removed:: d e>>[4m<<diff removed token:: f g>>[24m<<diff removed::>>[39m
1334 <<diff:: >>[38;5;2m<<diff added line_number:: 13>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::X >>[24m<<diff added::a >>[4m<<diff added token::Y >>[24m<<diff added::b d>>[4m<<diff added token::>>[24m[39m
1335 <<diff:: >>[38;5;2m<<diff added line_number:: 14>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::Z>>[24m<<diff added:: e>>[39m
1336 [EOF]
1337 ");
1338}
1339
1340#[test]
1341fn test_diff_missing_newline() {
1342 let test_env = TestEnvironment::default();
1343 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1344 let work_dir = test_env.work_dir("repo");
1345
1346 work_dir.write_file("file1", "foo");
1347 work_dir.write_file("file2", "foo\nbar");
1348 work_dir.run_jj(["new"]).success();
1349 work_dir.write_file("file1", "foo\nbar");
1350 work_dir.write_file("file2", "foo");
1351
1352 let output = work_dir.run_jj(["diff"]);
1353 insta::assert_snapshot!(output, @r"
1354 Modified regular file file1:
1355 1 1: foo
1356 2: bar
1357 Modified regular file file2:
1358 1 1: foo
1359 2 : bar
1360 [EOF]
1361 ");
1362
1363 let output = work_dir.run_jj(["diff", "--git"]);
1364 insta::assert_snapshot!(output, @r"
1365 diff --git a/file1 b/file1
1366 index 1910281566..a907ec3f43 100644
1367 --- a/file1
1368 +++ b/file1
1369 @@ -1,1 +1,2 @@
1370 -foo
1371 \ No newline at end of file
1372 +foo
1373 +bar
1374 \ No newline at end of file
1375 diff --git a/file2 b/file2
1376 index a907ec3f43..1910281566 100644
1377 --- a/file2
1378 +++ b/file2
1379 @@ -1,2 +1,1 @@
1380 -foo
1381 -bar
1382 \ No newline at end of file
1383 +foo
1384 \ No newline at end of file
1385 [EOF]
1386 ");
1387
1388 let output = work_dir.run_jj(["diff", "--stat"]);
1389 insta::assert_snapshot!(output, @r"
1390 file1 | 3 ++-
1391 file2 | 3 +--
1392 2 files changed, 3 insertions(+), 3 deletions(-)
1393 [EOF]
1394 ");
1395}
1396
1397#[test]
1398fn test_color_words_diff_missing_newline() {
1399 let test_env = TestEnvironment::default();
1400 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1401 let work_dir = test_env.work_dir("repo");
1402
1403 work_dir.write_file("file1", "");
1404 work_dir.run_jj(["commit", "-m", "=== Empty"]).success();
1405 work_dir.write_file("file1", "a\nb\nc\nd\ne\nf\ng\nh\ni");
1406 work_dir
1407 .run_jj(["commit", "-m", "=== Add no newline"])
1408 .success();
1409 work_dir.write_file("file1", "A\nb\nc\nd\ne\nf\ng\nh\ni");
1410 work_dir
1411 .run_jj(["commit", "-m", "=== Modify first line"])
1412 .success();
1413 work_dir.write_file("file1", "A\nb\nc\nd\nE\nf\ng\nh\ni");
1414 work_dir
1415 .run_jj(["commit", "-m", "=== Modify middle line"])
1416 .success();
1417 work_dir.write_file("file1", "A\nb\nc\nd\nE\nf\ng\nh\nI");
1418 work_dir
1419 .run_jj(["commit", "-m", "=== Modify last line"])
1420 .success();
1421 work_dir.write_file("file1", "A\nb\nc\nd\nE\nf\ng\nh\nI\n");
1422 work_dir
1423 .run_jj(["commit", "-m", "=== Append newline"])
1424 .success();
1425 work_dir.write_file("file1", "A\nb\nc\nd\nE\nf\ng\nh\nI");
1426 work_dir
1427 .run_jj(["commit", "-m", "=== Remove newline"])
1428 .success();
1429 work_dir.write_file("file1", "");
1430 work_dir.run_jj(["commit", "-m", "=== Empty"]).success();
1431
1432 let output = work_dir.run_jj([
1433 "log",
1434 "-Tdescription",
1435 "-pr::@-",
1436 "--no-graph",
1437 "--reversed",
1438 ]);
1439 insta::assert_snapshot!(output, @r"
1440 === Empty
1441 Added regular file file1:
1442 (empty)
1443 === Add no newline
1444 Modified regular file file1:
1445 1: a
1446 2: b
1447 3: c
1448 4: d
1449 5: e
1450 6: f
1451 7: g
1452 8: h
1453 9: i
1454 === Modify first line
1455 Modified regular file file1:
1456 1 1: aA
1457 2 2: b
1458 3 3: c
1459 4 4: d
1460 ...
1461 === Modify middle line
1462 Modified regular file file1:
1463 1 1: A
1464 2 2: b
1465 3 3: c
1466 4 4: d
1467 5 5: eE
1468 6 6: f
1469 7 7: g
1470 8 8: h
1471 9 9: i
1472 === Modify last line
1473 Modified regular file file1:
1474 ...
1475 6 6: f
1476 7 7: g
1477 8 8: h
1478 9 9: iI
1479 === Append newline
1480 Modified regular file file1:
1481 ...
1482 6 6: f
1483 7 7: g
1484 8 8: h
1485 9 9: I
1486 === Remove newline
1487 Modified regular file file1:
1488 ...
1489 6 6: f
1490 7 7: g
1491 8 8: h
1492 9 9: I
1493 === Empty
1494 Modified regular file file1:
1495 1 : A
1496 2 : b
1497 3 : c
1498 4 : d
1499 5 : E
1500 6 : f
1501 7 : g
1502 8 : h
1503 9 : I
1504 [EOF]
1505 ");
1506
1507 let output = work_dir.run_jj([
1508 "log",
1509 "--config=diff.color-words.max-inline-alternation=0",
1510 "-Tdescription",
1511 "-pr::@-",
1512 "--no-graph",
1513 "--reversed",
1514 ]);
1515 insta::assert_snapshot!(output, @r"
1516 === Empty
1517 Added regular file file1:
1518 (empty)
1519 === Add no newline
1520 Modified regular file file1:
1521 1: a
1522 2: b
1523 3: c
1524 4: d
1525 5: e
1526 6: f
1527 7: g
1528 8: h
1529 9: i
1530 === Modify first line
1531 Modified regular file file1:
1532 1 : a
1533 1: A
1534 2 2: b
1535 3 3: c
1536 4 4: d
1537 ...
1538 === Modify middle line
1539 Modified regular file file1:
1540 1 1: A
1541 2 2: b
1542 3 3: c
1543 4 4: d
1544 5 : e
1545 5: E
1546 6 6: f
1547 7 7: g
1548 8 8: h
1549 9 9: i
1550 === Modify last line
1551 Modified regular file file1:
1552 ...
1553 6 6: f
1554 7 7: g
1555 8 8: h
1556 9 : i
1557 9: I
1558 === Append newline
1559 Modified regular file file1:
1560 ...
1561 6 6: f
1562 7 7: g
1563 8 8: h
1564 9 : I
1565 9: I
1566 === Remove newline
1567 Modified regular file file1:
1568 ...
1569 6 6: f
1570 7 7: g
1571 8 8: h
1572 9 : I
1573 9: I
1574 === Empty
1575 Modified regular file file1:
1576 1 : A
1577 2 : b
1578 3 : c
1579 4 : d
1580 5 : E
1581 6 : f
1582 7 : g
1583 8 : h
1584 9 : I
1585 [EOF]
1586 ");
1587}
1588
1589#[test]
1590fn test_diff_ignore_whitespace() {
1591 let test_env = TestEnvironment::default();
1592 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1593 let work_dir = test_env.work_dir("repo");
1594
1595 work_dir.write_file(
1596 "file1",
1597 indoc! {"
1598 foo {
1599 bar;
1600 }
1601 baz {}
1602 "},
1603 );
1604 work_dir
1605 .run_jj(["new", "-mindent + whitespace insertion"])
1606 .success();
1607 work_dir.write_file(
1608 "file1",
1609 indoc! {"
1610 {
1611 foo {
1612 bar;
1613 }
1614 }
1615 baz { }
1616 "},
1617 );
1618 work_dir.run_jj(["status"]).success();
1619
1620 // Git diff as reference output
1621 let output = work_dir.run_jj(["diff", "--git", "--ignore-all-space"]);
1622 insta::assert_snapshot!(output, @r"
1623 diff --git a/file1 b/file1
1624 index f532aa68ad..033c4a6168 100644
1625 --- a/file1
1626 +++ b/file1
1627 @@ -1,4 +1,6 @@
1628 +{
1629 foo {
1630 bar;
1631 }
1632 +}
1633 baz { }
1634 [EOF]
1635 ");
1636 let output = work_dir.run_jj(["diff", "--git", "--ignore-space-change"]);
1637 insta::assert_snapshot!(output, @r"
1638 diff --git a/file1 b/file1
1639 index f532aa68ad..033c4a6168 100644
1640 --- a/file1
1641 +++ b/file1
1642 @@ -1,4 +1,6 @@
1643 -foo {
1644 +{
1645 + foo {
1646 bar;
1647 + }
1648 }
1649 -baz {}
1650 +baz { }
1651 [EOF]
1652 ");
1653
1654 // Diff-stat should respects the whitespace options
1655 let output = work_dir.run_jj(["diff", "--stat", "--ignore-all-space"]);
1656 insta::assert_snapshot!(output, @r"
1657 file1 | 2 ++
1658 1 file changed, 2 insertions(+), 0 deletions(-)
1659 [EOF]
1660 ");
1661 let output = work_dir.run_jj(["diff", "--stat", "--ignore-space-change"]);
1662 insta::assert_snapshot!(output, @r"
1663 file1 | 6 ++++--
1664 1 file changed, 4 insertions(+), 2 deletions(-)
1665 [EOF]
1666 ");
1667
1668 // Word-level changes are still highlighted
1669 let output = work_dir.run_jj(["diff", "--color=always", "--ignore-all-space"]);
1670 insta::assert_snapshot!(output, @r"
1671 [38;5;3mModified regular file file1:[39m
1672 [38;5;2m 1[39m: [4m[38;5;2m{[24m[39m
1673 [38;5;1m 1[39m [38;5;2m 2[39m: [4m[38;5;2m [24m[39mfoo {
1674 [38;5;1m 2[39m [38;5;2m 3[39m: [4m[38;5;2m [24m[39mbar;
1675 [38;5;1m 3[39m [38;5;2m 4[39m: [4m[38;5;2m [24m[39m}
1676 [38;5;2m 5[39m: [4m[38;5;2m}[24m[39m
1677 [38;5;1m 4[39m [38;5;2m 6[39m: baz {[4m[38;5;2m [24m[39m}
1678 [EOF]
1679 ");
1680 let output = work_dir.run_jj(["diff", "--color=always", "--ignore-space-change"]);
1681 insta::assert_snapshot!(output, @r"
1682 [38;5;3mModified regular file file1:[39m
1683 [38;5;2m 1[39m: [4m[38;5;2m{[24m[39m
1684 [38;5;1m 1[39m [38;5;2m 2[39m: [4m[38;5;2m [24m[39mfoo {
1685 [38;5;1m 2[39m [38;5;2m 3[39m: [4m[38;5;2m [24m[39mbar;
1686 [38;5;2m 4[39m: [4m[38;5;2m }[24m[39m
1687 [38;5;1m 3[39m [38;5;2m 5[39m: }
1688 [38;5;1m 4[39m [38;5;2m 6[39m: baz {[4m[38;5;2m [24m[39m}
1689 [EOF]
1690 ");
1691}
1692
1693#[test]
1694fn test_diff_skipped_context() {
1695 let test_env = TestEnvironment::default();
1696 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1697 let work_dir = test_env.work_dir("repo");
1698
1699 work_dir.write_file("file1", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj");
1700 work_dir
1701 .run_jj(["describe", "-m", "=== Left side of diffs"])
1702 .success();
1703
1704 work_dir
1705 .run_jj(["new", "@", "-m", "=== Must skip 2 lines"])
1706 .success();
1707 work_dir.write_file("file1", "A\nb\nc\nd\ne\nf\ng\nh\ni\nJ");
1708 work_dir
1709 .run_jj(["new", "@-", "-m", "=== Don't skip 1 line"])
1710 .success();
1711 work_dir.write_file("file1", "A\nb\nc\nd\ne\nf\ng\nh\nI\nj");
1712 work_dir
1713 .run_jj(["new", "@-", "-m", "=== No gap to skip"])
1714 .success();
1715 work_dir.write_file("file1", "a\nB\nc\nd\ne\nf\ng\nh\nI\nj");
1716 work_dir
1717 .run_jj(["new", "@-", "-m", "=== No gap to skip"])
1718 .success();
1719 work_dir.write_file("file1", "a\nb\nC\nd\ne\nf\ng\nh\nI\nj");
1720 work_dir
1721 .run_jj(["new", "@-", "-m", "=== 1 line at start"])
1722 .success();
1723 work_dir.write_file("file1", "a\nb\nc\nd\nE\nf\ng\nh\ni\nj");
1724 work_dir
1725 .run_jj(["new", "@-", "-m", "=== 1 line at end"])
1726 .success();
1727 work_dir.write_file("file1", "a\nb\nc\nd\ne\nF\ng\nh\ni\nj");
1728
1729 let output = work_dir.run_jj(["log", "-Tdescription", "-p", "--no-graph", "--reversed"]);
1730 insta::assert_snapshot!(output, @r"
1731 === Left side of diffs
1732 Added regular file file1:
1733 1: a
1734 2: b
1735 3: c
1736 4: d
1737 5: e
1738 6: f
1739 7: g
1740 8: h
1741 9: i
1742 10: j
1743 === Must skip 2 lines
1744 Modified regular file file1:
1745 1 1: aA
1746 2 2: b
1747 3 3: c
1748 4 4: d
1749 ...
1750 7 7: g
1751 8 8: h
1752 9 9: i
1753 10 10: jJ
1754 === Don't skip 1 line
1755 Modified regular file file1:
1756 1 1: aA
1757 2 2: b
1758 3 3: c
1759 4 4: d
1760 5 5: e
1761 6 6: f
1762 7 7: g
1763 8 8: h
1764 9 9: iI
1765 10 10: j
1766 === No gap to skip
1767 Modified regular file file1:
1768 1 1: a
1769 2 2: bB
1770 3 3: c
1771 4 4: d
1772 5 5: e
1773 6 6: f
1774 7 7: g
1775 8 8: h
1776 9 9: iI
1777 10 10: j
1778 === No gap to skip
1779 Modified regular file file1:
1780 1 1: a
1781 2 2: b
1782 3 3: cC
1783 4 4: d
1784 5 5: e
1785 6 6: f
1786 7 7: g
1787 8 8: h
1788 9 9: iI
1789 10 10: j
1790 === 1 line at start
1791 Modified regular file file1:
1792 1 1: a
1793 2 2: b
1794 3 3: c
1795 4 4: d
1796 5 5: eE
1797 6 6: f
1798 7 7: g
1799 8 8: h
1800 ...
1801 === 1 line at end
1802 Modified regular file file1:
1803 ...
1804 3 3: c
1805 4 4: d
1806 5 5: e
1807 6 6: fF
1808 7 7: g
1809 8 8: h
1810 9 9: i
1811 10 10: j
1812 [EOF]
1813 ");
1814}
1815
1816#[test]
1817fn test_diff_skipped_context_from_settings_color_words() {
1818 let test_env = TestEnvironment::default();
1819 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1820 let work_dir = test_env.work_dir("repo");
1821
1822 test_env.add_config(
1823 r#"
1824[diff.color-words]
1825context = 0
1826 "#,
1827 );
1828
1829 work_dir.write_file("file1", "a\nb\nc\nd\ne");
1830 work_dir
1831 .run_jj(["describe", "-m", "=== First commit"])
1832 .success();
1833
1834 work_dir
1835 .run_jj(["new", "@", "-m", "=== Must show 0 context"])
1836 .success();
1837 work_dir.write_file("file1", "a\nb\nC\nd\ne");
1838
1839 let output = work_dir.run_jj(["log", "-Tdescription", "-p", "--no-graph", "--reversed"]);
1840 insta::assert_snapshot!(output, @r"
1841 === First commit
1842 Added regular file file1:
1843 1: a
1844 2: b
1845 3: c
1846 4: d
1847 5: e
1848 === Must show 0 context
1849 Modified regular file file1:
1850 ...
1851 3 3: cC
1852 ...
1853 [EOF]
1854 ");
1855}
1856
1857#[test]
1858fn test_diff_skipped_context_from_settings_git() {
1859 let test_env = TestEnvironment::default();
1860 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1861 let work_dir = test_env.work_dir("repo");
1862
1863 test_env.add_config(
1864 r#"
1865[diff.git]
1866context = 0
1867 "#,
1868 );
1869
1870 work_dir.write_file("file1", "a\nb\nc\nd\ne");
1871 work_dir
1872 .run_jj(["describe", "-m", "=== First commit"])
1873 .success();
1874
1875 work_dir
1876 .run_jj(["new", "@", "-m", "=== Must show 0 context"])
1877 .success();
1878 work_dir.write_file("file1", "a\nb\nC\nd\ne");
1879
1880 let output = work_dir.run_jj([
1881 "log",
1882 "-Tdescription",
1883 "-p",
1884 "--git",
1885 "--no-graph",
1886 "--reversed",
1887 ]);
1888 insta::assert_snapshot!(output, @r"
1889 === First commit
1890 diff --git a/file1 b/file1
1891 new file mode 100644
1892 index 0000000000..0fec236860
1893 --- /dev/null
1894 +++ b/file1
1895 @@ -0,0 +1,5 @@
1896 +a
1897 +b
1898 +c
1899 +d
1900 +e
1901 \ No newline at end of file
1902 === Must show 0 context
1903 diff --git a/file1 b/file1
1904 index 0fec236860..b7615dae52 100644
1905 --- a/file1
1906 +++ b/file1
1907 @@ -3,1 +3,1 @@
1908 -c
1909 +C
1910 [EOF]
1911 ");
1912}
1913
1914#[test]
1915fn test_diff_skipped_context_nondefault() {
1916 let test_env = TestEnvironment::default();
1917 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1918 let work_dir = test_env.work_dir("repo");
1919
1920 work_dir.write_file("file1", "a\nb\nc\nd");
1921 work_dir
1922 .run_jj(["describe", "-m", "=== Left side of diffs"])
1923 .success();
1924
1925 work_dir
1926 .run_jj(["new", "@", "-m", "=== Must skip 2 lines"])
1927 .success();
1928 work_dir.write_file("file1", "A\nb\nc\nD");
1929 work_dir
1930 .run_jj(["new", "@-", "-m", "=== Don't skip 1 line"])
1931 .success();
1932 work_dir.write_file("file1", "A\nb\nC\nd");
1933 work_dir
1934 .run_jj(["new", "@-", "-m", "=== No gap to skip"])
1935 .success();
1936 work_dir.write_file("file1", "a\nB\nC\nd");
1937 work_dir
1938 .run_jj(["new", "@-", "-m", "=== 1 line at start"])
1939 .success();
1940 work_dir.write_file("file1", "a\nB\nc\nd");
1941 work_dir
1942 .run_jj(["new", "@-", "-m", "=== 1 line at end"])
1943 .success();
1944 work_dir.write_file("file1", "a\nb\nC\nd");
1945
1946 let output = work_dir.run_jj([
1947 "log",
1948 "-Tdescription",
1949 "-p",
1950 "--no-graph",
1951 "--reversed",
1952 "--context=0",
1953 ]);
1954 insta::assert_snapshot!(output, @r"
1955 === Left side of diffs
1956 Added regular file file1:
1957 1: a
1958 2: b
1959 3: c
1960 4: d
1961 === Must skip 2 lines
1962 Modified regular file file1:
1963 1 1: aA
1964 ...
1965 4 4: dD
1966 === Don't skip 1 line
1967 Modified regular file file1:
1968 1 1: aA
1969 2 2: b
1970 3 3: cC
1971 4 4: d
1972 === No gap to skip
1973 Modified regular file file1:
1974 1 1: a
1975 2 2: bB
1976 3 3: cC
1977 4 4: d
1978 === 1 line at start
1979 Modified regular file file1:
1980 1 1: a
1981 2 2: bB
1982 ...
1983 === 1 line at end
1984 Modified regular file file1:
1985 ...
1986 3 3: cC
1987 4 4: d
1988 [EOF]
1989 ");
1990}
1991
1992#[test]
1993fn test_diff_leading_trailing_context() {
1994 let test_env = TestEnvironment::default();
1995 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1996 let work_dir = test_env.work_dir("repo");
1997
1998 // N=5 context lines at start/end of the file
1999 work_dir.write_file("file1", "1\n2\n3\n4\n5\nL\n6\n7\n8\n9\n10\n11\n");
2000 work_dir.run_jj(["new"]).success();
2001 work_dir.write_file("file1", "1\n2\n3\n4\n5\n6\nR\n7\n8\n9\n10\n11\n");
2002
2003 // N=5 <= num_context_lines + 1: No room to skip.
2004 let output = work_dir.run_jj(["diff", "--context=4"]);
2005 insta::assert_snapshot!(output, @r"
2006 Modified regular file file1:
2007 1 1: 1
2008 2 2: 2
2009 3 3: 3
2010 4 4: 4
2011 5 5: 5
2012 6 : L
2013 7 6: 6
2014 7: R
2015 8 8: 7
2016 9 9: 8
2017 10 10: 9
2018 11 11: 10
2019 12 12: 11
2020 [EOF]
2021 ");
2022
2023 // N=5 <= 2 * num_context_lines + 1: The last hunk wouldn't be split if
2024 // trailing diff existed.
2025 let output = work_dir.run_jj(["diff", "--context=3"]);
2026 insta::assert_snapshot!(output, @r"
2027 Modified regular file file1:
2028 ...
2029 3 3: 3
2030 4 4: 4
2031 5 5: 5
2032 6 : L
2033 7 6: 6
2034 7: R
2035 8 8: 7
2036 9 9: 8
2037 10 10: 9
2038 ...
2039 [EOF]
2040 ");
2041
2042 // N=5 > 2 * num_context_lines + 1: The last hunk should be split no matter
2043 // if trailing diff existed.
2044 let output = work_dir.run_jj(["diff", "--context=1"]);
2045 insta::assert_snapshot!(output, @r"
2046 Modified regular file file1:
2047 ...
2048 5 5: 5
2049 6 : L
2050 7 6: 6
2051 7: R
2052 8 8: 7
2053 ...
2054 [EOF]
2055 ");
2056
2057 // N=5 <= num_context_lines: No room to skip.
2058 let output = work_dir.run_jj(["diff", "--git", "--context=5"]);
2059 insta::assert_snapshot!(output, @r"
2060 diff --git a/file1 b/file1
2061 index 1bf57dee4a..69b3e1865c 100644
2062 --- a/file1
2063 +++ b/file1
2064 @@ -1,12 +1,12 @@
2065 1
2066 2
2067 3
2068 4
2069 5
2070 -L
2071 6
2072 +R
2073 7
2074 8
2075 9
2076 10
2077 11
2078 [EOF]
2079 ");
2080
2081 // N=5 <= 2 * num_context_lines: The last hunk wouldn't be split if
2082 // trailing diff existed.
2083 let output = work_dir.run_jj(["diff", "--git", "--context=3"]);
2084 insta::assert_snapshot!(output, @r"
2085 diff --git a/file1 b/file1
2086 index 1bf57dee4a..69b3e1865c 100644
2087 --- a/file1
2088 +++ b/file1
2089 @@ -3,8 +3,8 @@
2090 3
2091 4
2092 5
2093 -L
2094 6
2095 +R
2096 7
2097 8
2098 9
2099 [EOF]
2100 ");
2101
2102 // N=5 > 2 * num_context_lines: The last hunk should be split no matter
2103 // if trailing diff existed.
2104 let output = work_dir.run_jj(["diff", "--git", "--context=2"]);
2105 insta::assert_snapshot!(output, @r"
2106 diff --git a/file1 b/file1
2107 index 1bf57dee4a..69b3e1865c 100644
2108 --- a/file1
2109 +++ b/file1
2110 @@ -4,6 +4,6 @@
2111 4
2112 5
2113 -L
2114 6
2115 +R
2116 7
2117 8
2118 [EOF]
2119 ");
2120}
2121
2122#[test]
2123fn test_diff_external_tool() {
2124 let mut test_env = TestEnvironment::default();
2125 let edit_script = test_env.set_up_fake_diff_editor();
2126 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2127 let work_dir = test_env.work_dir("repo");
2128
2129 work_dir.write_file("file1", "foo\n");
2130 work_dir.write_file("file2", "foo\n");
2131 work_dir.run_jj(["new"]).success();
2132 work_dir.remove_file("file1");
2133 work_dir.write_file("file2", "foo\nbar\n");
2134 work_dir.write_file("file3", "foo\n");
2135
2136 // nonzero exit codes should print a warning
2137 std::fs::write(&edit_script, "fail").unwrap();
2138 let output = work_dir.run_jj(["diff", "--config=ui.diff.tool=fake-diff-editor"]);
2139 let mut insta_settings = insta::Settings::clone_current();
2140 insta_settings.add_filter("exit (status|code)", "<exit status>");
2141 insta_settings.bind(|| {
2142 insta::assert_snapshot!(output, @r"
2143 ------- stderr -------
2144 Warning: Tool exited with <exit status>: 1 (run with --debug to see the exact invocation)
2145 [EOF]
2146 ");
2147 });
2148
2149 // nonzero exit codes should not print a warning if it's an expected exit code
2150 std::fs::write(&edit_script, "fail").unwrap();
2151 let output = work_dir.run_jj([
2152 "diff",
2153 "--tool",
2154 "fake-diff-editor",
2155 "--config=merge-tools.fake-diff-editor.diff-expected-exit-codes=[1]",
2156 ]);
2157 insta::assert_snapshot!(output, @"");
2158
2159 std::fs::write(
2160 &edit_script,
2161 "print-files-before\0print --\0print-files-after",
2162 )
2163 .unwrap();
2164
2165 // diff without file patterns
2166 insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-diff-editor"]), @r"
2167 file1
2168 file2
2169 --
2170 file2
2171 file3
2172 [EOF]
2173 ");
2174
2175 // diff with file patterns
2176 insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-diff-editor", "file1"]), @r"
2177 file1
2178 --
2179 [EOF]
2180 ");
2181
2182 insta::assert_snapshot!(work_dir.run_jj(["log", "-p", "--tool=fake-diff-editor"]), @r"
2183 @ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 39d9055d
2184 │ (no description set)
2185 │ file1
2186 │ file2
2187 │ --
2188 │ file2
2189 │ file3
2190 ○ qpvuntsm test.user@example.com 2001-02-03 08:05:08 0ad4ef22
2191 │ (no description set)
2192 │ --
2193 │ file1
2194 │ file2
2195 ◆ zzzzzzzz root() 00000000
2196 --
2197 [EOF]
2198 ");
2199
2200 insta::assert_snapshot!(work_dir.run_jj(["show", "--tool=fake-diff-editor"]), @r"
2201 Commit ID: 39d9055d70873099fd924b9af218289d5663eac8
2202 Change ID: rlvkpnrzqnoowoytxnquwvuryrwnrmlp
2203 Author : Test User <test.user@example.com> (2001-02-03 08:05:09)
2204 Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
2205
2206 (no description set)
2207
2208 file1
2209 file2
2210 --
2211 file2
2212 file3
2213 [EOF]
2214 ");
2215
2216 // Enabled by default, looks up the merge-tools table
2217 let config = "--config=ui.diff.tool=fake-diff-editor";
2218 insta::assert_snapshot!(work_dir.run_jj(["diff", config]), @r"
2219 file1
2220 file2
2221 --
2222 file2
2223 file3
2224 [EOF]
2225 ");
2226
2227 // Inlined command arguments
2228 let command_toml = to_toml_value(fake_diff_editor_path());
2229 let config = format!("--config=ui.diff.tool=[{command_toml}, '$right', '$left']");
2230 insta::assert_snapshot!(work_dir.run_jj(["diff", &config]), @r"
2231 file2
2232 file3
2233 --
2234 file1
2235 file2
2236 [EOF]
2237 ");
2238
2239 // Output of external diff tool shouldn't be escaped
2240 std::fs::write(&edit_script, "print \x1b[1;31mred").unwrap();
2241 insta::assert_snapshot!(work_dir.run_jj(["diff", "--color=always", "--tool=fake-diff-editor"]),
2242 @r"
2243 [1;31mred
2244 [EOF]
2245 ");
2246
2247 // Non-zero exit code isn't an error
2248 std::fs::write(&edit_script, "print diff\0fail").unwrap();
2249 let output = work_dir.run_jj(["show", "--tool=fake-diff-editor"]);
2250 insta::assert_snapshot!(output.normalize_stderr_exit_status(), @r"
2251 Commit ID: 39d9055d70873099fd924b9af218289d5663eac8
2252 Change ID: rlvkpnrzqnoowoytxnquwvuryrwnrmlp
2253 Author : Test User <test.user@example.com> (2001-02-03 08:05:09)
2254 Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
2255
2256 (no description set)
2257
2258 diff
2259 [EOF]
2260 ------- stderr -------
2261 Warning: Tool exited with exit status: 1 (run with --debug to see the exact invocation)
2262 [EOF]
2263 ");
2264
2265 // --tool=:builtin shouldn't be ignored
2266 let output = work_dir.run_jj(["diff", "--tool=:builtin"]);
2267 insta::assert_snapshot!(output.strip_stderr_last_line(), @r"
2268 ------- stderr -------
2269 Error: Failed to generate diff
2270 Caused by:
2271 1: Error executing ':builtin' (run with --debug to see the exact invocation)
2272 [EOF]
2273 [exit status: 1]
2274 ");
2275}
2276
2277#[test]
2278fn test_diff_external_file_by_file_tool() {
2279 let mut test_env = TestEnvironment::default();
2280 let edit_script = test_env.set_up_fake_diff_editor();
2281 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2282 let work_dir = test_env.work_dir("repo");
2283
2284 work_dir.write_file("file1", "file1\n");
2285 work_dir.write_file("file2", "file2\n");
2286 work_dir.run_jj(["new"]).success();
2287 work_dir.remove_file("file1");
2288 work_dir.write_file("file2", "file2\nfile2\n");
2289 work_dir.write_file("file3", "file3\n");
2290 work_dir.write_file("file4", "file1\n");
2291
2292 std::fs::write(
2293 edit_script,
2294 "print ==\0print-files-before\0print --\0print-files-after",
2295 )
2296 .unwrap();
2297
2298 // Enabled by default, looks up the merge-tools table
2299 let configs: &[_] = &[
2300 "--config=ui.diff.tool=fake-diff-editor",
2301 "--config=merge-tools.fake-diff-editor.diff-invocation-mode=file-by-file",
2302 ];
2303
2304 // diff without file patterns
2305 insta::assert_snapshot!(work_dir.run_jj_with(|cmd| cmd.arg("diff").args(configs)), @r"
2306 ==
2307 file2
2308 --
2309 file2
2310 ==
2311 file3
2312 --
2313 file3
2314 ==
2315 file1
2316 --
2317 file4
2318 [EOF]
2319 ");
2320
2321 // diff with file patterns
2322 insta::assert_snapshot!(
2323 work_dir.run_jj_with(|cmd| cmd.args(["diff", "file1"]).args(configs)), @r"
2324 ==
2325 file1
2326 --
2327 file1
2328 [EOF]
2329 ");
2330 insta::assert_snapshot!(
2331 work_dir.run_jj_with(|cmd| cmd.args(["log", "-p"]).args(configs)), @r"
2332 @ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 7b01704a
2333 │ (no description set)
2334 │ ==
2335 │ file2
2336 │ --
2337 │ file2
2338 │ ==
2339 │ file3
2340 │ --
2341 │ file3
2342 │ ==
2343 │ file1
2344 │ --
2345 │ file4
2346 ○ qpvuntsm test.user@example.com 2001-02-03 08:05:08 6e485984
2347 │ (no description set)
2348 │ ==
2349 │ file1
2350 │ --
2351 │ file1
2352 │ ==
2353 │ file2
2354 │ --
2355 │ file2
2356 ◆ zzzzzzzz root() 00000000
2357 [EOF]
2358 ");
2359
2360 insta::assert_snapshot!(work_dir.run_jj_with(|cmd| cmd.arg("show").args(configs)), @r"
2361 Commit ID: 7b01704a670bc77d11ed117d362855cff1d4513b
2362 Change ID: rlvkpnrzqnoowoytxnquwvuryrwnrmlp
2363 Author : Test User <test.user@example.com> (2001-02-03 08:05:09)
2364 Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
2365
2366 (no description set)
2367
2368 ==
2369 file2
2370 --
2371 file2
2372 ==
2373 file3
2374 --
2375 file3
2376 ==
2377 file1
2378 --
2379 file4
2380 [EOF]
2381 ");
2382}
2383
2384#[cfg(unix)]
2385#[test]
2386fn test_diff_external_tool_symlink() {
2387 let mut test_env = TestEnvironment::default();
2388 let edit_script = test_env.set_up_fake_diff_editor();
2389 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2390 let work_dir = test_env.work_dir("repo");
2391
2392 let external_file_path = test_env.env_root().join("external-file");
2393 std::fs::write(&external_file_path, "").unwrap();
2394 let external_file_permissions = external_file_path.symlink_metadata().unwrap().permissions();
2395
2396 std::os::unix::fs::symlink("non-existent1", work_dir.root().join("dead")).unwrap();
2397 std::os::unix::fs::symlink(&external_file_path, work_dir.root().join("file")).unwrap();
2398 work_dir.run_jj(["new"]).success();
2399 work_dir.remove_file("dead");
2400 std::os::unix::fs::symlink("non-existent2", work_dir.root().join("dead")).unwrap();
2401 work_dir.remove_file("file");
2402 work_dir.write_file("file", "");
2403
2404 std::fs::write(
2405 edit_script,
2406 "print-files-before\0print --\0print-files-after",
2407 )
2408 .unwrap();
2409
2410 // Shouldn't try to change permission of symlinks
2411 insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-diff-editor"]), @r"
2412 dead
2413 file
2414 --
2415 dead
2416 file
2417 [EOF]
2418 ");
2419
2420 // External file should be intact
2421 assert_eq!(
2422 external_file_path.symlink_metadata().unwrap().permissions(),
2423 external_file_permissions
2424 );
2425}
2426
2427#[test]
2428fn test_diff_external_tool_conflict_marker_style() {
2429 let mut test_env = TestEnvironment::default();
2430 let edit_script = test_env.set_up_fake_diff_editor();
2431 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2432 let work_dir = test_env.work_dir("repo");
2433 let file_path = "file";
2434
2435 // Create a conflict
2436 work_dir.write_file(
2437 file_path,
2438 indoc! {"
2439 line 1
2440 line 2
2441 line 3
2442 line 4
2443 line 5
2444 "},
2445 );
2446 work_dir.run_jj(["commit", "-m", "base"]).success();
2447 work_dir.write_file(
2448 file_path,
2449 indoc! {"
2450 line 1
2451 line 2.1
2452 line 2.2
2453 line 3
2454 line 4.1
2455 line 5
2456 "},
2457 );
2458 work_dir.run_jj(["describe", "-m", "side-a"]).success();
2459 work_dir
2460 .run_jj(["new", "description(base)", "-m", "side-b"])
2461 .success();
2462 work_dir.write_file(
2463 file_path,
2464 indoc! {"
2465 line 1
2466 line 2.3
2467 line 3
2468 line 4.2
2469 line 4.3
2470 line 5
2471 "},
2472 );
2473
2474 // Resolve one of the conflicts in the working copy
2475 work_dir
2476 .run_jj(["new", "description(side-a)", "description(side-b)"])
2477 .success();
2478 work_dir.write_file(
2479 file_path,
2480 indoc! {"
2481 line 1
2482 line 2.1
2483 line 2.2
2484 line 2.3
2485 line 3
2486 <<<<<<<
2487 %%%%%%%
2488 -line 4
2489 +line 4.1
2490 +++++++
2491 line 4.2
2492 line 4.3
2493 >>>>>>>
2494 line 5
2495 "},
2496 );
2497
2498 // Set up diff editor to use "snapshot" conflict markers
2499 test_env.add_config(r#"merge-tools.fake-diff-editor.conflict-marker-style = "snapshot""#);
2500
2501 // We want to see whether the diff is using the correct conflict markers
2502 std::fs::write(
2503 &edit_script,
2504 ["files-before file", "files-after file", "dump file file"].join("\0"),
2505 )
2506 .unwrap();
2507 let output = work_dir.run_jj(["diff", "--tool", "fake-diff-editor"]);
2508 insta::assert_snapshot!(output, @"");
2509 // Conflicts should render using "snapshot" format
2510 insta::assert_snapshot!(
2511 std::fs::read_to_string(test_env.env_root().join("file")).unwrap(), @r"
2512 line 1
2513 line 2.1
2514 line 2.2
2515 line 2.3
2516 line 3
2517 <<<<<<< Conflict 1 of 1
2518 +++++++ Contents of side #1
2519 line 4.1
2520 ------- Contents of base
2521 line 4
2522 +++++++ Contents of side #2
2523 line 4.2
2524 line 4.3
2525 >>>>>>> Conflict 1 of 1 ends
2526 line 5
2527 ");
2528}
2529
2530#[test]
2531fn test_diff_stat() {
2532 let test_env = TestEnvironment::default();
2533 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2534 let work_dir = test_env.work_dir("repo");
2535 work_dir.write_file("file1", "foo\n");
2536
2537 let output = work_dir.run_jj(["diff", "--stat"]);
2538 insta::assert_snapshot!(output, @r"
2539 file1 | 1 +
2540 1 file changed, 1 insertion(+), 0 deletions(-)
2541 [EOF]
2542 ");
2543
2544 work_dir.run_jj(["new"]).success();
2545
2546 let output = work_dir.run_jj(["diff", "--stat"]);
2547 insta::assert_snapshot!(output, @r"
2548 0 files changed, 0 insertions(+), 0 deletions(-)
2549 [EOF]
2550 ");
2551
2552 work_dir.write_file("file1", "foo\nbar\n");
2553 work_dir.run_jj(["new"]).success();
2554 work_dir.write_file("file1", "bar\n");
2555
2556 let output = work_dir.run_jj(["diff", "--stat"]);
2557 insta::assert_snapshot!(output, @r"
2558 file1 | 1 -
2559 1 file changed, 0 insertions(+), 1 deletion(-)
2560 [EOF]
2561 ");
2562}
2563
2564#[test]
2565fn test_diff_stat_long_name_or_stat() {
2566 let mut test_env = TestEnvironment::default();
2567 test_env.add_env_var("COLUMNS", "30");
2568 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2569 let work_dir = test_env.work_dir("repo");
2570
2571 let get_stat = |work_dir: &TestWorkDir, path_length: usize, stat_size: usize| {
2572 work_dir.run_jj(["new", "root()"]).success();
2573 let ascii_name = "1234567890".chars().cycle().take(path_length).join("");
2574 let han_name = "一二三四五六七八九十"
2575 .chars()
2576 .cycle()
2577 .take(path_length)
2578 .join("");
2579 let content = "content line\n".repeat(stat_size);
2580 work_dir.write_file(ascii_name, &content);
2581 work_dir.write_file(han_name, &content);
2582 work_dir.run_jj(["diff", "--stat"])
2583 };
2584
2585 insta::assert_snapshot!(get_stat(&work_dir, 1, 1), @r"
2586 1 | 1 +
2587 一 | 1 +
2588 2 files changed, 2 insertions(+), 0 deletions(-)
2589 [EOF]
2590 ");
2591 insta::assert_snapshot!(get_stat(&work_dir, 1, 10), @r"
2592 1 | 10 ++++++++++
2593 一 | 10 ++++++++++
2594 2 files changed, 20 insertions(+), 0 deletions(-)
2595 [EOF]
2596 ");
2597 insta::assert_snapshot!(get_stat(&work_dir, 1, 100), @r"
2598 1 | 100 +++++++++++++++++
2599 一 | 100 +++++++++++++++++
2600 2 files changed, 200 insertions(+), 0 deletions(-)
2601 [EOF]
2602 ");
2603 insta::assert_snapshot!(get_stat(&work_dir, 10, 1), @r"
2604 1234567890 | 1 +
2605 ...五六七八九十 | 1 +
2606 2 files changed, 2 insertions(+), 0 deletions(-)
2607 [EOF]
2608 ");
2609 insta::assert_snapshot!(get_stat(&work_dir, 10, 10), @r"
2610 1234567890 | 10 +++++++
2611 ...六七八九十 | 10 +++++++
2612 2 files changed, 20 insertions(+), 0 deletions(-)
2613 [EOF]
2614 ");
2615 insta::assert_snapshot!(get_stat(&work_dir, 10, 100), @r"
2616 1234567890 | 100 ++++++
2617 ...六七八九十 | 100 ++++++
2618 2 files changed, 200 insertions(+), 0 deletions(-)
2619 [EOF]
2620 ");
2621 insta::assert_snapshot!(get_stat(&work_dir, 50, 1), @r"
2622 ...901234567890 | 1 +
2623 ...五六七八九十 | 1 +
2624 2 files changed, 2 insertions(+), 0 deletions(-)
2625 [EOF]
2626 ");
2627 insta::assert_snapshot!(get_stat(&work_dir, 50, 10), @r"
2628 ...01234567890 | 10 +++++++
2629 ...六七八九十 | 10 +++++++
2630 2 files changed, 20 insertions(+), 0 deletions(-)
2631 [EOF]
2632 ");
2633 insta::assert_snapshot!(get_stat(&work_dir, 50, 100), @r"
2634 ...01234567890 | 100 ++++++
2635 ...六七八九十 | 100 ++++++
2636 2 files changed, 200 insertions(+), 0 deletions(-)
2637 [EOF]
2638 ");
2639
2640 // Lengths around where we introduce the ellipsis
2641 insta::assert_snapshot!(get_stat(&work_dir, 13, 100), @r"
2642 1234567890123 | 100 ++++++
2643 ...九十一二三 | 100 ++++++
2644 2 files changed, 200 insertions(+), 0 deletions(-)
2645 [EOF]
2646 ");
2647 insta::assert_snapshot!(get_stat(&work_dir, 14, 100), @r"
2648 12345678901234 | 100 ++++++
2649 ...十一二三四 | 100 ++++++
2650 2 files changed, 200 insertions(+), 0 deletions(-)
2651 [EOF]
2652 ");
2653 insta::assert_snapshot!(get_stat(&work_dir, 15, 100), @r"
2654 ...56789012345 | 100 ++++++
2655 ...一二三四五 | 100 ++++++
2656 2 files changed, 200 insertions(+), 0 deletions(-)
2657 [EOF]
2658 ");
2659 insta::assert_snapshot!(get_stat(&work_dir, 16, 100), @r"
2660 ...67890123456 | 100 ++++++
2661 ...二三四五六 | 100 ++++++
2662 2 files changed, 200 insertions(+), 0 deletions(-)
2663 [EOF]
2664 ");
2665
2666 // Very narrow terminal (doesn't have to fit, just don't crash)
2667 test_env.add_env_var("COLUMNS", "10");
2668 let work_dir = test_env.work_dir("repo");
2669 insta::assert_snapshot!(get_stat(&work_dir, 10, 10), @r"
2670 ... | 10 ++
2671 ... | 10 ++
2672 2 files changed, 20 insertions(+), 0 deletions(-)
2673 [EOF]
2674 ");
2675 test_env.add_env_var("COLUMNS", "3");
2676 let work_dir = test_env.work_dir("repo");
2677 insta::assert_snapshot!(get_stat(&work_dir, 10, 10), @r"
2678 ... | 10 ++
2679 ... | 10 ++
2680 2 files changed, 20 insertions(+), 0 deletions(-)
2681 [EOF]
2682 ");
2683 insta::assert_snapshot!(get_stat(&work_dir, 3, 10), @r"
2684 123 | 10 ++
2685 ... | 10 ++
2686 2 files changed, 20 insertions(+), 0 deletions(-)
2687 [EOF]
2688 ");
2689 insta::assert_snapshot!(get_stat(&work_dir, 1, 10), @r"
2690 1 | 10 ++
2691 一 | 10 ++
2692 2 files changed, 20 insertions(+), 0 deletions(-)
2693 [EOF]
2694 ");
2695}
2696
2697#[test]
2698fn test_diff_binary() {
2699 let test_env = TestEnvironment::default();
2700 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2701 let work_dir = test_env.work_dir("repo");
2702
2703 work_dir.write_file("file1.png", b"\x89PNG\r\n\x1a\nabcdefg\0");
2704 work_dir.write_file("file2.png", b"\x89PNG\r\n\x1a\n0123456\0");
2705 work_dir.run_jj(["new"]).success();
2706 work_dir.remove_file("file1.png");
2707 work_dir.write_file("file2.png", "foo\nbar\n");
2708 work_dir.write_file("file3.png", b"\x89PNG\r\n\x1a\nxyz\0");
2709 // try a file that's valid UTF-8 but contains control characters
2710 work_dir.write_file("file4.png", b"\0\0\0");
2711
2712 let output = work_dir.run_jj(["diff"]);
2713 insta::assert_snapshot!(output, @r"
2714 Removed regular file file1.png:
2715 (binary)
2716 Modified regular file file2.png:
2717 (binary)
2718 Added regular file file3.png:
2719 (binary)
2720 Added regular file file4.png:
2721 (binary)
2722 [EOF]
2723 ");
2724
2725 let output = work_dir.run_jj(["diff", "--git"]);
2726 insta::assert_snapshot!(output, @r"
2727 diff --git a/file1.png b/file1.png
2728 deleted file mode 100644
2729 index 2b65b23c22..0000000000
2730 Binary files a/file1.png and /dev/null differ
2731 diff --git a/file2.png b/file2.png
2732 index 7f036ce788..3bd1f0e297 100644
2733 Binary files a/file2.png and b/file2.png differ
2734 diff --git a/file3.png b/file3.png
2735 new file mode 100644
2736 index 0000000000..deacfbc286
2737 Binary files /dev/null and b/file3.png differ
2738 diff --git a/file4.png b/file4.png
2739 new file mode 100644
2740 index 0000000000..4227ca4e87
2741 Binary files /dev/null and b/file4.png differ
2742 [EOF]
2743 ");
2744
2745 let output = work_dir.run_jj(["diff", "--stat"]);
2746 insta::assert_snapshot!(output, @r"
2747 file1.png | 3 ---
2748 file2.png | 5 ++---
2749 file3.png | 3 +++
2750 file4.png | 1 +
2751 4 files changed, 6 insertions(+), 6 deletions(-)
2752 [EOF]
2753 ");
2754}
2755
2756#[test]
2757fn test_diff_revisions() {
2758 let test_env = TestEnvironment::default();
2759 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2760 let work_dir = test_env.work_dir("repo");
2761
2762 //
2763 //
2764 //
2765 // E
2766 // |\
2767 // C D
2768 // |/
2769 // B
2770 // |
2771 // A
2772 create_commit(&work_dir, "A", &[]);
2773 create_commit(&work_dir, "B", &["A"]);
2774 create_commit(&work_dir, "C", &["B"]);
2775 create_commit(&work_dir, "D", &["B"]);
2776 create_commit(&work_dir, "E", &["C", "D"]);
2777
2778 let diff_revisions = |expression: &str| -> CommandOutput {
2779 work_dir.run_jj(["diff", "--name-only", "-r", expression])
2780 };
2781 // Can diff a single revision
2782 insta::assert_snapshot!(diff_revisions("B"), @r"
2783 B
2784 [EOF]
2785 ");
2786
2787 // Can diff a merge
2788 insta::assert_snapshot!(diff_revisions("E"), @r"
2789 E
2790 [EOF]
2791 ");
2792
2793 // A gap in the range is not allowed (yet at least)
2794 insta::assert_snapshot!(diff_revisions("A|C"), @r"
2795 ------- stderr -------
2796 Error: Cannot diff revsets with gaps in.
2797 Hint: Revision 50c75fd767bf would need to be in the set.
2798 [EOF]
2799 [exit status: 1]
2800 ");
2801
2802 // Can diff a linear chain
2803 insta::assert_snapshot!(diff_revisions("A::C"), @r"
2804 A
2805 B
2806 C
2807 [EOF]
2808 ");
2809
2810 // Can diff a chain with an internal merge
2811 insta::assert_snapshot!(diff_revisions("B::E"), @r"
2812 B
2813 C
2814 D
2815 E
2816 [EOF]
2817 ");
2818
2819 // Can diff a set with multiple roots
2820 insta::assert_snapshot!(diff_revisions("C|D|E"), @r"
2821 C
2822 D
2823 E
2824 [EOF]
2825 ");
2826
2827 // Can diff a set with multiple heads
2828 insta::assert_snapshot!(diff_revisions("B|C|D"), @r"
2829 B
2830 C
2831 D
2832 [EOF]
2833 ");
2834
2835 // Can diff a set with multiple root and multiple heads
2836 insta::assert_snapshot!(diff_revisions("B|C"), @r"
2837 B
2838 C
2839 [EOF]
2840 ");
2841}