just playing with tangled
1// Copyright 2022 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use itertools::Itertools;
16
17use crate::common::{escaped_fake_diff_editor_path, strip_last_line, TestEnvironment};
18
19#[test]
20fn test_diff_basic() {
21 let test_env = TestEnvironment::default();
22 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
23 let repo_path = test_env.env_root().join("repo");
24
25 std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
26 std::fs::write(repo_path.join("file2"), "foo\n").unwrap();
27 test_env.jj_cmd_ok(&repo_path, &["new"]);
28 std::fs::remove_file(repo_path.join("file1")).unwrap();
29 std::fs::write(repo_path.join("file2"), "foo\nbar\n").unwrap();
30 std::fs::write(repo_path.join("file3"), "foo\n").unwrap();
31
32 let stdout = test_env.jj_cmd_success(&repo_path, &["diff"]);
33 insta::assert_snapshot!(stdout, @r###"
34 Removed regular file file1:
35 1 : foo
36 Modified regular file file2:
37 1 1: foo
38 2: bar
39 Added regular file file3:
40 1: foo
41 "###);
42
43 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--context=0"]);
44 insta::assert_snapshot!(stdout, @r###"
45 Removed regular file file1:
46 1 : foo
47 Modified regular file file2:
48 1 1: foo
49 2: bar
50 Added regular file file3:
51 1: foo
52 "###);
53
54 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]);
55 insta::assert_snapshot!(stdout, @r###"
56 D file1
57 M file2
58 A file3
59 "###);
60
61 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--types"]);
62 insta::assert_snapshot!(stdout, @r###"
63 F- file1
64 FF file2
65 -F file3
66 "###);
67
68 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--git"]);
69 insta::assert_snapshot!(stdout, @r###"
70 diff --git a/file1 b/file1
71 deleted file mode 100644
72 index 257cc5642c..0000000000
73 --- a/file1
74 +++ /dev/null
75 @@ -1,1 +1,0 @@
76 -foo
77 diff --git a/file2 b/file2
78 index 257cc5642c...3bd1f0e297 100644
79 --- a/file2
80 +++ b/file2
81 @@ -1,1 +1,2 @@
82 foo
83 +bar
84 diff --git a/file3 b/file3
85 new file mode 100644
86 index 0000000000..257cc5642c
87 --- /dev/null
88 +++ b/file3
89 @@ -1,0 +1,1 @@
90 +foo
91 "###);
92
93 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--git", "--context=0"]);
94 insta::assert_snapshot!(stdout, @r###"
95 diff --git a/file1 b/file1
96 deleted file mode 100644
97 index 257cc5642c..0000000000
98 --- a/file1
99 +++ /dev/null
100 @@ -1,1 +1,0 @@
101 -foo
102 diff --git a/file2 b/file2
103 index 257cc5642c...3bd1f0e297 100644
104 --- a/file2
105 +++ b/file2
106 @@ -2,0 +2,1 @@
107 +bar
108 diff --git a/file3 b/file3
109 new file mode 100644
110 index 0000000000..257cc5642c
111 --- /dev/null
112 +++ b/file3
113 @@ -1,0 +1,1 @@
114 +foo
115 "###);
116
117 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s", "--git"]);
118 insta::assert_snapshot!(stdout, @r###"
119 D file1
120 M file2
121 A file3
122 diff --git a/file1 b/file1
123 deleted file mode 100644
124 index 257cc5642c..0000000000
125 --- a/file1
126 +++ /dev/null
127 @@ -1,1 +1,0 @@
128 -foo
129 diff --git a/file2 b/file2
130 index 257cc5642c...3bd1f0e297 100644
131 --- a/file2
132 +++ b/file2
133 @@ -1,1 +1,2 @@
134 foo
135 +bar
136 diff --git a/file3 b/file3
137 new file mode 100644
138 index 0000000000..257cc5642c
139 --- /dev/null
140 +++ b/file3
141 @@ -1,0 +1,1 @@
142 +foo
143 "###);
144
145 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--stat"]);
146 insta::assert_snapshot!(stdout, @r###"
147 file1 | 1 -
148 file2 | 1 +
149 file3 | 1 +
150 3 files changed, 2 insertions(+), 1 deletion(-)
151 "###);
152
153 // Filter by glob pattern
154 let stdout = test_env.jj_cmd_success(
155 &repo_path,
156 &[
157 "diff",
158 "--config-toml=ui.allow-filesets=true",
159 "-s",
160 r#"glob:"file[12]""#,
161 ],
162 );
163 insta::assert_snapshot!(stdout, @r###"
164 D file1
165 M file2
166 "###);
167
168 // Unmatched paths should generate warnings
169 let (stdout, stderr) = test_env.jj_cmd_ok(
170 test_env.env_root(),
171 &[
172 "diff",
173 "-Rrepo",
174 "-s",
175 "repo", // matches directory
176 "repo/file1", // deleted in to_tree, but exists in from_tree
177 "repo/x",
178 "repo/y/z",
179 ],
180 );
181 insta::assert_snapshot!(stdout.replace('\\', "/"), @r###"
182 D repo/file1
183 M repo/file2
184 A repo/file3
185 "###);
186 insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
187 Warning: No matching entries for paths: repo/x, repo/y/z
188 "###);
189
190 // Unmodified paths shouldn't generate warnings
191 let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["diff", "-s", "--from=@", "file2"]);
192 insta::assert_snapshot!(stdout, @"");
193 insta::assert_snapshot!(stderr, @"");
194}
195
196#[test]
197fn test_diff_empty() {
198 let test_env = TestEnvironment::default();
199 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
200 let repo_path = test_env.env_root().join("repo");
201
202 std::fs::write(repo_path.join("file1"), "").unwrap();
203 let stdout = test_env.jj_cmd_success(&repo_path, &["diff"]);
204 insta::assert_snapshot!(stdout, @r###"
205 Added regular file file1:
206 (empty)
207 "###);
208
209 test_env.jj_cmd_ok(&repo_path, &["new"]);
210 std::fs::remove_file(repo_path.join("file1")).unwrap();
211 let stdout = test_env.jj_cmd_success(&repo_path, &["diff"]);
212 insta::assert_snapshot!(stdout, @r###"
213 Removed regular file file1:
214 (empty)
215 "###);
216
217 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--stat"]);
218 insta::assert_snapshot!(stdout, @r###"
219 file1 | 0
220 1 file changed, 0 insertions(+), 0 deletions(-)
221 "###);
222}
223
224#[test]
225fn test_diff_types() {
226 let test_env = TestEnvironment::default();
227 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
228 let repo_path = test_env.env_root().join("repo");
229
230 let file_path = repo_path.join("foo");
231
232 // Missing
233 test_env.jj_cmd_ok(&repo_path, &["new", "root()", "-m=missing"]);
234
235 // Normal file
236 test_env.jj_cmd_ok(&repo_path, &["new", "root()", "-m=file"]);
237 std::fs::write(&file_path, "foo").unwrap();
238
239 // Conflict (add/add)
240 test_env.jj_cmd_ok(&repo_path, &["new", "root()", "-m=conflict"]);
241 std::fs::write(&file_path, "foo").unwrap();
242 test_env.jj_cmd_ok(&repo_path, &["new", "root()"]);
243 std::fs::write(&file_path, "bar").unwrap();
244 test_env.jj_cmd_ok(&repo_path, &["squash", r#"--into=description("conflict")"#]);
245
246 #[cfg(unix)]
247 {
248 use std::os::unix::fs::PermissionsExt;
249 use std::path::PathBuf;
250
251 // Executable
252 test_env.jj_cmd_ok(&repo_path, &["new", "root()", "-m=executable"]);
253 std::fs::write(&file_path, "foo").unwrap();
254 std::fs::set_permissions(&file_path, std::fs::Permissions::from_mode(0o755)).unwrap();
255
256 // Symlink
257 test_env.jj_cmd_ok(&repo_path, &["new", "root()", "-m=symlink"]);
258 std::os::unix::fs::symlink(PathBuf::from("."), &file_path).unwrap();
259 }
260
261 let diff = |from: &str, to: &str| {
262 test_env.jj_cmd_success(
263 &repo_path,
264 &[
265 "diff",
266 "--types",
267 &format!(r#"--from=description("{}")"#, from),
268 &format!(r#"--to=description("{}")"#, to),
269 ],
270 )
271 };
272 insta::assert_snapshot!(diff("missing", "file"), @r###"
273 -F foo
274 "###);
275 insta::assert_snapshot!(diff("file", "conflict"), @r###"
276 FC foo
277 "###);
278 insta::assert_snapshot!(diff("conflict", "missing"), @r###"
279 C- foo
280 "###);
281
282 #[cfg(unix)]
283 {
284 insta::assert_snapshot!(diff("symlink", "file"), @r###"
285 LF foo
286 "###);
287 insta::assert_snapshot!(diff("missing", "executable"), @r###"
288 -F foo
289 "###);
290 }
291}
292
293#[test]
294fn test_diff_bad_args() {
295 let test_env = TestEnvironment::default();
296 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
297 let repo_path = test_env.env_root().join("repo");
298
299 let stderr = test_env.jj_cmd_cli_error(&repo_path, &["diff", "-s", "--types"]);
300 insta::assert_snapshot!(stderr, @r###"
301 error: the argument '--summary' cannot be used with '--types'
302
303 Usage: jj diff --summary [PATHS]...
304
305 For more information, try '--help'.
306 "###);
307
308 let stderr = test_env.jj_cmd_cli_error(&repo_path, &["diff", "--color-words", "--git"]);
309 insta::assert_snapshot!(stderr, @r###"
310 error: the argument '--color-words' cannot be used with '--git'
311
312 Usage: jj diff --color-words [PATHS]...
313
314 For more information, try '--help'.
315 "###);
316}
317
318#[test]
319fn test_diff_relative_paths() {
320 let test_env = TestEnvironment::default();
321 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
322 let repo_path = test_env.env_root().join("repo");
323
324 std::fs::create_dir_all(repo_path.join("dir1").join("subdir1")).unwrap();
325 std::fs::create_dir(repo_path.join("dir2")).unwrap();
326 std::fs::write(repo_path.join("file1"), "foo1\n").unwrap();
327 std::fs::write(repo_path.join("dir1").join("file2"), "foo2\n").unwrap();
328 std::fs::write(
329 repo_path.join("dir1").join("subdir1").join("file3"),
330 "foo3\n",
331 )
332 .unwrap();
333 std::fs::write(repo_path.join("dir2").join("file4"), "foo4\n").unwrap();
334 test_env.jj_cmd_ok(&repo_path, &["new"]);
335 std::fs::write(repo_path.join("file1"), "bar1\n").unwrap();
336 std::fs::write(repo_path.join("dir1").join("file2"), "bar2\n").unwrap();
337 std::fs::write(
338 repo_path.join("dir1").join("subdir1").join("file3"),
339 "bar3\n",
340 )
341 .unwrap();
342 std::fs::write(repo_path.join("dir2").join("file4"), "bar4\n").unwrap();
343
344 let stdout = test_env.jj_cmd_success(&repo_path.join("dir1"), &["diff"]);
345 #[cfg(unix)]
346 insta::assert_snapshot!(stdout, @r###"
347 Modified regular file file2:
348 1 1: foo2bar2
349 Modified regular file subdir1/file3:
350 1 1: foo3bar3
351 Modified regular file ../dir2/file4:
352 1 1: foo4bar4
353 Modified regular file ../file1:
354 1 1: foo1bar1
355 "###);
356 #[cfg(windows)]
357 insta::assert_snapshot!(stdout, @r###"
358 Modified regular file file2:
359 1 1: foo2bar2
360 Modified regular file subdir1\file3:
361 1 1: foo3bar3
362 Modified regular file ..\dir2\file4:
363 1 1: foo4bar4
364 Modified regular file ..\file1:
365 1 1: foo1bar1
366 "###);
367
368 let stdout = test_env.jj_cmd_success(&repo_path.join("dir1"), &["diff", "-s"]);
369 #[cfg(unix)]
370 insta::assert_snapshot!(stdout, @r###"
371 M file2
372 M subdir1/file3
373 M ../dir2/file4
374 M ../file1
375 "###);
376 #[cfg(windows)]
377 insta::assert_snapshot!(stdout, @r###"
378 M file2
379 M subdir1\file3
380 M ..\dir2\file4
381 M ..\file1
382 "###);
383
384 let stdout = test_env.jj_cmd_success(&repo_path.join("dir1"), &["diff", "--types"]);
385 #[cfg(unix)]
386 insta::assert_snapshot!(stdout, @r###"
387 FF file2
388 FF subdir1/file3
389 FF ../dir2/file4
390 FF ../file1
391 "###);
392 #[cfg(windows)]
393 insta::assert_snapshot!(stdout, @r###"
394 FF file2
395 FF subdir1\file3
396 FF ..\dir2\file4
397 FF ..\file1
398 "###);
399
400 let stdout = test_env.jj_cmd_success(&repo_path.join("dir1"), &["diff", "--git"]);
401 insta::assert_snapshot!(stdout, @r###"
402 diff --git a/dir1/file2 b/dir1/file2
403 index 54b060eee9...1fe912cdd8 100644
404 --- a/dir1/file2
405 +++ b/dir1/file2
406 @@ -1,1 +1,1 @@
407 -foo2
408 +bar2
409 diff --git a/dir1/subdir1/file3 b/dir1/subdir1/file3
410 index c1ec6c6f12...f3c8b75ec6 100644
411 --- a/dir1/subdir1/file3
412 +++ b/dir1/subdir1/file3
413 @@ -1,1 +1,1 @@
414 -foo3
415 +bar3
416 diff --git a/dir2/file4 b/dir2/file4
417 index a0016dbc4c...17375f7a12 100644
418 --- a/dir2/file4
419 +++ b/dir2/file4
420 @@ -1,1 +1,1 @@
421 -foo4
422 +bar4
423 diff --git a/file1 b/file1
424 index 1715acd6a5...05c4fe6772 100644
425 --- a/file1
426 +++ b/file1
427 @@ -1,1 +1,1 @@
428 -foo1
429 +bar1
430 "###);
431
432 let stdout = test_env.jj_cmd_success(&repo_path.join("dir1"), &["diff", "--stat"]);
433 #[cfg(unix)]
434 insta::assert_snapshot!(stdout, @r###"
435 file2 | 2 +-
436 subdir1/file3 | 2 +-
437 ../dir2/file4 | 2 +-
438 ../file1 | 2 +-
439 4 files changed, 4 insertions(+), 4 deletions(-)
440 "###);
441 #[cfg(windows)]
442 insta::assert_snapshot!(stdout, @r###"
443 file2 | 2 +-
444 subdir1\file3 | 2 +-
445 ..\dir2\file4 | 2 +-
446 ..\file1 | 2 +-
447 4 files changed, 4 insertions(+), 4 deletions(-)
448 "###);
449}
450
451#[test]
452fn test_diff_missing_newline() {
453 let test_env = TestEnvironment::default();
454 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
455 let repo_path = test_env.env_root().join("repo");
456
457 std::fs::write(repo_path.join("file1"), "foo").unwrap();
458 std::fs::write(repo_path.join("file2"), "foo\nbar").unwrap();
459 test_env.jj_cmd_ok(&repo_path, &["new"]);
460 std::fs::write(repo_path.join("file1"), "foo\nbar").unwrap();
461 std::fs::write(repo_path.join("file2"), "foo").unwrap();
462
463 let stdout = test_env.jj_cmd_success(&repo_path, &["diff"]);
464 insta::assert_snapshot!(stdout, @r###"
465 Modified regular file file1:
466 1 1: foo
467 2: bar
468 Modified regular file file2:
469 1 1: foo
470 2 : bar
471 "###);
472
473 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--git"]);
474 insta::assert_snapshot!(stdout, @r###"
475 diff --git a/file1 b/file1
476 index 1910281566...a907ec3f43 100644
477 --- a/file1
478 +++ b/file1
479 @@ -1,1 +1,2 @@
480 -foo
481 \ No newline at end of file
482 +foo
483 +bar
484 \ No newline at end of file
485 diff --git a/file2 b/file2
486 index a907ec3f43...1910281566 100644
487 --- a/file2
488 +++ b/file2
489 @@ -1,2 +1,1 @@
490 -foo
491 -bar
492 \ No newline at end of file
493 +foo
494 \ No newline at end of file
495 "###);
496
497 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--stat"]);
498 insta::assert_snapshot!(stdout, @r###"
499 file1 | 3 ++-
500 file2 | 3 +--
501 2 files changed, 3 insertions(+), 3 deletions(-)
502 "###);
503}
504
505#[test]
506fn test_color_words_diff_missing_newline() {
507 let test_env = TestEnvironment::default();
508 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
509 let repo_path = test_env.env_root().join("repo");
510
511 std::fs::write(repo_path.join("file1"), "").unwrap();
512 test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "=== Empty"]);
513 std::fs::write(repo_path.join("file1"), "a\nb\nc\nd\ne\nf\ng\nh\ni").unwrap();
514 test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "=== Add no newline"]);
515 std::fs::write(repo_path.join("file1"), "A\nb\nc\nd\ne\nf\ng\nh\ni").unwrap();
516 test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "=== Modify first line"]);
517 std::fs::write(repo_path.join("file1"), "A\nb\nc\nd\nE\nf\ng\nh\ni").unwrap();
518 test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "=== Modify middle line"]);
519 std::fs::write(repo_path.join("file1"), "A\nb\nc\nd\nE\nf\ng\nh\nI").unwrap();
520 test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "=== Modify last line"]);
521 std::fs::write(repo_path.join("file1"), "A\nb\nc\nd\nE\nf\ng\nh\nI\n").unwrap();
522 test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "=== Append newline"]);
523 std::fs::write(repo_path.join("file1"), "A\nb\nc\nd\nE\nf\ng\nh\nI").unwrap();
524 test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "=== Remove newline"]);
525 std::fs::write(repo_path.join("file1"), "").unwrap();
526 test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "=== Empty"]);
527
528 let stdout = test_env.jj_cmd_success(
529 &repo_path,
530 &[
531 "log",
532 "-Tdescription",
533 "-pr::@-",
534 "--no-graph",
535 "--reversed",
536 ],
537 );
538 insta::assert_snapshot!(stdout, @r###"
539 === Empty
540 Added regular file file1:
541 (empty)
542 === Add no newline
543 Modified regular file file1:
544 1: a
545 2: b
546 3: c
547 4: d
548 5: e
549 6: f
550 7: g
551 8: h
552 9: i
553 === Modify first line
554 Modified regular file file1:
555 1 1: aA
556 2 2: b
557 3 3: c
558 4 4: d
559 ...
560 === Modify middle line
561 Modified regular file file1:
562 1 1: A
563 2 2: b
564 3 3: c
565 4 4: d
566 5 5: eE
567 6 6: f
568 7 7: g
569 8 8: h
570 9 9: i
571 === Modify last line
572 Modified regular file file1:
573 ...
574 6 6: f
575 7 7: g
576 8 8: h
577 9 9: iI
578 === Append newline
579 Modified regular file file1:
580 ...
581 6 6: f
582 7 7: g
583 8 8: h
584 9 9: I
585 === Remove newline
586 Modified regular file file1:
587 ...
588 6 6: f
589 7 7: g
590 8 8: h
591 9 9: I
592 === Empty
593 Modified regular file file1:
594 1 : A
595 2 : b
596 3 : c
597 4 : d
598 5 : E
599 6 : f
600 7 : g
601 8 : h
602 9 : I
603 "###);
604}
605
606#[test]
607fn test_diff_skipped_context() {
608 let test_env = TestEnvironment::default();
609 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
610 let repo_path = test_env.env_root().join("repo");
611
612 std::fs::write(repo_path.join("file1"), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj").unwrap();
613 test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "=== Left side of diffs"]);
614
615 test_env.jj_cmd_ok(&repo_path, &["new", "@", "-m", "=== Must skip 2 lines"]);
616 std::fs::write(repo_path.join("file1"), "A\nb\nc\nd\ne\nf\ng\nh\ni\nJ").unwrap();
617 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== Don't skip 1 line"]);
618 std::fs::write(repo_path.join("file1"), "A\nb\nc\nd\ne\nf\ng\nh\nI\nj").unwrap();
619 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== No gap to skip"]);
620 std::fs::write(repo_path.join("file1"), "a\nB\nc\nd\ne\nf\ng\nh\nI\nj").unwrap();
621 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== No gap to skip"]);
622 std::fs::write(repo_path.join("file1"), "a\nb\nC\nd\ne\nf\ng\nh\nI\nj").unwrap();
623 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== 1 line at start"]);
624 std::fs::write(repo_path.join("file1"), "a\nb\nc\nd\nE\nf\ng\nh\ni\nj").unwrap();
625 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== 1 line at end"]);
626 std::fs::write(repo_path.join("file1"), "a\nb\nc\nd\ne\nF\ng\nh\ni\nj").unwrap();
627
628 let stdout = test_env.jj_cmd_success(
629 &repo_path,
630 &["log", "-Tdescription", "-p", "--no-graph", "--reversed"],
631 );
632 insta::assert_snapshot!(stdout, @r###"
633 === Left side of diffs
634 Added regular file file1:
635 1: a
636 2: b
637 3: c
638 4: d
639 5: e
640 6: f
641 7: g
642 8: h
643 9: i
644 10: j
645 === Must skip 2 lines
646 Modified regular file file1:
647 1 1: aA
648 2 2: b
649 3 3: c
650 4 4: d
651 ...
652 7 7: g
653 8 8: h
654 9 9: i
655 10 10: jJ
656 === Don't skip 1 line
657 Modified regular file file1:
658 1 1: aA
659 2 2: b
660 3 3: c
661 4 4: d
662 5 5: e
663 6 6: f
664 7 7: g
665 8 8: h
666 9 9: iI
667 10 10: j
668 === No gap to skip
669 Modified regular file file1:
670 1 1: a
671 2 2: bB
672 3 3: c
673 4 4: d
674 5 5: e
675 6 6: f
676 7 7: g
677 8 8: h
678 9 9: iI
679 10 10: j
680 === No gap to skip
681 Modified regular file file1:
682 1 1: a
683 2 2: b
684 3 3: cC
685 4 4: d
686 5 5: e
687 6 6: f
688 7 7: g
689 8 8: h
690 9 9: iI
691 10 10: j
692 === 1 line at start
693 Modified regular file file1:
694 1 1: a
695 2 2: b
696 3 3: c
697 4 4: d
698 5 5: eE
699 6 6: f
700 7 7: g
701 8 8: h
702 ...
703 === 1 line at end
704 Modified regular file file1:
705 ...
706 3 3: c
707 4 4: d
708 5 5: e
709 6 6: fF
710 7 7: g
711 8 8: h
712 9 9: i
713 10 10: j
714 "###);
715}
716
717#[test]
718fn test_diff_skipped_context_nondefault() {
719 let test_env = TestEnvironment::default();
720 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
721 let repo_path = test_env.env_root().join("repo");
722
723 std::fs::write(repo_path.join("file1"), "a\nb\nc\nd").unwrap();
724 test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "=== Left side of diffs"]);
725
726 test_env.jj_cmd_ok(&repo_path, &["new", "@", "-m", "=== Must skip 2 lines"]);
727 std::fs::write(repo_path.join("file1"), "A\nb\nc\nD").unwrap();
728 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== Don't skip 1 line"]);
729 std::fs::write(repo_path.join("file1"), "A\nb\nC\nd").unwrap();
730 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== No gap to skip"]);
731 std::fs::write(repo_path.join("file1"), "a\nB\nC\nd").unwrap();
732 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== 1 line at start"]);
733 std::fs::write(repo_path.join("file1"), "a\nB\nc\nd").unwrap();
734 test_env.jj_cmd_ok(&repo_path, &["new", "@-", "-m", "=== 1 line at end"]);
735 std::fs::write(repo_path.join("file1"), "a\nb\nC\nd").unwrap();
736
737 let stdout = test_env.jj_cmd_success(
738 &repo_path,
739 &[
740 "log",
741 "-Tdescription",
742 "-p",
743 "--no-graph",
744 "--reversed",
745 "--context=0",
746 ],
747 );
748 insta::assert_snapshot!(stdout, @r###"
749 === Left side of diffs
750 Added regular file file1:
751 1: a
752 2: b
753 3: c
754 4: d
755 === Must skip 2 lines
756 Modified regular file file1:
757 1 1: aA
758 ...
759 4 4: dD
760 === Don't skip 1 line
761 Modified regular file file1:
762 1 1: aA
763 2 2: b
764 3 3: cC
765 4 4: d
766 === No gap to skip
767 Modified regular file file1:
768 1 1: a
769 2 2: bB
770 3 3: cC
771 4 4: d
772 === 1 line at start
773 Modified regular file file1:
774 1 1: a
775 2 2: bB
776 ...
777 === 1 line at end
778 Modified regular file file1:
779 ...
780 3 3: cC
781 4 4: d
782 "###);
783}
784
785#[test]
786fn test_diff_external_tool() {
787 let mut test_env = TestEnvironment::default();
788 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
789 let repo_path = test_env.env_root().join("repo");
790
791 std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
792 std::fs::write(repo_path.join("file2"), "foo\n").unwrap();
793 test_env.jj_cmd_ok(&repo_path, &["new"]);
794 std::fs::remove_file(repo_path.join("file1")).unwrap();
795 std::fs::write(repo_path.join("file2"), "foo\nbar\n").unwrap();
796 std::fs::write(repo_path.join("file3"), "foo\n").unwrap();
797
798 let edit_script = test_env.set_up_fake_diff_editor();
799 std::fs::write(
800 &edit_script,
801 "print-files-before\0print --\0print-files-after",
802 )
803 .unwrap();
804
805 // diff without file patterns
806 insta::assert_snapshot!(
807 test_env.jj_cmd_success(&repo_path, &["diff", "--tool=fake-diff-editor"]), @r###"
808 file1
809 file2
810 --
811 file2
812 file3
813 "###);
814
815 // diff with file patterns
816 insta::assert_snapshot!(
817 test_env.jj_cmd_success(&repo_path, &["diff", "--tool=fake-diff-editor", "file1"]), @r###"
818 file1
819 --
820 "###);
821
822 insta::assert_snapshot!(
823 test_env.jj_cmd_success(&repo_path, &["log", "-p", "--tool=fake-diff-editor"]), @r###"
824 @ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 0cba70c7
825 │ (no description set)
826 │ file1
827 │ file2
828 │ --
829 │ file2
830 │ file3
831 ◉ qpvuntsm test.user@example.com 2001-02-03 08:05:08 39b5a56f
832 │ (no description set)
833 │ --
834 │ file1
835 │ file2
836 ◉ zzzzzzzz root() 00000000
837 --
838 "###);
839
840 insta::assert_snapshot!(
841 test_env.jj_cmd_success(&repo_path, &["show", "--tool=fake-diff-editor"]), @r###"
842 Commit ID: 0cba70c72186eabb5a2f91be63a8366b9f6da6c6
843 Change ID: rlvkpnrzqnoowoytxnquwvuryrwnrmlp
844 Author: Test User <test.user@example.com> (2001-02-03 08:05:08)
845 Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
846
847 (no description set)
848
849 file1
850 file2
851 --
852 file2
853 file3
854 "###);
855
856 // Enabled by default, looks up the merge-tools table
857 let config = "--config-toml=ui.diff.tool='fake-diff-editor'";
858 insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", config]), @r###"
859 file1
860 file2
861 --
862 file2
863 file3
864 "###);
865
866 // Inlined command arguments
867 let command = escaped_fake_diff_editor_path();
868 let config = format!(r#"--config-toml=ui.diff.tool=["{command}", "$right", "$left"]"#);
869 insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", &config]), @r###"
870 file2
871 file3
872 --
873 file1
874 file2
875 "###);
876
877 // Output of external diff tool shouldn't be escaped
878 std::fs::write(&edit_script, "print \x1b[1;31mred").unwrap();
879 insta::assert_snapshot!(
880 test_env.jj_cmd_success(&repo_path, &["diff", "--color=always", "--tool=fake-diff-editor"]),
881 @r###"
882 [1;31mred
883 "###);
884
885 // Non-zero exit code isn't an error
886 std::fs::write(&edit_script, "print diff\0fail").unwrap();
887 let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["show", "--tool=fake-diff-editor"]);
888 insta::assert_snapshot!(stdout, @r###"
889 Commit ID: 0cba70c72186eabb5a2f91be63a8366b9f6da6c6
890 Change ID: rlvkpnrzqnoowoytxnquwvuryrwnrmlp
891 Author: Test User <test.user@example.com> (2001-02-03 08:05:08)
892 Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
893
894 (no description set)
895
896 diff
897 "###);
898 insta::assert_snapshot!(stderr, @r###"
899 Warning: Tool exited with a non-zero code (run with --debug to see the exact invocation). Exit code: 1.
900 "###);
901
902 // --tool=:builtin shouldn't be ignored
903 let stderr = test_env.jj_cmd_failure(&repo_path, &["diff", "--tool=:builtin"]);
904 insta::assert_snapshot!(strip_last_line(&stderr), @r###"
905 Error: Failed to generate diff
906 Caused by:
907 1: Error executing ':builtin' (run with --debug to see the exact invocation)
908 "###);
909}
910
911#[cfg(unix)]
912#[test]
913fn test_diff_external_tool_symlink() {
914 let mut test_env = TestEnvironment::default();
915 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
916 let repo_path = test_env.env_root().join("repo");
917
918 let external_file_path = test_env.env_root().join("external-file");
919 std::fs::write(&external_file_path, "").unwrap();
920 let external_file_permissions = external_file_path.symlink_metadata().unwrap().permissions();
921
922 std::os::unix::fs::symlink("non-existent1", repo_path.join("dead")).unwrap();
923 std::os::unix::fs::symlink(&external_file_path, repo_path.join("file")).unwrap();
924 test_env.jj_cmd_ok(&repo_path, &["new"]);
925 std::fs::remove_file(repo_path.join("dead")).unwrap();
926 std::os::unix::fs::symlink("non-existent2", repo_path.join("dead")).unwrap();
927 std::fs::remove_file(repo_path.join("file")).unwrap();
928 std::fs::write(repo_path.join("file"), "").unwrap();
929
930 let edit_script = test_env.set_up_fake_diff_editor();
931 std::fs::write(
932 edit_script,
933 "print-files-before\0print --\0print-files-after",
934 )
935 .unwrap();
936
937 // Shouldn't try to change permission of symlinks
938 insta::assert_snapshot!(
939 test_env.jj_cmd_success(&repo_path, &["diff", "--tool=fake-diff-editor"]), @r###"
940 dead
941 file
942 --
943 dead
944 file
945 "###);
946
947 // External file should be intact
948 assert_eq!(
949 external_file_path.symlink_metadata().unwrap().permissions(),
950 external_file_permissions
951 );
952}
953
954#[test]
955fn test_diff_stat() {
956 let test_env = TestEnvironment::default();
957 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
958 let repo_path = test_env.env_root().join("repo");
959 std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
960
961 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--stat"]);
962 insta::assert_snapshot!(stdout, @r###"
963 file1 | 1 +
964 1 file changed, 1 insertion(+), 0 deletions(-)
965 "###);
966
967 test_env.jj_cmd_ok(&repo_path, &["new"]);
968
969 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--stat"]);
970 insta::assert_snapshot!(stdout, @"0 files changed, 0 insertions(+), 0 deletions(-)");
971
972 std::fs::write(repo_path.join("file1"), "foo\nbar\n").unwrap();
973 test_env.jj_cmd_ok(&repo_path, &["new"]);
974 std::fs::write(repo_path.join("file1"), "bar\n").unwrap();
975
976 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--stat"]);
977 insta::assert_snapshot!(stdout, @r###"
978 file1 | 1 -
979 1 file changed, 0 insertions(+), 1 deletion(-)
980 "###);
981}
982
983#[test]
984fn test_diff_stat_long_name_or_stat() {
985 let mut test_env = TestEnvironment::default();
986 test_env.add_env_var("COLUMNS", "30");
987 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
988 let repo_path = test_env.env_root().join("repo");
989
990 let get_stat = |test_env: &TestEnvironment, path_length: usize, stat_size: usize| {
991 test_env.jj_cmd_ok(&repo_path, &["new", "root()"]);
992 let ascii_name = "1234567890".chars().cycle().take(path_length).join("");
993 let han_name = "一二三四五六七八九十"
994 .chars()
995 .cycle()
996 .take(path_length)
997 .join("");
998 let content = "content line\n".repeat(stat_size);
999 std::fs::write(repo_path.join(ascii_name), &content).unwrap();
1000 std::fs::write(repo_path.join(han_name), &content).unwrap();
1001 test_env.jj_cmd_success(&repo_path, &["diff", "--stat"])
1002 };
1003
1004 insta::assert_snapshot!(get_stat(&test_env, 1, 1), @r###"
1005 1 | 1 +
1006 一 | 1 +
1007 2 files changed, 2 insertions(+), 0 deletions(-)
1008 "###);
1009 insta::assert_snapshot!(get_stat(&test_env, 1, 10), @r###"
1010 1 | 10 ++++++++++
1011 一 | 10 ++++++++++
1012 2 files changed, 20 insertions(+), 0 deletions(-)
1013 "###);
1014 insta::assert_snapshot!(get_stat(&test_env, 1, 100), @r###"
1015 1 | 100 +++++++++++++++++
1016 一 | 100 +++++++++++++++++
1017 2 files changed, 200 insertions(+), 0 deletions(-)
1018 "###);
1019 insta::assert_snapshot!(get_stat(&test_env, 10, 1), @r###"
1020 1234567890 | 1 +
1021 ...五六七八九十 | 1 +
1022 2 files changed, 2 insertions(+), 0 deletions(-)
1023 "###);
1024 insta::assert_snapshot!(get_stat(&test_env, 10, 10), @r###"
1025 1234567890 | 10 +++++++
1026 ...六七八九十 | 10 +++++++
1027 2 files changed, 20 insertions(+), 0 deletions(-)
1028 "###);
1029 insta::assert_snapshot!(get_stat(&test_env, 10, 100), @r###"
1030 1234567890 | 100 ++++++
1031 ...六七八九十 | 100 ++++++
1032 2 files changed, 200 insertions(+), 0 deletions(-)
1033 "###);
1034 insta::assert_snapshot!(get_stat(&test_env, 50, 1), @r###"
1035 ...901234567890 | 1 +
1036 ...五六七八九十 | 1 +
1037 2 files changed, 2 insertions(+), 0 deletions(-)
1038 "###);
1039 insta::assert_snapshot!(get_stat(&test_env, 50, 10), @r###"
1040 ...01234567890 | 10 +++++++
1041 ...六七八九十 | 10 +++++++
1042 2 files changed, 20 insertions(+), 0 deletions(-)
1043 "###);
1044 insta::assert_snapshot!(get_stat(&test_env, 50, 100), @r###"
1045 ...01234567890 | 100 ++++++
1046 ...六七八九十 | 100 ++++++
1047 2 files changed, 200 insertions(+), 0 deletions(-)
1048 "###);
1049
1050 // Lengths around where we introduce the ellipsis
1051 insta::assert_snapshot!(get_stat(&test_env, 13, 100), @r###"
1052 1234567890123 | 100 ++++++
1053 ...九十一二三 | 100 ++++++
1054 2 files changed, 200 insertions(+), 0 deletions(-)
1055 "###);
1056 insta::assert_snapshot!(get_stat(&test_env, 14, 100), @r###"
1057 12345678901234 | 100 ++++++
1058 ...十一二三四 | 100 ++++++
1059 2 files changed, 200 insertions(+), 0 deletions(-)
1060 "###);
1061 insta::assert_snapshot!(get_stat(&test_env, 15, 100), @r###"
1062 ...56789012345 | 100 ++++++
1063 ...一二三四五 | 100 ++++++
1064 2 files changed, 200 insertions(+), 0 deletions(-)
1065 "###);
1066 insta::assert_snapshot!(get_stat(&test_env, 16, 100), @r###"
1067 ...67890123456 | 100 ++++++
1068 ...二三四五六 | 100 ++++++
1069 2 files changed, 200 insertions(+), 0 deletions(-)
1070 "###);
1071
1072 // Very narrow terminal (doesn't have to fit, just don't crash)
1073 test_env.add_env_var("COLUMNS", "10");
1074 insta::assert_snapshot!(get_stat(&test_env, 10, 10), @r###"
1075 ... | 10 ++
1076 ... | 10 ++
1077 2 files changed, 20 insertions(+), 0 deletions(-)
1078 "###);
1079 test_env.add_env_var("COLUMNS", "3");
1080 insta::assert_snapshot!(get_stat(&test_env, 10, 10), @r###"
1081 ... | 10 ++
1082 ... | 10 ++
1083 2 files changed, 20 insertions(+), 0 deletions(-)
1084 "###);
1085 insta::assert_snapshot!(get_stat(&test_env, 3, 10), @r###"
1086 123 | 10 ++
1087 ... | 10 ++
1088 2 files changed, 20 insertions(+), 0 deletions(-)
1089 "###);
1090 insta::assert_snapshot!(get_stat(&test_env, 1, 10), @r###"
1091 1 | 10 ++
1092 一 | 10 ++
1093 2 files changed, 20 insertions(+), 0 deletions(-)
1094 "###);
1095}
1096
1097#[test]
1098fn test_diff_binary() {
1099 let test_env = TestEnvironment::default();
1100 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
1101 let repo_path = test_env.env_root().join("repo");
1102
1103 std::fs::write(repo_path.join("file1.png"), b"\x89PNG\r\n\x1a\nabcdefg\0").unwrap();
1104 std::fs::write(repo_path.join("file2.png"), b"\x89PNG\r\n\x1a\n0123456\0").unwrap();
1105 test_env.jj_cmd_ok(&repo_path, &["new"]);
1106 std::fs::remove_file(repo_path.join("file1.png")).unwrap();
1107 std::fs::write(repo_path.join("file2.png"), "foo\nbar\n").unwrap();
1108 std::fs::write(repo_path.join("file3.png"), b"\x89PNG\r\n\x1a\nxyz\0").unwrap();
1109 // try a file that's valid UTF-8 but contains control characters
1110 std::fs::write(repo_path.join("file4.png"), b"\0\0\0").unwrap();
1111
1112 let stdout = test_env.jj_cmd_success(&repo_path, &["diff"]);
1113 insta::assert_snapshot!(stdout, @r###"
1114 Removed regular file file1.png:
1115 (binary)
1116 Modified regular file file2.png:
1117 (binary)
1118 Added regular file file3.png:
1119 (binary)
1120 Added regular file file4.png:
1121 (binary)
1122 "###);
1123
1124 let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--stat"]);
1125 insta::assert_snapshot!(stdout, @r###"
1126 file1.png | 3 ---
1127 file2.png | 5 ++---
1128 file3.png | 3 +++
1129 file4.png | 1 +
1130 4 files changed, 6 insertions(+), 6 deletions(-)
1131 "###);
1132}