Files
jj-vcs-jj/cli/tests/test_diff_command.rs
Gaëtan Lehmann bd036daa12 change-id: update tests
Note: we use longer short change-IDs in test_abandon_command to avoid
false codebook detection.
2026-06-27 15:29:00 +02:00

4092 lines
131 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use indoc::indoc;
use itertools::Itertools as _;
use testutils::TestResult;
use crate::common::CommandOutput;
use crate::common::TestEnvironment;
use crate::common::TestWorkDir;
use crate::common::create_commit;
use crate::common::create_commit_with_files;
use crate::common::fake_diff_editor_path;
use crate::common::to_toml_value;
fn strip_ansi_escape_codes(output: String) -> String {
anstream::adapter::strip_str(&output).to_string()
}
#[test]
fn test_diff_basic() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "foo\n");
work_dir.write_file("file2", "1\n2\n3\n4\n");
work_dir.run_jj(["new"]).success();
work_dir.remove_file("file1");
work_dir.write_file("file2", "1\n5\n3\n");
work_dir.write_file("file3", "foo\n");
work_dir.write_file("file4", "1\n2\n3\n4\n");
let output = work_dir.run_jj(["diff"]);
insta::assert_snapshot!(output, @"
Modified regular file file2:
1 1: 1
2 : 2
2: 5
3 3: 3
4 : 4
Modified regular file file3 (file1 => file3):
Modified regular file file4 (file2 => file4):
[EOF]
");
let output = work_dir.run_jj(["diff", "--context=0"]);
insta::assert_snapshot!(output, @"
Modified regular file file2:
1 1: 1
2 : 2
2: 5
3 3: 3
4 : 4
Modified regular file file3 (file1 => file3):
Modified regular file file4 (file2 => file4):
[EOF]
");
let output = work_dir.run_jj(["diff", "--color=debug"]);
insta::assert_snapshot!(output, @"
<<diff color_words header::Modified regular file file2:>>
<<diff color_words context removed line_number:: 1>><<diff color_words context:: >><<diff color_words context added line_number:: 1>><<diff color_words context::: 1>>
<<diff color_words removed line_number:: 2>><<diff color_words:: >><<diff color_words added line_number:: 2>><<diff color_words::: >><<diff color_words removed token::2>><<diff color_words added token::5>><<diff color_words::>>
<<diff color_words context removed line_number:: 3>><<diff color_words context:: >><<diff color_words context added line_number:: 3>><<diff color_words context::: 3>>
<<diff color_words removed line_number:: 4>><<diff color_words:: : >><<diff color_words removed token::4>>
<<diff color_words header::Modified regular file file3 (file1 => file3):>>
<<diff color_words header::Modified regular file file4 (file2 => file4):>>
[EOF]
");
let output = work_dir.run_jj(["diff", "-s"]);
insta::assert_snapshot!(output, @"
M file2
R {file1 => file3}
C {file2 => file4}
[EOF]
");
let output = work_dir.run_jj(["diff", "--types"]);
insta::assert_snapshot!(output, @"
FF file2
FF {file1 => file3}
FF {file2 => file4}
[EOF]
");
let output = work_dir.run_jj(["diff", "--types", "glob:file[12]"]);
insta::assert_snapshot!(output, @"
F- file1
FF file2
[EOF]
");
let template = r#"source.path() ++ ' => ' ++ target.path() ++ ' (' ++ status ++ ")\n""#;
let output = work_dir.run_jj(["diff", "-T", template]);
insta::assert_snapshot!(output, @"
file2 => file2 (modified)
file1 => file3 (renamed)
file2 => file4 (copied)
[EOF]
");
let output = work_dir.run_jj(["diff", "--git", "file1"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
deleted file mode 100644
index 257cc5642c..0000000000
--- a/file1
+++ /dev/null
@@ -1,1 +0,0 @@
-foo
[EOF]
");
let output = work_dir.run_jj(["diff", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/file2 b/file2
index 94ebaf9001..1ffc51b472 100644
--- a/file2
+++ b/file2
@@ -1,4 +1,3 @@
1
-2
+5
3
-4
diff --git a/file1 b/file3
rename from file1
rename to file3
diff --git a/file2 b/file4
copy from file2
copy to file4
[EOF]
");
let output = work_dir.run_jj(["diff", "--git", "--context=0"]);
insta::assert_snapshot!(output, @"
diff --git a/file2 b/file2
index 94ebaf9001..1ffc51b472 100644
--- a/file2
+++ b/file2
@@ -2,1 +2,1 @@
-2
+5
@@ -4,1 +3,0 @@
-4
diff --git a/file1 b/file3
rename from file1
rename to file3
diff --git a/file2 b/file4
copy from file2
copy to file4
[EOF]
");
let output = work_dir.run_jj(["diff", "--git", "--color=debug"]);
insta::assert_snapshot!(output, @"
<<diff git file_header::diff --git a/file2 b/file2>>
<<diff git file_header::index 94ebaf9001..1ffc51b472 100644>>
<<diff git file_header::--- a/file2>>
<<diff git file_header::+++ b/file2>>
<<diff git hunk_header::@@ -1,4 +1,3 @@>>
<<diff git context:: 1>>
<<diff git removed::->><<diff git removed token::2>><<diff git removed::>>
<<diff git added::+>><<diff git added token::5>><<diff git added::>>
<<diff git context:: 3>>
<<diff git removed::->><<diff git removed token::4>>
<<diff git file_header::diff --git a/file1 b/file3>>
<<diff git file_header::rename from file1>>
<<diff git file_header::rename to file3>>
<<diff git file_header::diff --git a/file2 b/file4>>
<<diff git file_header::copy from file2>>
<<diff git file_header::copy to file4>>
[EOF]
");
let output = work_dir.run_jj(["diff", "-s", "--git"]);
insta::assert_snapshot!(output, @"
M file2
R {file1 => file3}
C {file2 => file4}
diff --git a/file2 b/file2
index 94ebaf9001..1ffc51b472 100644
--- a/file2
+++ b/file2
@@ -1,4 +1,3 @@
1
-2
+5
3
-4
diff --git a/file1 b/file3
rename from file1
rename to file3
diff --git a/file2 b/file4
copy from file2
copy to file4
[EOF]
");
let output = work_dir.run_jj(["diff", "--git", "--config=diff.git.show-path-prefix=false"]);
insta::assert_snapshot!(output, @"
diff --git file2 file2
index 94ebaf9001..1ffc51b472 100644
--- file2
+++ file2
@@ -1,4 +1,3 @@
1
-2
+5
3
-4
diff --git file1 file3
rename from file1
rename to file3
diff --git file2 file4
copy from file2
copy to file4
[EOF]
");
let output = work_dir.run_jj(["diff", "--stat"]);
insta::assert_snapshot!(output, @"
file2 | 3 +--
{file1 => file3} | 0
{file2 => file4} | 0
3 files changed, 1 insertion(+), 2 deletions(-)
[EOF]
");
// Reverse-order diff
let output = work_dir.run_jj(["diff", "--to", "@-", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/file3 b/file1
rename from file3
rename to file1
diff --git a/file2 b/file2
index 1ffc51b472..94ebaf9001 100644
--- a/file2
+++ b/file2
@@ -1,3 +1,4 @@
1
-5
+2
3
+4
diff --git a/file4 b/file4
deleted file mode 100644
index 94ebaf9001..0000000000
--- a/file4
+++ /dev/null
@@ -1,4 +0,0 @@
-1
-2
-3
-4
[EOF]
");
// Filter by glob pattern
let output = work_dir.run_jj(["diff", "-s", "glob:file[12]"]);
insta::assert_snapshot!(output, @"
D file1
M file2
[EOF]
");
// Unmatched paths should generate warnings
let output = test_env.run_jj_in(
".",
[
"diff",
"-Rrepo",
"-s",
"repo", // matches directory
"repo/file1", // deleted in to_tree, but exists in from_tree
"repo/x",
"repo/y/z",
],
);
insta::assert_snapshot!(output.normalize_backslash(), @"
M repo/file2
R repo/{file1 => file3}
C repo/{file2 => file4}
[EOF]
------- stderr -------
Warning: No matching entries for paths: repo/x, repo/y/z
[EOF]
");
// Unmodified paths shouldn't generate warnings
let output = work_dir.run_jj(["diff", "-s", "--from=@", "file2"]);
insta::assert_snapshot!(output, @"");
// --tool=:<builtin>
let output = work_dir.run_jj(["diff", "--tool=:summary", "--color-words", "file2"]);
insta::assert_snapshot!(output, @"
M file2
Modified regular file file2:
1 1: 1
2 : 2
2: 5
3 3: 3
4 : 4
[EOF]
");
let output = work_dir.run_jj(["diff", "--tool=:git", "--stat", "file2"]);
insta::assert_snapshot!(output, @"
file2 | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/file2 b/file2
index 94ebaf9001..1ffc51b472 100644
--- a/file2
+++ b/file2
@@ -1,4 +1,3 @@
1
-2
+5
3
-4
[EOF]
");
// Bad combination
let output = work_dir.run_jj(["diff", "--summary", "--tool=:stat"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: --tool=:stat cannot be used with --summary
[EOF]
[exit status: 2]
");
let output = work_dir.run_jj(["diff", "--git", "--tool=:git"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: --tool=:git cannot be used with --git
[EOF]
[exit status: 2]
");
let output = work_dir.run_jj(["diff", "--git", "--tool=external"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: --tool=external cannot be used with --git
[EOF]
[exit status: 2]
");
let output = work_dir.run_jj(["diff", "-T''", "--summary"]);
insta::assert_snapshot!(output, @"
------- stderr -------
error: the argument '--template <TEMPLATE>' cannot be used with:
--summary
--stat
--types
--name-only
Usage: jj diff --template <TEMPLATE> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
let output = work_dir.run_jj(["diff", "-T''", "--git"]);
insta::assert_snapshot!(output, @"
------- stderr -------
error: the argument '--template <TEMPLATE>' cannot be used with:
--git
--color-words
Usage: jj diff --template <TEMPLATE> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
let output = work_dir.run_jj(["diff", "-T''", "--tool=:git"]);
insta::assert_snapshot!(output, @"
------- stderr -------
error: the argument '--template <TEMPLATE>' cannot be used with '--tool <TOOL>'
Usage: jj diff --template <TEMPLATE> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
// Bad builtin format
let output = work_dir.run_jj(["diff", "--config=ui.diff-formatter=:unknown"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Config error: Invalid type or value for ui.diff-formatter
Caused by: Invalid builtin diff format: unknown
For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`.
[EOF]
[exit status: 1]
");
}
#[test]
fn test_diff_empty() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "");
let output = work_dir.run_jj(["diff"]);
insta::assert_snapshot!(output, @"
Added regular file file1:
(empty)
[EOF]
");
work_dir.run_jj(["new"]).success();
work_dir.remove_file("file1");
let output = work_dir.run_jj(["diff"]);
insta::assert_snapshot!(output, @"
Removed regular file file1:
(empty)
[EOF]
");
let output = work_dir.run_jj(["diff", "--stat"]);
insta::assert_snapshot!(output, @"
file1 | 0
1 file changed, 0 insertions(+), 0 deletions(-)
[EOF]
");
}
#[test]
fn test_diff_file_mode() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Test content+mode/mode-only changes of empty/non-empty files:
// - file1: ("", x) -> ("2", n) empty, content+mode
// - file2: ("1", x) -> ("1", n) non-empty, mode-only
// - file3: ("1", n) -> ("2", x) non-empty, content+mode
// - file4: ("", n) -> ("", x) empty, mode-only
work_dir.write_file("file1", "");
work_dir.write_file("file2", "1\n");
work_dir.write_file("file3", "1\n");
work_dir.write_file("file4", "");
work_dir
.run_jj(["file", "chmod", "x", "file1", "file2"])
.success();
work_dir.run_jj(["new"]).success();
work_dir.write_file("file1", "2\n");
work_dir.write_file("file3", "2\n");
work_dir
.run_jj(["file", "chmod", "n", "file1", "file2"])
.success();
work_dir
.run_jj(["file", "chmod", "x", "file3", "file4"])
.success();
work_dir.run_jj(["new"]).success();
work_dir.remove_file("file1");
work_dir.remove_file("file2");
work_dir.remove_file("file3");
work_dir.remove_file("file4");
let output = work_dir.run_jj(["diff", "-r@--"]);
insta::assert_snapshot!(output, @"
Added executable file file1:
(empty)
Added executable file file2:
1: 1
Added regular file file3:
1: 1
Added regular file file4:
(empty)
[EOF]
");
let output = work_dir.run_jj(["diff", "-r@-"]);
insta::assert_snapshot!(output, @"
Executable file became non-executable at file1:
1: 2
Executable file became non-executable at file2:
Non-executable file became executable at file3:
1 : 1
1: 2
Non-executable file became executable at file4:
[EOF]
");
let output = work_dir.run_jj(["diff", "-r@"]);
insta::assert_snapshot!(output, @"
Removed regular file file1:
1 : 2
Removed regular file file2:
1 : 1
Removed executable file file3:
1 : 2
Removed executable file file4:
(empty)
[EOF]
");
let output = work_dir.run_jj(["diff", "-r@--", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
new file mode 100755
index 0000000000..e69de29bb2
diff --git a/file2 b/file2
new file mode 100755
index 0000000000..d00491fd7e
--- /dev/null
+++ b/file2
@@ -0,0 +1,1 @@
+1
diff --git a/file3 b/file3
new file mode 100644
index 0000000000..d00491fd7e
--- /dev/null
+++ b/file3
@@ -0,0 +1,1 @@
+1
diff --git a/file4 b/file4
new file mode 100644
index 0000000000..e69de29bb2
[EOF]
");
let output = work_dir.run_jj(["diff", "-r@-", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
old mode 100755
new mode 100644
index e69de29bb2..0cfbf08886
--- a/file1
+++ b/file1
@@ -0,0 +1,1 @@
+2
diff --git a/file2 b/file2
old mode 100755
new mode 100644
diff --git a/file3 b/file3
old mode 100644
new mode 100755
index d00491fd7e..0cfbf08886
--- a/file3
+++ b/file3
@@ -1,1 +1,1 @@
-1
+2
diff --git a/file4 b/file4
old mode 100644
new mode 100755
[EOF]
");
let output = work_dir.run_jj(["diff", "-r@", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
deleted file mode 100644
index 0cfbf08886..0000000000
--- a/file1
+++ /dev/null
@@ -1,1 +0,0 @@
-2
diff --git a/file2 b/file2
deleted file mode 100644
index d00491fd7e..0000000000
--- a/file2
+++ /dev/null
@@ -1,1 +0,0 @@
-1
diff --git a/file3 b/file3
deleted file mode 100755
index 0cfbf08886..0000000000
--- a/file3
+++ /dev/null
@@ -1,1 +0,0 @@
-2
diff --git a/file4 b/file4
deleted file mode 100755
index e69de29bb2..0000000000
[EOF]
");
}
#[test]
fn test_diff_types() -> TestResult {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let file_path = "foo";
// Missing
work_dir.run_jj(["new", "root()", "-m=missing"]).success();
// Normal file
work_dir.run_jj(["new", "root()", "-m=file"]).success();
work_dir.write_file(file_path, "foo");
// Conflict (add/add)
work_dir.run_jj(["new", "root()", "-m=conflict"]).success();
work_dir.write_file(file_path, "foo");
work_dir.run_jj(["new", "root()"]).success();
work_dir.write_file(file_path, "bar");
work_dir
.run_jj(["squash", "--into=subject(conflict)"])
.success();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
use std::path::PathBuf;
// Executable
work_dir
.run_jj(["new", "root()", "-m=executable"])
.success();
work_dir.write_file(file_path, "foo");
std::fs::set_permissions(
work_dir.root().join(file_path),
std::fs::Permissions::from_mode(0o755),
)?;
// Symlink
work_dir.run_jj(["new", "root()", "-m=symlink"]).success();
std::os::unix::fs::symlink(PathBuf::from("."), work_dir.root().join(file_path))?;
}
let diff = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--types",
&format!(r#"--from=subject("{from}")"#),
&format!(r#"--to=subject("{to}")"#),
])
};
insta::assert_snapshot!(diff("missing", "file"), @"
-F foo
[EOF]
");
insta::assert_snapshot!(diff("file", "conflict"), @"
FC foo
[EOF]
");
insta::assert_snapshot!(diff("conflict", "missing"), @"
C- foo
[EOF]
");
#[cfg(unix)]
{
insta::assert_snapshot!(diff("symlink", "file"), @"
LF foo
[EOF]
");
insta::assert_snapshot!(diff("missing", "executable"), @"
-F foo
[EOF]
");
}
Ok(())
}
#[test]
fn test_diff_name_only() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["new"]).success();
work_dir.write_file("deleted", "d");
work_dir.write_file("modified", "m");
insta::assert_snapshot!(work_dir.run_jj(["diff", "--name-only"]), @"
deleted
modified
[EOF]
");
work_dir.run_jj(["commit", "-mfirst"]).success();
work_dir.remove_file("deleted");
work_dir.write_file("modified", "mod");
work_dir.write_file("added", "add");
work_dir.create_dir("sub");
work_dir.write_file("sub/added", "sub/add");
insta::assert_snapshot!(work_dir.run_jj(["diff", "--name-only"]).normalize_backslash(), @"
added
deleted
modified
sub/added
[EOF]
");
}
#[test]
fn test_diff_renamed_file_and_dir() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("x", "content1");
work_dir.create_dir("y");
work_dir.write_file("y/file", "content2");
work_dir.run_jj(["new"]).success();
work_dir.remove_file("x");
work_dir.remove_dir_all("y");
work_dir.create_dir("x");
work_dir.write_file("x/file", "content2");
work_dir.write_file("y", "content1");
// Renamed directory y->x shouldn't be reported
let output = work_dir.run_jj(["diff", "--summary"]);
insta::assert_snapshot!(output.normalize_backslash(), @"
R {y => x}/file
R {x => y}
[EOF]
");
let output = work_dir.run_jj(["diff", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/y/file b/x/file
rename from y/file
rename to x/file
diff --git a/x b/y
rename from x
rename to y
[EOF]
");
}
#[test]
fn test_diff_bad_args() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["diff", "-s", "--types"]);
insta::assert_snapshot!(output, @"
------- stderr -------
error: the argument '--summary' cannot be used with '--types'
Usage: jj diff --summary [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
let output = work_dir.run_jj(["diff", "--color-words", "--git"]);
insta::assert_snapshot!(output, @"
------- stderr -------
error: the argument '--color-words' cannot be used with '--git'
Usage: jj diff --color-words [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
}
#[test]
fn test_diff_relative_paths() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.create_dir_all("dir1/subdir1");
work_dir.create_dir("dir2");
work_dir.write_file("file1", "foo1\n");
work_dir.write_file("dir1/file2", "foo2\n");
work_dir.write_file("dir1/subdir1/file3", "foo3\n");
work_dir.write_file("dir2/file4", "foo4\n");
work_dir.run_jj(["new"]).success();
work_dir.write_file("file1", "bar1\n");
work_dir.write_file("dir1/file2", "bar2\n");
work_dir.write_file("dir1/subdir1/file3", "bar3\n");
work_dir.write_file("dir2/file4", "bar4\n");
let sub_dir1 = work_dir.dir("dir1");
let output = sub_dir1.run_jj(["diff"]);
#[cfg(unix)]
insta::assert_snapshot!(output, @"
Modified regular file file2:
1 : foo2
1: bar2
Modified regular file subdir1/file3:
1 : foo3
1: bar3
Modified regular file ../dir2/file4:
1 : foo4
1: bar4
Modified regular file ../file1:
1 : foo1
1: bar1
[EOF]
");
#[cfg(windows)]
insta::assert_snapshot!(output, @r"
Modified regular file file2:
1 : foo2
1: bar2
Modified regular file subdir1\file3:
1 : foo3
1: bar3
Modified regular file ..\dir2\file4:
1 : foo4
1: bar4
Modified regular file ..\file1:
1 : foo1
1: bar1
[EOF]
");
let output = sub_dir1.run_jj(["diff", "-s"]);
#[cfg(unix)]
insta::assert_snapshot!(output, @"
M file2
M subdir1/file3
M ../dir2/file4
M ../file1
[EOF]
");
#[cfg(windows)]
insta::assert_snapshot!(output, @r"
M file2
M subdir1\file3
M ..\dir2\file4
M ..\file1
[EOF]
");
let output = sub_dir1.run_jj(["diff", "--types"]);
#[cfg(unix)]
insta::assert_snapshot!(output, @"
FF file2
FF subdir1/file3
FF ../dir2/file4
FF ../file1
[EOF]
");
#[cfg(windows)]
insta::assert_snapshot!(output, @r"
FF file2
FF subdir1\file3
FF ..\dir2\file4
FF ..\file1
[EOF]
");
let output = sub_dir1.run_jj(["diff", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/dir1/file2 b/dir1/file2
index 54b060eee9..1fe912cdd8 100644
--- a/dir1/file2
+++ b/dir1/file2
@@ -1,1 +1,1 @@
-foo2
+bar2
diff --git a/dir1/subdir1/file3 b/dir1/subdir1/file3
index c1ec6c6f12..f3c8b75ec6 100644
--- a/dir1/subdir1/file3
+++ b/dir1/subdir1/file3
@@ -1,1 +1,1 @@
-foo3
+bar3
diff --git a/dir2/file4 b/dir2/file4
index a0016dbc4c..17375f7a12 100644
--- a/dir2/file4
+++ b/dir2/file4
@@ -1,1 +1,1 @@
-foo4
+bar4
diff --git a/file1 b/file1
index 1715acd6a5..05c4fe6772 100644
--- a/file1
+++ b/file1
@@ -1,1 +1,1 @@
-foo1
+bar1
[EOF]
");
let output = sub_dir1.run_jj(["diff", "--stat"]);
#[cfg(unix)]
insta::assert_snapshot!(output, @"
file2 | 2 +-
subdir1/file3 | 2 +-
../dir2/file4 | 2 +-
../file1 | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
[EOF]
");
#[cfg(windows)]
insta::assert_snapshot!(output, @r"
file2 | 2 +-
subdir1\file3 | 2 +-
..\dir2\file4 | 2 +-
..\file1 | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
[EOF]
");
}
#[test]
fn test_diff_hunks() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Test added, removed, inserted, and modified lines. The modified line
// contains unchanged words.
work_dir.write_file("file1", "");
work_dir.write_file("file2", "foo\n");
work_dir.write_file("file3", "foo\nbaz qux blah blah\n");
work_dir.run_jj(["new"]).success();
work_dir.write_file("file1", "foo\n");
work_dir.write_file("file2", "");
work_dir.write_file("file3", "foo\nbar\nbaz quux blah blah\n");
let output = work_dir.run_jj(["diff"]);
insta::assert_snapshot!(output, @"
Modified regular file file1:
1: foo
Modified regular file file2:
1 : foo
Modified regular file file3:
1 1: foo
2 : baz qux blah blah
2: bar
3: baz quux blah blah
[EOF]
");
let output = work_dir.run_jj(["diff", "--color=debug"]);
insta::assert_snapshot!(output, @"
<<diff color_words header::Modified regular file file1:>>
<<diff color_words:: >><<diff color_words added line_number:: 1>><<diff color_words::: >><<diff color_words added token::foo>>
<<diff color_words header::Modified regular file file2:>>
<<diff color_words removed line_number:: 1>><<diff color_words:: : >><<diff color_words removed token::foo>>
<<diff color_words header::Modified regular file file3:>>
<<diff color_words context removed line_number:: 1>><<diff color_words context:: >><<diff color_words context added line_number:: 1>><<diff color_words context::: foo>>
<<diff color_words:: >><<diff color_words added line_number:: 2>><<diff color_words::: >><<diff color_words added token::bar>>
<<diff color_words removed line_number:: 2>><<diff color_words:: >><<diff color_words added line_number:: 3>><<diff color_words::: baz >><<diff color_words removed token::qux>><<diff color_words added token::quux>><<diff color_words:: blah blah>>
[EOF]
");
let output = work_dir.run_jj(["diff", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
index e69de29bb2..257cc5642c 100644
--- a/file1
+++ b/file1
@@ -0,0 +1,1 @@
+foo
diff --git a/file2 b/file2
index 257cc5642c..e69de29bb2 100644
--- a/file2
+++ b/file2
@@ -1,1 +0,0 @@
-foo
diff --git a/file3 b/file3
index 221a95a095..a543ef3892 100644
--- a/file3
+++ b/file3
@@ -1,2 +1,3 @@
foo
-baz qux blah blah
+bar
+baz quux blah blah
[EOF]
");
let output = work_dir.run_jj(["diff", "--git", "--color=debug"]);
insta::assert_snapshot!(output, @"
<<diff git file_header::diff --git a/file1 b/file1>>
<<diff git file_header::index e69de29bb2..257cc5642c 100644>>
<<diff git file_header::--- a/file1>>
<<diff git file_header::+++ b/file1>>
<<diff git hunk_header::@@ -0,0 +1,1 @@>>
<<diff git added::+>><<diff git added token::foo>>
<<diff git file_header::diff --git a/file2 b/file2>>
<<diff git file_header::index 257cc5642c..e69de29bb2 100644>>
<<diff git file_header::--- a/file2>>
<<diff git file_header::+++ b/file2>>
<<diff git hunk_header::@@ -1,1 +0,0 @@>>
<<diff git removed::->><<diff git removed token::foo>>
<<diff git file_header::diff --git a/file3 b/file3>>
<<diff git file_header::index 221a95a095..a543ef3892 100644>>
<<diff git file_header::--- a/file3>>
<<diff git file_header::+++ b/file3>>
<<diff git hunk_header::@@ -1,2 +1,3 @@>>
<<diff git context:: foo>>
<<diff git removed::-baz >><<diff git removed token::qux>><<diff git removed:: blah blah>>
<<diff git added::+>><<diff git added token::bar>>
<<diff git added::+baz >><<diff git added token::quux>><<diff git added:: blah blah>>
[EOF]
");
}
#[test]
fn test_diff_color_words_inlining_threshold() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let render_diff = |max_alternation: i32, args: &[&str]| {
let config = format!("diff.color-words.max-inline-alternation={max_alternation}");
work_dir.run_jj_with(|cmd| cmd.args(["diff", "--config", &config]).args(args))
};
let render_color_diff = |max_alternation| {
render_diff(max_alternation, &["--color=always"])
.normalize_stdout_with(strip_ansi_escape_codes)
};
let file1_path = "file1-single-line";
let file2_path = "file2-multiple-lines-in-single-hunk";
let file3_path = "file3-changes-across-lines";
work_dir.write_file(
file1_path,
indoc! {"
== adds ==
a b c
== removes ==
a b c d e f g
== adds + removes ==
a b c d e
== adds + removes + adds ==
a b c d e
== adds + removes + adds + removes ==
a b c d e f g
"},
);
work_dir.write_file(
file2_path,
indoc! {"
== adds; removes; adds + removes ==
a b c
a b c d e f g
a b c d e
== adds + removes + adds; adds + removes + adds + removes ==
a b c d e
a b c d e f g
"},
);
work_dir.write_file(
file3_path,
indoc! {"
== adds ==
a b c
== removes ==
a b c d
e f g
== adds + removes ==
a b c
d e
== adds + removes + adds ==
a b c
d e
== adds + removes + adds + removes ==
a b
c d e f g
"},
);
work_dir.run_jj(["new"]).success();
work_dir.write_file(
file1_path,
indoc! {"
== adds ==
a X b Y Z c
== removes ==
a c f
== adds + removes ==
a X b d
== adds + removes + adds ==
a X b d Y
== adds + removes + adds + removes ==
X a Y b d Z e
"},
);
work_dir.write_file(
file2_path,
indoc! {"
== adds; removes; adds + removes ==
a X b Y Z c
a c f
a X b d
== adds + removes + adds; adds + removes + adds + removes ==
a X b d Y
X a Y b d Z e
"},
);
work_dir.write_file(
file3_path,
indoc! {"
== adds ==
a X b
Y Z c
== removes ==
a c f
== adds + removes ==
a
X b d
== adds + removes + adds ==
a X b d
Y
== adds + removes + adds + removes ==
X a Y b d
Z e
"},
);
// default
let output = work_dir
.run_jj(["diff", "--color=always"])
.normalize_stdout_with(strip_ansi_escape_codes);
insta::assert_snapshot!(output, @"
Modified regular file file1-single-line:
1 1: == adds ==
2 2: a X b Y Z c
3 3: == removes ==
4 4: a b c d e f g
5 5: == adds + removes ==
6 6: a X b c d e
7 7: == adds + removes + adds ==
8 8: a X b c d eY
9 9: == adds + removes + adds + removes ==
10 : a b c d e f g
10: X a Y b d Z e
Modified regular file file2-multiple-lines-in-single-hunk:
1 1: == adds; removes; adds + removes ==
2 2: a X b Y Z c
3 3: a b c d e f g
4 4: a X b c d e
5 5: == adds + removes + adds; adds + removes + adds + removes ==
6 : a b c d e
7 : a b c d e f g
6: a X b d Y
7: X a Y b d Z e
Modified regular file file3-changes-across-lines:
1 1: == adds ==
2 2: a X b
2 3: Y Z c
3 4: == removes ==
4 5: a b c d
5 5: e f g
6 6: == adds + removes ==
7 7: a
7 8: X b c
8 8: d e
9 9: == adds + removes + adds ==
10 10: a X b c
11 10: d e
11 11: Y
12 12: == adds + removes + adds + removes ==
13 : a b
14 : c d e f g
13: X a Y b d
14: Z e
[EOF]
");
// -1: inline all
insta::assert_snapshot!(render_color_diff(-1), @"
Modified regular file file1-single-line:
1 1: == adds ==
2 2: a X b Y Z c
3 3: == removes ==
4 4: a b c d e f g
5 5: == adds + removes ==
6 6: a X b c d e
7 7: == adds + removes + adds ==
8 8: a X b c d eY
9 9: == adds + removes + adds + removes ==
10 10: X a Y b c d Z e f g
Modified regular file file2-multiple-lines-in-single-hunk:
1 1: == adds; removes; adds + removes ==
2 2: a X b Y Z c
3 3: a b c d e f g
4 4: a X b c d e
5 5: == adds + removes + adds; adds + removes + adds + removes ==
6 6: a X b c d eY
7 7: X a Y b c d Z e f g
Modified regular file file3-changes-across-lines:
1 1: == adds ==
2 2: a X b
2 3: Y Z c
3 4: == removes ==
4 5: a b c d
5 5: e f g
6 6: == adds + removes ==
7 7: a
7 8: X b c
8 8: d e
9 9: == adds + removes + adds ==
10 10: a X b c
11 10: d e
11 11: Y
12 12: == adds + removes + adds + removes ==
13 13: X a Y b
14 13: c d
14 14: Z e f g
[EOF]
");
// 0: no inlining
insta::assert_snapshot!(render_color_diff(0), @"
Modified regular file file1-single-line:
1 1: == adds ==
2 : a b c
2: a X b Y Z c
3 3: == removes ==
4 : a b c d e f g
4: a c f
5 5: == adds + removes ==
6 : a b c d e
6: a X b d
7 7: == adds + removes + adds ==
8 : a b c d e
8: a X b d Y
9 9: == adds + removes + adds + removes ==
10 : a b c d e f g
10: X a Y b d Z e
Modified regular file file2-multiple-lines-in-single-hunk:
1 1: == adds; removes; adds + removes ==
2 : a b c
3 : a b c d e f g
4 : a b c d e
2: a X b Y Z c
3: a c f
4: a X b d
5 5: == adds + removes + adds; adds + removes + adds + removes ==
6 : a b c d e
7 : a b c d e f g
6: a X b d Y
7: X a Y b d Z e
Modified regular file file3-changes-across-lines:
1 1: == adds ==
2 : a b c
2: a X b
3: Y Z c
3 4: == removes ==
4 : a b c d
5 : e f g
5: a c f
6 6: == adds + removes ==
7 : a b c
8 : d e
7: a
8: X b d
9 9: == adds + removes + adds ==
10 : a b c
11 : d e
10: a X b d
11: Y
12 12: == adds + removes + adds + removes ==
13 : a b
14 : c d e f g
13: X a Y b d
14: Z e
[EOF]
");
// 1: inline adds-only or removes-only lines
insta::assert_snapshot!(render_color_diff(1), @"
Modified regular file file1-single-line:
1 1: == adds ==
2 2: a X b Y Z c
3 3: == removes ==
4 4: a b c d e f g
5 5: == adds + removes ==
6 : a b c d e
6: a X b d
7 7: == adds + removes + adds ==
8 : a b c d e
8: a X b d Y
9 9: == adds + removes + adds + removes ==
10 : a b c d e f g
10: X a Y b d Z e
Modified regular file file2-multiple-lines-in-single-hunk:
1 1: == adds; removes; adds + removes ==
2 : a b c
3 : a b c d e f g
4 : a b c d e
2: a X b Y Z c
3: a c f
4: a X b d
5 5: == adds + removes + adds; adds + removes + adds + removes ==
6 : a b c d e
7 : a b c d e f g
6: a X b d Y
7: X a Y b d Z e
Modified regular file file3-changes-across-lines:
1 1: == adds ==
2 2: a X b
2 3: Y Z c
3 4: == removes ==
4 5: a b c d
5 5: e f g
6 6: == adds + removes ==
7 : a b c
8 : d e
7: a
8: X b d
9 9: == adds + removes + adds ==
10 : a b c
11 : d e
10: a X b d
11: Y
12 12: == adds + removes + adds + removes ==
13 : a b
14 : c d e f g
13: X a Y b d
14: Z e
[EOF]
");
// 2: inline up to adds + removes lines
insta::assert_snapshot!(render_color_diff(2), @"
Modified regular file file1-single-line:
1 1: == adds ==
2 2: a X b Y Z c
3 3: == removes ==
4 4: a b c d e f g
5 5: == adds + removes ==
6 6: a X b c d e
7 7: == adds + removes + adds ==
8 : a b c d e
8: a X b d Y
9 9: == adds + removes + adds + removes ==
10 : a b c d e f g
10: X a Y b d Z e
Modified regular file file2-multiple-lines-in-single-hunk:
1 1: == adds; removes; adds + removes ==
2 2: a X b Y Z c
3 3: a b c d e f g
4 4: a X b c d e
5 5: == adds + removes + adds; adds + removes + adds + removes ==
6 : a b c d e
7 : a b c d e f g
6: a X b d Y
7: X a Y b d Z e
Modified regular file file3-changes-across-lines:
1 1: == adds ==
2 2: a X b
2 3: Y Z c
3 4: == removes ==
4 5: a b c d
5 5: e f g
6 6: == adds + removes ==
7 7: a
7 8: X b c
8 8: d e
9 9: == adds + removes + adds ==
10 : a b c
11 : d e
10: a X b d
11: Y
12 12: == adds + removes + adds + removes ==
13 : a b
14 : c d e f g
13: X a Y b d
14: Z e
[EOF]
");
// 3: inline up to adds + removes + adds lines
insta::assert_snapshot!(render_color_diff(3), @"
Modified regular file file1-single-line:
1 1: == adds ==
2 2: a X b Y Z c
3 3: == removes ==
4 4: a b c d e f g
5 5: == adds + removes ==
6 6: a X b c d e
7 7: == adds + removes + adds ==
8 8: a X b c d eY
9 9: == adds + removes + adds + removes ==
10 : a b c d e f g
10: X a Y b d Z e
Modified regular file file2-multiple-lines-in-single-hunk:
1 1: == adds; removes; adds + removes ==
2 2: a X b Y Z c
3 3: a b c d e f g
4 4: a X b c d e
5 5: == adds + removes + adds; adds + removes + adds + removes ==
6 : a b c d e
7 : a b c d e f g
6: a X b d Y
7: X a Y b d Z e
Modified regular file file3-changes-across-lines:
1 1: == adds ==
2 2: a X b
2 3: Y Z c
3 4: == removes ==
4 5: a b c d
5 5: e f g
6 6: == adds + removes ==
7 7: a
7 8: X b c
8 8: d e
9 9: == adds + removes + adds ==
10 10: a X b c
11 10: d e
11 11: Y
12 12: == adds + removes + adds + removes ==
13 : a b
14 : c d e f g
13: X a Y b d
14: Z e
[EOF]
");
// 4: inline up to adds + removes + adds + removes lines
insta::assert_snapshot!(render_color_diff(4), @"
Modified regular file file1-single-line:
1 1: == adds ==
2 2: a X b Y Z c
3 3: == removes ==
4 4: a b c d e f g
5 5: == adds + removes ==
6 6: a X b c d e
7 7: == adds + removes + adds ==
8 8: a X b c d eY
9 9: == adds + removes + adds + removes ==
10 10: X a Y b c d Z e f g
Modified regular file file2-multiple-lines-in-single-hunk:
1 1: == adds; removes; adds + removes ==
2 2: a X b Y Z c
3 3: a b c d e f g
4 4: a X b c d e
5 5: == adds + removes + adds; adds + removes + adds + removes ==
6 6: a X b c d eY
7 7: X a Y b c d Z e f g
Modified regular file file3-changes-across-lines:
1 1: == adds ==
2 2: a X b
2 3: Y Z c
3 4: == removes ==
4 5: a b c d
5 5: e f g
6 6: == adds + removes ==
7 7: a
7 8: X b c
8 8: d e
9 9: == adds + removes + adds ==
10 10: a X b c
11 10: d e
11 11: Y
12 12: == adds + removes + adds + removes ==
13 13: X a Y b
14 13: c d
14 14: Z e f g
[EOF]
");
// context words in added/removed lines should be labeled as such
insta::assert_snapshot!(render_diff(2, &["--color=always"]), @"
Modified regular file file1-single-line:
 1  1: == adds ==
 2  2: a X b Y Z c
 3  3: == removes ==
 4  4: a b c d e f g
 5  5: == adds + removes ==
 6  6: a X b c d e
 7  7: == adds + removes + adds ==
 8 : a b c d e
 8: a X b d Y
 9  9: == adds + removes + adds + removes ==
 10 : a b c d e f g
 10: X a Y b d Z e
Modified regular file file2-multiple-lines-in-single-hunk:
 1  1: == adds; removes; adds + removes ==
 2  2: a X b Y Z c
 3  3: a b c d e f g
 4  4: a X b c d e
 5  5: == adds + removes + adds; adds + removes + adds + removes ==
 6 : a b c d e
 7 : a b c d e f g
 6: a X b d Y
 7: X a Y b d Z e
Modified regular file file3-changes-across-lines:
 1  1: == adds ==
 2  2: a X b
 2  3: Y Z c
 3  4: == removes ==
 4  5: a b c d
 5  5: e f g
 6  6: == adds + removes ==
 7  7: a
 7  8: X b c
 8  8: d e
 9  9: == adds + removes + adds ==
 10 : a b c
 11 : d e
 10: a X b d
 11: Y
 12  12: == adds + removes + adds + removes ==
 13 : a b
 14 : c d e f g
 13: X a Y b d
 14: Z e
[EOF]
");
insta::assert_snapshot!(render_diff(2, &["--color=debug"]), @"
<<diff color_words header::Modified regular file file1-single-line:>>
<<diff color_words context removed line_number:: 1>><<diff color_words context:: >><<diff color_words context added line_number:: 1>><<diff color_words context::: == adds ==>>
<<diff color_words removed line_number:: 2>><<diff color_words:: >><<diff color_words added line_number:: 2>><<diff color_words::: a >><<diff color_words added token::X >><<diff color_words::b >><<diff color_words added token::Y Z >><<diff color_words::c>>
<<diff color_words context removed line_number:: 3>><<diff color_words context:: >><<diff color_words context added line_number:: 3>><<diff color_words context::: == removes ==>>
<<diff color_words removed line_number:: 4>><<diff color_words:: >><<diff color_words added line_number:: 4>><<diff color_words::: a >><<diff color_words removed token::b >><<diff color_words::c >><<diff color_words removed token::d e >><<diff color_words::f>><<diff color_words removed token:: g>><<diff color_words::>>
<<diff color_words context removed line_number:: 5>><<diff color_words context:: >><<diff color_words context added line_number:: 5>><<diff color_words context::: == adds + removes ==>>
<<diff color_words removed line_number:: 6>><<diff color_words:: >><<diff color_words added line_number:: 6>><<diff color_words::: a >><<diff color_words added token::X >><<diff color_words::b >><<diff color_words removed token::c >><<diff color_words::d>><<diff color_words removed token:: e>><<diff color_words::>>
<<diff color_words context removed line_number:: 7>><<diff color_words context:: >><<diff color_words context added line_number:: 7>><<diff color_words context::: == adds + removes + adds ==>>
<<diff color_words removed line_number:: 8>><<diff color_words:: : >><<diff color_words removed::a b >><<diff color_words removed token::c >><<diff color_words removed::d >><<diff color_words removed token::e>><<diff color_words removed::>>
<<diff color_words:: >><<diff color_words added line_number:: 8>><<diff color_words::: >><<diff color_words added::a >><<diff color_words added token::X >><<diff color_words added::b d >><<diff color_words added token::Y>><<diff color_words added::>>
<<diff color_words context removed line_number:: 9>><<diff color_words context:: >><<diff color_words context added line_number:: 9>><<diff color_words context::: == adds + removes + adds + removes ==>>
<<diff color_words removed line_number:: 10>><<diff color_words:: : >><<diff color_words removed::a b >><<diff color_words removed token::c >><<diff color_words removed::d e>><<diff color_words removed token:: f g>><<diff color_words removed::>>
<<diff color_words:: >><<diff color_words added line_number:: 10>><<diff color_words::: >><<diff color_words added token::X >><<diff color_words added::a >><<diff color_words added token::Y >><<diff color_words added::b d >><<diff color_words added token::Z >><<diff color_words added::e>>
<<diff color_words header::Modified regular file file2-multiple-lines-in-single-hunk:>>
<<diff color_words context removed line_number:: 1>><<diff color_words context:: >><<diff color_words context added line_number:: 1>><<diff color_words context::: == adds; removes; adds + removes ==>>
<<diff color_words removed line_number:: 2>><<diff color_words:: >><<diff color_words added line_number:: 2>><<diff color_words::: a >><<diff color_words added token::X >><<diff color_words::b >><<diff color_words added token::Y Z >><<diff color_words::c>>
<<diff color_words removed line_number:: 3>><<diff color_words:: >><<diff color_words added line_number:: 3>><<diff color_words::: a >><<diff color_words removed token::b >><<diff color_words::c >><<diff color_words removed token::d e >><<diff color_words::f>><<diff color_words removed token:: g>><<diff color_words::>>
<<diff color_words removed line_number:: 4>><<diff color_words:: >><<diff color_words added line_number:: 4>><<diff color_words::: a >><<diff color_words added token::X >><<diff color_words::b >><<diff color_words removed token::c >><<diff color_words::d>><<diff color_words removed token:: e>><<diff color_words::>>
<<diff color_words context removed line_number:: 5>><<diff color_words context:: >><<diff color_words context added line_number:: 5>><<diff color_words context::: == adds + removes + adds; adds + removes + adds + removes ==>>
<<diff color_words removed line_number:: 6>><<diff color_words:: : >><<diff color_words removed::a b >><<diff color_words removed token::c >><<diff color_words removed::d >><<diff color_words removed token::e>><<diff color_words removed::>>
<<diff color_words removed line_number:: 7>><<diff color_words:: : >><<diff color_words removed::a b >><<diff color_words removed token::c >><<diff color_words removed::d e>><<diff color_words removed token:: f g>><<diff color_words removed::>>
<<diff color_words:: >><<diff color_words added line_number:: 6>><<diff color_words::: >><<diff color_words added::a >><<diff color_words added token::X >><<diff color_words added::b d >><<diff color_words added token::Y>><<diff color_words added::>>
<<diff color_words:: >><<diff color_words added line_number:: 7>><<diff color_words::: >><<diff color_words added token::X >><<diff color_words added::a >><<diff color_words added token::Y >><<diff color_words added::b d >><<diff color_words added token::Z >><<diff color_words added::e>>
<<diff color_words header::Modified regular file file3-changes-across-lines:>>
<<diff color_words context removed line_number:: 1>><<diff color_words context:: >><<diff color_words context added line_number:: 1>><<diff color_words context::: == adds ==>>
<<diff color_words removed line_number:: 2>><<diff color_words:: >><<diff color_words added line_number:: 2>><<diff color_words::: a >><<diff color_words added token::X >><<diff color_words::b>><<diff color_words added token::>>
<<diff color_words removed line_number:: 2>><<diff color_words:: >><<diff color_words added line_number:: 3>><<diff color_words::: >><<diff color_words added token::Y Z>><<diff color_words:: c>>
<<diff color_words context removed line_number:: 3>><<diff color_words context:: >><<diff color_words context added line_number:: 4>><<diff color_words context::: == removes ==>>
<<diff color_words removed line_number:: 4>><<diff color_words:: >><<diff color_words added line_number:: 5>><<diff color_words::: a >><<diff color_words removed token::b >><<diff color_words::c >><<diff color_words removed token::d>>
<<diff color_words removed line_number:: 5>><<diff color_words:: >><<diff color_words added line_number:: 5>><<diff color_words::: >><<diff color_words removed token::e >><<diff color_words::f>><<diff color_words removed token:: g>><<diff color_words::>>
<<diff color_words context removed line_number:: 6>><<diff color_words context:: >><<diff color_words context added line_number:: 6>><<diff color_words context::: == adds + removes ==>>
<<diff color_words removed line_number:: 7>><<diff color_words:: >><<diff color_words added line_number:: 7>><<diff color_words::: a>><<diff color_words added token::>>
<<diff color_words removed line_number:: 7>><<diff color_words:: >><<diff color_words added line_number:: 8>><<diff color_words::: >><<diff color_words added token::X>><<diff color_words:: b >><<diff color_words removed token::c>>
<<diff color_words removed line_number:: 8>><<diff color_words:: >><<diff color_words added line_number:: 8>><<diff color_words::: d>><<diff color_words removed token:: e>><<diff color_words::>>
<<diff color_words context removed line_number:: 9>><<diff color_words context:: >><<diff color_words context added line_number:: 9>><<diff color_words context::: == adds + removes + adds ==>>
<<diff color_words removed line_number:: 10>><<diff color_words:: : >><<diff color_words removed::a b >><<diff color_words removed token::c>>
<<diff color_words removed line_number:: 11>><<diff color_words:: : >><<diff color_words removed::d>><<diff color_words removed token:: e>><<diff color_words removed::>>
<<diff color_words:: >><<diff color_words added line_number:: 10>><<diff color_words::: >><<diff color_words added::a >><<diff color_words added token::X >><<diff color_words added::b d>><<diff color_words added token::>>
<<diff color_words:: >><<diff color_words added line_number:: 11>><<diff color_words::: >><<diff color_words added token::Y>><<diff color_words added::>>
<<diff color_words context removed line_number:: 12>><<diff color_words context:: >><<diff color_words context added line_number:: 12>><<diff color_words context::: == adds + removes + adds + removes ==>>
<<diff color_words removed line_number:: 13>><<diff color_words:: : >><<diff color_words removed::a b>><<diff color_words removed token::>>
<<diff color_words removed line_number:: 14>><<diff color_words:: : >><<diff color_words removed token::c>><<diff color_words removed:: d e>><<diff color_words removed token:: f g>><<diff color_words removed::>>
<<diff color_words:: >><<diff color_words added line_number:: 13>><<diff color_words::: >><<diff color_words added token::X >><<diff color_words added::a >><<diff color_words added token::Y >><<diff color_words added::b d>><<diff color_words added token::>>
<<diff color_words:: >><<diff color_words added line_number:: 14>><<diff color_words::: >><<diff color_words added token::Z>><<diff color_words added:: e>>
[EOF]
");
}
#[test]
fn test_diff_color_words_omit_blank_right_line() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// The middle hunk of file1 and file3 is
// left = " x\n..."
// right = "\n y\nz "
//
// file2 is different because left/right sides have the same number of "\n".
work_dir.write_file(
"file1",
indoc! {"
a x
b
"},
);
work_dir.write_file(
"file2",
indoc! {"
a x
b
"},
);
work_dir.write_file(
"file3",
indoc! {"
a x
b
"},
);
work_dir.run_jj(["new"]).success();
work_dir.write_file(
"file1",
indoc! {"
a
y
z b
"},
);
work_dir.write_file(
"file2",
indoc! {"
a
y
z b
"},
);
work_dir.write_file(
"file3",
indoc! {"
a
y
z b
"},
);
let output = work_dir
.run_jj(["diff", "--color=always"])
.normalize_stdout_with(strip_ansi_escape_codes);
insta::assert_snapshot!(output, @"
Modified regular file file1:
1 1: a x
2: y
2 3: z b
Modified regular file file2:
1 1: a x
2 2: y
3 3: z b
Modified regular file file3:
1 1: a x
2 :
3 :
2: y
4 3: z b
[EOF]
");
}
#[test]
fn test_diff_missing_newline() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "foo");
work_dir.write_file("file2", "foo\nbar");
work_dir.run_jj(["new"]).success();
work_dir.write_file("file1", "foo\nbar");
work_dir.write_file("file2", "foo");
let output = work_dir.run_jj(["diff"]);
insta::assert_snapshot!(output, @"
Modified regular file file1:
1 : foo
1: foo
2: bar
Modified regular file file2:
1 : foo
2 : bar
1: foo
[EOF]
");
let output = work_dir.run_jj(["diff", "--git"]);
insta::assert_snapshot!(output, @r"
diff --git a/file1 b/file1
index 1910281566..a907ec3f43 100644
--- a/file1
+++ b/file1
@@ -1,1 +1,2 @@
-foo
\ No newline at end of file
+foo
+bar
\ No newline at end of file
diff --git a/file2 b/file2
index a907ec3f43..1910281566 100644
--- a/file2
+++ b/file2
@@ -1,2 +1,1 @@
-foo
-bar
\ No newline at end of file
+foo
\ No newline at end of file
[EOF]
");
let output = work_dir.run_jj(["diff", "--stat"]);
insta::assert_snapshot!(output, @"
file1 | 3 ++-
file2 | 3 +--
2 files changed, 3 insertions(+), 3 deletions(-)
[EOF]
");
}
#[test]
fn test_color_words_diff_missing_newline() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "");
work_dir.run_jj(["commit", "-m", "=== Empty"]).success();
work_dir.write_file("file1", "a\nb\nc\nd\ne\nf\ng\nh\ni");
work_dir
.run_jj(["commit", "-m", "=== Add no newline"])
.success();
work_dir.write_file("file1", "A\nb\nc\nd\ne\nf\ng\nh\ni");
work_dir
.run_jj(["commit", "-m", "=== Modify first line"])
.success();
work_dir.write_file("file1", "A\nb\nc\nd\nE\nf\ng\nh\ni");
work_dir
.run_jj(["commit", "-m", "=== Modify middle line"])
.success();
work_dir.write_file("file1", "A\nb\nc\nd\nE\nf\ng\nh\nI");
work_dir
.run_jj(["commit", "-m", "=== Modify last line"])
.success();
work_dir.write_file("file1", "A\nb\nc\nd\nE\nf\ng\nh\nI\n");
work_dir
.run_jj(["commit", "-m", "=== Append newline"])
.success();
work_dir.write_file("file1", "A\nb\nc\nd\nE\nf\ng\nh\nI");
work_dir
.run_jj(["commit", "-m", "=== Remove newline"])
.success();
work_dir.write_file("file1", "");
work_dir.run_jj(["commit", "-m", "=== Empty"]).success();
let output = work_dir
.run_jj([
"log",
"-Tdescription",
"-pr::@-",
"--no-graph",
"--reversed",
"--color=always",
])
.normalize_stdout_with(strip_ansi_escape_codes);
insta::assert_snapshot!(output, @"
=== Empty
Added regular file file1:
(empty)
=== Add no newline
Modified regular file file1:
1: a
2: b
3: c
4: d
5: e
6: f
7: g
8: h
9: i
=== Modify first line
Modified regular file file1:
1 1: aA
2 2: b
3 3: c
4 4: d
...
=== Modify middle line
Modified regular file file1:
1 1: A
2 2: b
3 3: c
4 4: d
5 5: eE
6 6: f
7 7: g
8 8: h
9 9: i
=== Modify last line
Modified regular file file1:
...
6 6: f
7 7: g
8 8: h
9 9: iI
=== Append newline
Modified regular file file1:
...
6 6: f
7 7: g
8 8: h
9 9: I
=== Remove newline
Modified regular file file1:
...
6 6: f
7 7: g
8 8: h
9 9: I
=== Empty
Modified regular file file1:
1 : A
2 : b
3 : c
4 : d
5 : E
6 : f
7 : g
8 : h
9 : I
[EOF]
");
let output = work_dir
.run_jj([
"log",
"--config=diff.color-words.max-inline-alternation=0",
"-Tdescription",
"-pr::@-",
"--no-graph",
"--reversed",
"--color=always",
])
.normalize_stdout_with(strip_ansi_escape_codes);
insta::assert_snapshot!(output, @"
=== Empty
Added regular file file1:
(empty)
=== Add no newline
Modified regular file file1:
1: a
2: b
3: c
4: d
5: e
6: f
7: g
8: h
9: i
=== Modify first line
Modified regular file file1:
1 : a
1: A
2 2: b
3 3: c
4 4: d
...
=== Modify middle line
Modified regular file file1:
1 1: A
2 2: b
3 3: c
4 4: d
5 : e
5: E
6 6: f
7 7: g
8 8: h
9 9: i
=== Modify last line
Modified regular file file1:
...
6 6: f
7 7: g
8 8: h
9 : i
9: I
=== Append newline
Modified regular file file1:
...
6 6: f
7 7: g
8 8: h
9 : I
9: I
=== Remove newline
Modified regular file file1:
...
6 6: f
7 7: g
8 8: h
9 : I
9: I
=== Empty
Modified regular file file1:
1 : A
2 : b
3 : c
4 : d
5 : E
6 : f
7 : g
8 : h
9 : I
[EOF]
");
}
#[test]
fn test_diff_ignore_whitespace() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file(
"file1",
indoc! {"
foo {
bar;
}
baz {}
"},
);
work_dir.run_jj(["new", "-minsert whitespace"]).success();
work_dir.write_file(
"file1",
indoc! {"
foo {
bar;
}
baz { }
"},
);
work_dir.run_jj(["new", "-mindent"]).success();
work_dir.write_file(
"file1",
indoc! {"
{
foo {
bar;
}
}
baz { }
"},
);
// Git diff as reference output
let output = work_dir.run_jj(["diff", "-r@-", "--git", "--ignore-all-space"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
index f532aa68ad..d33445991b 100644
--- a/file1
+++ b/file1
[EOF]
");
let output = work_dir.run_jj(["diff", "--from=@--", "--git", "--ignore-all-space"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
index f532aa68ad..033c4a6168 100644
--- a/file1
+++ b/file1
@@ -1,4 +1,6 @@
+{
foo {
bar;
}
+}
baz { }
[EOF]
");
let output = work_dir.run_jj(["diff", "--from=@--", "--git", "--ignore-space-change"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
index f532aa68ad..033c4a6168 100644
--- a/file1
+++ b/file1
@@ -1,4 +1,6 @@
-foo {
+{
+ foo {
bar;
+ }
}
-baz {}
+baz { }
[EOF]
");
// Diff-stat should respects the whitespace options
let output = work_dir.run_jj(["diff", "-r@-", "--stat", "--ignore-all-space"]);
insta::assert_snapshot!(output, @"
file1 | 0
1 file changed, 0 insertions(+), 0 deletions(-)
[EOF]
");
let output = work_dir.run_jj(["diff", "--from=@--", "--stat", "--ignore-all-space"]);
insta::assert_snapshot!(output, @"
file1 | 2 ++
1 file changed, 2 insertions(+), 0 deletions(-)
[EOF]
");
let output = work_dir.run_jj(["diff", "--from=@--", "--stat", "--ignore-space-change"]);
insta::assert_snapshot!(output, @"
file1 | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
[EOF]
");
// "..." is printed if contents differ only in whitespace
let output = work_dir.run_jj(["diff", "-r@-", "--color=always", "--ignore-all-space"]);
insta::assert_snapshot!(output, @"
Modified regular file file1:
...
[EOF]
");
// Word-level changes are still highlighted
let output = work_dir.run_jj(["diff", "--from=@--", "--color=always", "--ignore-all-space"]);
insta::assert_snapshot!(output, @"
Modified regular file file1:
 1: {
 1  2:  foo {
 2  3:  bar;
 3  4:  }
 5: }
 4  6: baz { }
[EOF]
");
let output = work_dir.run_jj([
"diff",
"--from=@--",
"--color=always",
"--ignore-space-change",
]);
insta::assert_snapshot!(output, @"
Modified regular file file1:
 1: {
 1  2:  foo {
 2  3:  bar;
 4:  }
 3  5: }
 4  6: baz { }
[EOF]
");
}
#[test]
fn test_diff_skipped_context() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj");
work_dir
.run_jj(["describe", "-m", "=== Left side of diffs"])
.success();
work_dir
.run_jj(["new", "@", "-m", "=== Must skip 2 lines"])
.success();
work_dir.write_file("file1", "A\nb\nc\nd\ne\nf\ng\nh\ni\nJ");
work_dir
.run_jj(["new", "@-", "-m", "=== Don't skip 1 line"])
.success();
work_dir.write_file("file1", "A\nb\nc\nd\ne\nf\ng\nh\nI\nj");
work_dir
.run_jj(["new", "@-", "-m", "=== No gap to skip"])
.success();
work_dir.write_file("file1", "a\nB\nc\nd\ne\nf\ng\nh\nI\nj");
work_dir
.run_jj(["new", "@-", "-m", "=== No gap to skip"])
.success();
work_dir.write_file("file1", "a\nb\nC\nd\ne\nf\ng\nh\nI\nj");
work_dir
.run_jj(["new", "@-", "-m", "=== 1 line at start"])
.success();
work_dir.write_file("file1", "a\nb\nc\nd\nE\nf\ng\nh\ni\nj");
work_dir
.run_jj(["new", "@-", "-m", "=== 1 line at end"])
.success();
work_dir.write_file("file1", "a\nb\nc\nd\ne\nF\ng\nh\ni\nj");
let output = work_dir.run_jj(["log", "-Tdescription", "-p", "--no-graph", "--reversed"]);
insta::assert_snapshot!(output, @"
=== Left side of diffs
Added regular file file1:
1: a
2: b
3: c
4: d
5: e
6: f
7: g
8: h
9: i
10: j
=== Must skip 2 lines
Modified regular file file1:
1 : a
1: A
2 2: b
3 3: c
4 4: d
...
7 7: g
8 8: h
9 9: i
10 : j
10: J
=== Don't skip 1 line
Modified regular file file1:
1 : a
1: A
2 2: b
3 3: c
4 4: d
5 5: e
6 6: f
7 7: g
8 8: h
9 : i
9: I
10 10: j
=== No gap to skip
Modified regular file file1:
1 1: a
2 : b
2: B
3 3: c
4 4: d
5 5: e
6 6: f
7 7: g
8 8: h
9 : i
9: I
10 10: j
=== No gap to skip
Modified regular file file1:
1 1: a
2 2: b
3 : c
3: C
4 4: d
5 5: e
6 6: f
7 7: g
8 8: h
9 : i
9: I
10 10: j
=== 1 line at start
Modified regular file file1:
1 1: a
2 2: b
3 3: c
4 4: d
5 : e
5: E
6 6: f
7 7: g
8 8: h
...
=== 1 line at end
Modified regular file file1:
...
3 3: c
4 4: d
5 5: e
6 : f
6: F
7 7: g
8 8: h
9 9: i
10 10: j
[EOF]
");
}
#[test]
fn test_diff_skipped_context_from_settings_color_words() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
test_env.add_config(
r#"
[diff.color-words]
context = 0
"#,
);
work_dir.write_file("file1", "a\nb\nc\nd\ne");
work_dir
.run_jj(["describe", "-m", "=== First commit"])
.success();
work_dir
.run_jj(["new", "@", "-m", "=== Must show 0 context"])
.success();
work_dir.write_file("file1", "a\nb\nC\nd\ne");
let output = work_dir
.run_jj([
"log",
"-Tdescription",
"-p",
"--no-graph",
"--reversed",
"--color=always",
])
.normalize_stdout_with(strip_ansi_escape_codes);
insta::assert_snapshot!(output, @"
=== First commit
Added regular file file1:
1: a
2: b
3: c
4: d
5: e
=== Must show 0 context
Modified regular file file1:
...
3 3: cC
...
[EOF]
");
}
#[test]
fn test_diff_skipped_context_from_settings_git() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
test_env.add_config(
r#"
[diff.git]
context = 0
"#,
);
work_dir.write_file("file1", "a\nb\nc\nd\ne");
work_dir
.run_jj(["describe", "-m", "=== First commit"])
.success();
work_dir
.run_jj(["new", "@", "-m", "=== Must show 0 context"])
.success();
work_dir.write_file("file1", "a\nb\nC\nd\ne");
let output = work_dir.run_jj([
"log",
"-Tdescription",
"-p",
"--git",
"--no-graph",
"--reversed",
]);
insta::assert_snapshot!(output, @r"
=== First commit
diff --git a/file1 b/file1
new file mode 100644
index 0000000000..0fec236860
--- /dev/null
+++ b/file1
@@ -0,0 +1,5 @@
+a
+b
+c
+d
+e
\ No newline at end of file
=== Must show 0 context
diff --git a/file1 b/file1
index 0fec236860..b7615dae52 100644
--- a/file1
+++ b/file1
@@ -3,1 +3,1 @@
-c
+C
[EOF]
");
}
#[test]
fn test_diff_skipped_context_nondefault() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "a\nb\nc\nd");
work_dir
.run_jj(["describe", "-m", "=== Left side of diffs"])
.success();
work_dir
.run_jj(["new", "@", "-m", "=== Must skip 2 lines"])
.success();
work_dir.write_file("file1", "A\nb\nc\nD");
work_dir
.run_jj(["new", "@-", "-m", "=== Don't skip 1 line"])
.success();
work_dir.write_file("file1", "A\nb\nC\nd");
work_dir
.run_jj(["new", "@-", "-m", "=== No gap to skip"])
.success();
work_dir.write_file("file1", "a\nB\nC\nd");
work_dir
.run_jj(["new", "@-", "-m", "=== 1 line at start"])
.success();
work_dir.write_file("file1", "a\nB\nc\nd");
work_dir
.run_jj(["new", "@-", "-m", "=== 1 line at end"])
.success();
work_dir.write_file("file1", "a\nb\nC\nd");
let output = work_dir.run_jj([
"log",
"-Tdescription",
"-p",
"--no-graph",
"--reversed",
"--context=0",
]);
insta::assert_snapshot!(output, @"
=== Left side of diffs
Added regular file file1:
1: a
2: b
3: c
4: d
=== Must skip 2 lines
Modified regular file file1:
1 : a
1: A
...
4 : d
4: D
=== Don't skip 1 line
Modified regular file file1:
1 : a
1: A
2 2: b
3 : c
3: C
4 4: d
=== No gap to skip
Modified regular file file1:
1 1: a
2 : b
3 : c
2: B
3: C
4 4: d
=== 1 line at start
Modified regular file file1:
1 1: a
2 : b
2: B
...
=== 1 line at end
Modified regular file file1:
...
3 : c
3: C
4 4: d
[EOF]
");
}
#[test]
fn test_diff_leading_trailing_context() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// N=5 context lines at start/end of the file
work_dir.write_file("file1", "1\n2\n3\n4\n5\nL\n6\n7\n8\n9\n10\n11\n");
work_dir.run_jj(["new"]).success();
work_dir.write_file("file1", "1\n2\n3\n4\n5\n6\nR\n7\n8\n9\n10\n11\n");
// N=5 <= num_context_lines + 1: No room to skip.
let output = work_dir.run_jj(["diff", "--context=4"]);
insta::assert_snapshot!(output, @"
Modified regular file file1:
1 1: 1
2 2: 2
3 3: 3
4 4: 4
5 5: 5
6 : L
7 6: 6
7: R
8 8: 7
9 9: 8
10 10: 9
11 11: 10
12 12: 11
[EOF]
");
// N=5 <= 2 * num_context_lines + 1: The last hunk wouldn't be split if
// trailing diff existed.
let output = work_dir.run_jj(["diff", "--context=3"]);
insta::assert_snapshot!(output, @"
Modified regular file file1:
...
3 3: 3
4 4: 4
5 5: 5
6 : L
7 6: 6
7: R
8 8: 7
9 9: 8
10 10: 9
...
[EOF]
");
// N=5 > 2 * num_context_lines + 1: The last hunk should be split no matter
// if trailing diff existed.
let output = work_dir.run_jj(["diff", "--context=1"]);
insta::assert_snapshot!(output, @"
Modified regular file file1:
...
5 5: 5
6 : L
7 6: 6
7: R
8 8: 7
...
[EOF]
");
// N=5 <= num_context_lines: No room to skip.
let output = work_dir.run_jj(["diff", "--git", "--context=5"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
index 1bf57dee4a..69b3e1865c 100644
--- a/file1
+++ b/file1
@@ -1,12 +1,12 @@
1
2
3
4
5
-L
6
+R
7
8
9
10
11
[EOF]
");
// N=5 <= 2 * num_context_lines: The last hunk wouldn't be split if
// trailing diff existed.
let output = work_dir.run_jj(["diff", "--git", "--context=3"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
index 1bf57dee4a..69b3e1865c 100644
--- a/file1
+++ b/file1
@@ -3,8 +3,8 @@
3
4
5
-L
6
+R
7
8
9
[EOF]
");
// N=5 > 2 * num_context_lines: The last hunk should be split no matter
// if trailing diff existed.
let output = work_dir.run_jj(["diff", "--git", "--context=2"]);
insta::assert_snapshot!(output, @"
diff --git a/file1 b/file1
index 1bf57dee4a..69b3e1865c 100644
--- a/file1
+++ b/file1
@@ -4,6 +4,6 @@
4
5
-L
6
+R
7
8
[EOF]
");
}
#[test]
fn test_diff_conflict_sides_differ() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Based on test_materialize_conflict_basic() in test_conflicts.rs
let base_content = indoc! {"
line 1
line 2
line 3
line 4
line 5
"};
let left1_content = indoc! {"
line 1
line 2
left 3.1
left 3.2
left 3.3
line 4
line 5
"};
let left2_content = indoc! {"
left 1.1
line 2
left 3.1
left 3.2
left 3.3
left 3.4
line 4
line 5
"};
let right1_content = indoc! {"
line 1
line 2
right 3.1
line 4
line 5
"};
let right2_content = indoc! {"
line 1
line 2
right 3.1
line 4
"};
let create_content_commit = |name: &str, parents: &[&str], content: &str| {
create_commit_with_files(&work_dir, name, parents, &[("file", content)]);
};
create_content_commit("base", &[], base_content);
create_content_commit("left1", &["base"], left1_content);
create_content_commit("left2", &["left1"], left2_content);
create_content_commit("right1", &["base"], right1_content);
create_content_commit("right2", &["right1"], right2_content);
create_commit_with_files(&work_dir, "left1+right1", &["left1", "right1"], &[]);
create_commit_with_files(&work_dir, "left2+right2", &["left2", "right2"], &[]);
// Test the setup. left2+right2 can be considered a modified version of
// left1+right1.
work_dir.run_jj(["new", "root()"]).success();
insta::assert_snapshot!(work_dir.run_jj(["log", "-r~@"]), @"
× lylxulpl test.user@example.com 2001-02-03 08:05:20 left2+right2 eb31b480 (conflict)
├─╮ (empty) left2+right2
│ ○ nnkkpsqq test.user@example.com 2001-02-03 08:05:17 right2 423d82be
│ │ right2
○ │ ooyxmykx test.user@example.com 2001-02-03 08:05:13 left2 07184bf6
│ │ left2
│ │ × wmkuslsw test.user@example.com 2001-02-03 08:05:18 left1+right1 0921c844 (conflict)
╭─┬─╯ (empty) left1+right1
│ ○ truxwmqv test.user@example.com 2001-02-03 08:05:15 right1 9b436591
│ │ right1
○ │ psuskuln test.user@example.com 2001-02-03 08:05:11 left1 c3121775
├─╯ left1
○ ylvkpnrz test.user@example.com 2001-02-03 08:05:09 base 7953f024
│ base
◆ zzzzzzzz root() 00000000
[EOF]
");
// Diff of empty merge commit
let output = work_dir.run_jj(["diff", "--git", "-rleft1+right1"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["diff", "--color=always", "-rleft1+right1"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj([
"diff",
"--color=always",
"--config=diff.color-words.conflict=pair",
"-rleft1+right1",
]);
insta::assert_snapshot!(output, @"");
let diff_git_materialized = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--git",
"--context=1",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
let diff_color_words_materialized = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--color=always",
"--context=1",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
let diff_color_words_conflict_pair = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--color=always",
"--context=1",
"--config=diff.color-words.conflict=pair",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
// Diff from resolved to conflict
insta::assert_snapshot!(diff_git_materialized("base", "left1+right1"), @r#"
diff --git a/file b/file
index 94c99a3280..0000000000 100644
--- a/file
+++ b/file
@@ -2,3 +2,12 @@
line 2
-line 3
+<<<<<<< conflict 1 of 1
++++++++ psuskuln c3121775 "left1"
+left 3.1
+left 3.2
+left 3.3
+%%%%%%% diff from: ylvkpnrz 7953f024 "base"
+\\\\\\\ to: truxwmqv 9b436591 "right1"
+-line 3
++right 3.1
+>>>>>>> conflict 1 of 1 ends
line 4
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_materialized("base", "left1+right1"), @r#"
Created conflict in file:
 1  1: line 1
 2  2: line 2
 3: <<<<<<< conflict 1 of 1
 4: +++++++ psuskuln c3121775 "left1"
 5: left 3.1
 6: left 3.2
 7: left 3.3
 8: %%%%%%% diff from: ylvkpnrz 7953f024 "base"
 9: \\\\\\\ to: truxwmqv 9b436591 "right1"
 3  10: -line 3
 11: +right 3.1
 12: >>>>>>> conflict 1 of 1 ends
 4  13: line 4
 5  14: line 5
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_conflict_pair("base", "left1+right1"), @"
Created conflict in file:
 1  1: line 1
 2  2: line 2
<<<<<<< Created conflict
+++++++ left side #1 to right side #1
 3  3: lineleft 3.1
 4: left 3.2
 3  5: left 3.3
------- left side #1 to right base #1
 3  3: line 3
+++++++ left side #1 to right side #2
 3  3: lineright 3.1
>>>>>>> Conflict ends
 4  6: line 4
 5  7: line 5
[EOF]
");
// Diff from conflict to resolved
insta::assert_snapshot!(diff_git_materialized("left1+right1", "base"), @r#"
diff --git a/file b/file
index 0000000000..94c99a3280 100644
--- a/file
+++ b/file
@@ -2,12 +2,3 @@
line 2
-<<<<<<< conflict 1 of 1
-+++++++ psuskuln c3121775 "left1"
-left 3.1
-left 3.2
-left 3.3
-%%%%%%% diff from: ylvkpnrz 7953f024 "base"
-\\\\\\\ to: truxwmqv 9b436591 "right1"
--line 3
-+right 3.1
->>>>>>> conflict 1 of 1 ends
+line 3
line 4
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_materialized("left1+right1", "base"), @r#"
Resolved conflict in file:
 1  1: line 1
 2  2: line 2
 3 : <<<<<<< conflict 1 of 1
 4 : +++++++ psuskuln c3121775 "left1"
 5 : left 3.1
 6 : left 3.2
 7 : left 3.3
 8 : %%%%%%% diff from: ylvkpnrz 7953f024 "base"
 9 : \\\\\\\ to: truxwmqv 9b436591 "right1"
 10  3: -line 3
 11 : +right 3.1
 12 : >>>>>>> conflict 1 of 1 ends
 13  4: line 4
 14  5: line 5
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_conflict_pair("left1+right1", "base"), @"
Resolved conflict in file:
 1  1: line 1
 2  2: line 2
<<<<<<< Resolved conflict
+++++++ left side #1 to right side #1
 3  3: leftline 3.1
 4 : left 3.2
 5  3: left 3.3
------- left base #1 to right side #1
 3  3: line 3
+++++++ left side #2 to right side #1
 3  3: rightline 3.1
>>>>>>> Conflict ends
 6  4: line 4
 7  5: line 5
[EOF]
");
// Diff between conflicts
insta::assert_snapshot!(diff_git_materialized("left1+right1", "left2+right2"), @r#"
diff --git a/file b/file
--- a/file
+++ b/file
@@ -1,5 +1,5 @@
-line 1
+left 1.1
line 2
<<<<<<< conflict 1 of 1
-+++++++ psuskuln c3121775 "left1"
++++++++ ooyxmykx 07184bf6 "left2"
left 3.1
@@ -7,4 +7,5 @@
left 3.3
+left 3.4
%%%%%%% diff from: ylvkpnrz 7953f024 "base"
-\\\\\\\ to: truxwmqv 9b436591 "right1"
+\\\\\\\ to: nnkkpsqq 423d82be "right2"
-line 3
@@ -13,2 +14,1 @@
line 4
-line 5
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_materialized("left1+right1", "left2+right2"), @r#"
Modified conflict in file:
 1  1: lineleft 1.1
 2  2: line 2
 3  3: <<<<<<< conflict 1 of 1
 4 : +++++++ psuskuln c3121775 "left1"
 4: +++++++ ooyxmykx 07184bf6 "left2"
 5  5: left 3.1
 6  6: left 3.2
 7  7: left 3.3
 8: left 3.4
 8  9: %%%%%%% diff from: ylvkpnrz 7953f024 "base"
 9 : \\\\\\\ to: truxwmqv 9b436591 "right1"
 10: \\\\\\\ to: nnkkpsqq 423d82be "right2"
 10  11: -line 3
...
 13  14: line 4
 14 : line 5
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_conflict_pair("left1+right1", "left2+right2"), @"
Modified conflict in file:
 1  1: lineleft 1.1
 2  2: line 2
<<<<<<< Modified conflict
+++++++ left side #1 to right side #1
...
 5  5: left 3.3
 6: left 3.4
------- left base #1 to right base #1
 3  3: line 3
+++++++ left side #2 to right side #2
 3  3: right 3.1
>>>>>>> Conflict ends
 6  7: line 4
 7 : line 5
[EOF]
");
}
#[test]
fn test_diff_conflict_bases_differ() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Based on test_materialize_conflict_basic() in test_conflicts.rs
//
// - "line1" differs between L1-B1+R1 and L2-B2+R2
// - "line5" and "line5.1" are deleted cleanly at L1-B1+R1 and L2-B2+R2
// respectively
let base1_content = indoc! {"
line 1
line 2
line 3
line 4
line 5
"};
let base2_content = indoc! {"
line 2
line 3.1
line 3.2
line 4
line 5.1
"};
let left1_content = indoc! {"
line 1
line 2
left 3.1
left 3.2
left 3.3
line 4
line 5
"};
let left2_content = indoc! {"
line 2
left 3.1
left 3.2
left 3.3
line 4
line 5.1
"};
let right1_content = indoc! {"
line 1
line 2
right 3.1
line 4
"};
let right2_content = indoc! {"
line 2
right 3.1
line 4
"};
let create_content_commit = |name: &str, parents: &[&str], content: &str| {
create_commit_with_files(&work_dir, name, parents, &[("file", content)]);
};
create_content_commit("base1", &[], base1_content);
create_content_commit("left1", &["base1"], left1_content);
create_content_commit("right1", &["base1"], right1_content);
create_content_commit("base2", &["base1"], base2_content);
create_content_commit("left2", &["base2"], left2_content);
create_content_commit("right2", &["base2"], right2_content);
create_commit_with_files(&work_dir, "left1+right1", &["left1", "right1"], &[]);
create_commit_with_files(&work_dir, "left2+right2", &["left2", "right2"], &[]);
// Test the setup. left2+right2 can be considered a rebased version of
// left1+right1.
work_dir.run_jj(["new", "root()"]).success();
insta::assert_snapshot!(work_dir.run_jj(["log", "-r~@"]), @"
× ukmrtpmo test.user@example.com 2001-02-03 08:05:22 left2+right2 caa21817 (conflict)
├─╮ (empty) left2+right2
│ ○ wmkuslsw test.user@example.com 2001-02-03 08:05:19 right2 60c9bca0
│ │ right2
○ │ nnkkpsqq test.user@example.com 2001-02-03 08:05:17 left2 2ebb86f7
├─╯ left2
○ truxwmqv test.user@example.com 2001-02-03 08:05:15 base2 6d16668a
│ base2
× lylxulpl test.user@example.com 2001-02-03 08:05:20 left1+right1 8dd9a211 (conflict)
│ ├─╮ (empty) left1+right1
│ │ ○ ooyxmykx test.user@example.com 2001-02-03 08:05:13 right1 af53f139
├───╯ right1
│ ○ psuskuln test.user@example.com 2001-02-03 08:05:11 left1 dc799b14
├─╯ left1
○ ylvkpnrz test.user@example.com 2001-02-03 08:05:09 base1 492e331f
│ base1
◆ zzzzzzzz root() 00000000
[EOF]
");
let diff_git_materialized = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--git",
"--context=1",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
let diff_color_words_materialized = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--color=always",
"--context=1",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
let diff_color_words_conflict_pair = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--color=always",
"--context=1",
"--config=diff.color-words.conflict=pair",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
// Diff between conflicts
insta::assert_snapshot!(diff_git_materialized("left1+right1", "left2+right2"), @r#"
diff --git a/file b/file
--- a/file
+++ b/file
@@ -1,5 +1,4 @@
-line 1
line 2
<<<<<<< conflict 1 of 1
-+++++++ psuskuln dc799b14 "left1"
++++++++ nnkkpsqq 2ebb86f7 "left2"
left 3.1
@@ -7,5 +6,6 @@
left 3.3
-%%%%%%% diff from: ylvkpnrz 492e331f "base1"
-\\\\\\\ to: ooyxmykx af53f139 "right1"
--line 3
+%%%%%%% diff from: truxwmqv 6d16668a "base2"
+\\\\\\\ to: wmkuslsw 60c9bca0 "right2"
+-line 3.1
+-line 3.2
+right 3.1
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_materialized("left1+right1", "left2+right2"), @r#"
Modified conflict in file:
 1 : line 1
 2  1: line 2
 3  2: <<<<<<< conflict 1 of 1
 4 : +++++++ psuskuln dc799b14 "left1"
 3: +++++++ nnkkpsqq 2ebb86f7 "left2"
 5  4: left 3.1
 6  5: left 3.2
 7  6: left 3.3
 8 : %%%%%%% diff from: ylvkpnrz 492e331f "base1"
 9 : \\\\\\\ to: ooyxmykx af53f139 "right1"
 10 : -line 3
 7: %%%%%%% diff from: truxwmqv 6d16668a "base2"
 8: \\\\\\\ to: wmkuslsw 60c9bca0 "right2"
 9: -line 3.1
 10: -line 3.2
 11  11: +right 3.1
...
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_conflict_pair("left1+right1", "left2+right2"), @"
Modified conflict in file:
 1 : line 1
 2  1: line 2
<<<<<<< Modified conflict
+++++++ left side #1 to right side #1
...
------- left base #1 to right base #1
 3  2: line 3.1
 3  3: line 3.2
+++++++ left side #2 to right side #2
 3  2: right 3.1
>>>>>>> Conflict ends
 6  5: line 4
[EOF]
");
}
#[test]
fn test_diff_conflict_three_sides() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Based on test_materialize_conflict_three_sides() in test_conflicts.rs
let base1_content = indoc! {"
line 1
line 2 base
line 5
"};
let base2_content = indoc! {"
line 1
line 2 base
line 3 base
line 4 base
line 5
"};
let side1_content = indoc! {"
line 1
line 2 a.1
line 3 a.2
line 4 base
line 5
"};
let side2_content = indoc! {"
line 1
line 2 b.1
line 3 base
line 4 b.2
line 5
"};
let side3_content = indoc! {"
line 1
line 2 base
line 3 c.2
line 5
"};
let create_content_commit = |name: &str, parents: &[&str], content: &str| {
create_commit_with_files(&work_dir, name, parents, &[("file", content)]);
};
// S1 S2 S3
// B2
// B1
create_content_commit("base1", &[], base1_content);
create_content_commit("base2", &["base1"], base2_content);
create_content_commit("side1", &["base2"], side1_content);
create_content_commit("side2", &["base2"], side2_content);
create_content_commit("side3", &["base1"], side3_content);
create_commit_with_files(&work_dir, "side1+side2", &["side1", "side2"], &[]);
create_commit_with_files(
&work_dir,
"side1+side2+side3",
&["side1+side2", "side3"],
&[],
);
// Test the setup
work_dir.run_jj(["new", "root()"]).success();
insta::assert_snapshot!(work_dir.run_jj(["log", "-r~@"]), @"
× lylxulpl test.user@example.com 2001-02-03 08:05:20 side1+side2+side3 163eba3f (conflict)
├─╮ (empty) side1+side2+side3
│ ○ nnkkpsqq test.user@example.com 2001-02-03 08:05:17 side3 ddd87c7e
│ │ side3
× │ wmkuslsw test.user@example.com 2001-02-03 08:05:18 side1+side2 4a9fb965 (conflict)
├───╮ (empty) side1+side2
│ │ ○ truxwmqv test.user@example.com 2001-02-03 08:05:15 side2 b5249b46
│ │ │ side2
○ │ │ ooyxmykx test.user@example.com 2001-02-03 08:05:13 side1 a6f323a0
├───╯ side1
○ │ psuskuln test.user@example.com 2001-02-03 08:05:11 base2 d6dfffea
├─╯ base2
○ ylvkpnrz test.user@example.com 2001-02-03 08:05:09 base1 f0c1c076
│ base1
◆ zzzzzzzz root() 00000000
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj(["resolve", "-lrside1+side2"]), @"
file 2-sided conflict
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj(["resolve", "-lrside1+side2+side3"]), @"
file 3-sided conflict
[EOF]
");
let diff_git_materialized = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--git",
"--context=1",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
let diff_color_words_materialized = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--color=always",
"--context=1",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
let diff_color_words_conflict_pair = |from: &str, to: &str| {
work_dir.run_jj([
"diff",
"--color=always",
"--context=1",
"--config=diff.color-words.conflict=pair",
&format!("--from={from}"),
&format!("--to={to}"),
])
};
// Diff between conflicts
insta::assert_snapshot!(diff_git_materialized("side1+side2", "side1+side2+side3"), @r#"
diff --git a/file b/file
--- a/file
+++ b/file
@@ -13,2 +13,6 @@
line 4 b.2
+%%%%%%% diff from: ylvkpnrz f0c1c076 "base1"
+\\\\\\\ to: nnkkpsqq ddd87c7e "side3"
+ line 2 base
++line 3 c.2
>>>>>>> conflict 1 of 1 ends
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_materialized("side1+side2", "side1+side2+side3"), @r#"
Modified conflict in file:
...
 13  13: line 4 b.2
 14: %%%%%%% diff from: ylvkpnrz f0c1c076 "base1"
 15: \\\\\\\ to: nnkkpsqq ddd87c7e "side3"
 16:  line 2 base
 17: +line 3 c.2
 14  18: >>>>>>> conflict 1 of 1 ends
 15  19: line 5
[EOF]
"#);
insta::assert_snapshot!(diff_color_words_conflict_pair("side1+side2", "side1+side2+side3"), @"
Modified conflict in file:
 1  1: line 1
<<<<<<< Modified conflict
+++++++ left side #1 to right side #1
...
------- left base #1 to right base #1
...
+++++++ left side #2 to right side #2
...
------- left side #1 to right base #2
 2  2: line 2 a.1
 3 : line 3 a.2
 4  2: line 4 base
+++++++ left side #1 to right side #3
 2  2: line 2 a.1
 3 : line 3 a.2
 4  2: line 4 base
 3: line 3 c.2
>>>>>>> Conflict ends
 5  5: line 5
[EOF]
");
}
#[test]
fn test_diff_external_tool() -> TestResult {
let mut test_env = TestEnvironment::default();
let edit_script = test_env.set_up_fake_diff_editor();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "foo\n");
work_dir.write_file("file2", "foo\n");
work_dir.run_jj(["new"]).success();
work_dir.remove_file("file1");
work_dir.write_file("file2", "foo\nbar\n");
work_dir.write_file("file3", "foo\n");
// nonzero exit codes should print a warning
std::fs::write(&edit_script, "fail")?;
let output = work_dir.run_jj(["diff", "--config=ui.diff-formatter=fake-diff-editor"]);
let insta_portable_exit_status = {
let mut settings = insta::Settings::clone_current();
settings.add_filter("exit (status|code)", "<exit status>");
settings
};
insta_portable_exit_status.bind(|| {
insta::assert_snapshot!(output, @"
------- stderr -------
Warning: Tool exited with <exit status>: 1 (run with --debug to see the exact invocation)
[EOF]
");
});
// nonzero exit codes should not print a warning if it's an expected exit code
std::fs::write(&edit_script, "fail")?;
let output = work_dir.run_jj([
"diff",
"--tool",
"fake-diff-editor",
"--config=merge-tools.fake-diff-editor.diff-expected-exit-codes=[1]",
]);
insta::assert_snapshot!(output, @"");
std::fs::write(
&edit_script,
"print-files-before\0print --\0print-files-after",
)?;
// diff without file patterns
insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-diff-editor"]), @"
file1
file2
--
file2
file3
[EOF]
");
// diff with unset edit-args
insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-diff-editor",
"--config=merge-tools.fake-diff-editor.edit-args=[]",
]), @"
file1
file2
--
file2
file3
[EOF]
");
// diff with explicitly unset edit-args and diff-args
insta_portable_exit_status.bind(|| {
insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-diff-editor",
"--config=merge-tools.fake-diff-editor.edit-args=[]",
"--config=merge-tools.fake-diff-editor.diff-args=[]",
]), @"
------- stderr -------
Error: The tool `fake-diff-editor` cannot be used for diff formatting
[EOF]
[<exit status>: 2]
");
});
// diff with file patterns
insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-diff-editor", "file1"]), @"
file1
--
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj(["log", "-p", "--tool=fake-diff-editor"]), @"
@ ylvkpnrz test.user@example.com 2001-02-03 08:05:09 0027b000
│ (no description set)
│ file1
│ file2
│ --
│ file2
│ file3
○ qpvuntsm test.user@example.com 2001-02-03 08:05:08 74c18ac3
│ (no description set)
│ --
│ file1
│ file2
◆ zzzzzzzz root() 00000000
--
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj(["show", "--tool=fake-diff-editor"]), @"
Commit ID: 0027b0000da4d2d2e6a450203ad7bb3f2b5dc624
Change ID: ylvkpnrzqnoowoytxnquwvuryrwnrmlp
Author : Test User <test.user@example.com> (2001-02-03 08:05:09)
Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
(no description set)
file1
file2
--
file2
file3
[EOF]
");
// Enabled by default, looks up the merge-tools table
let config = "--config=ui.diff-formatter=fake-diff-editor";
insta::assert_snapshot!(work_dir.run_jj(["diff", config]), @"
file1
file2
--
file2
file3
[EOF]
");
// Inlined command arguments
let command_toml = to_toml_value(fake_diff_editor_path());
let config = format!("--config=ui.diff-formatter=[{command_toml}, '$right', '$left']");
insta::assert_snapshot!(work_dir.run_jj(["diff", &config]), @"
file2
file3
--
file1
file2
[EOF]
");
// Output of external diff tool shouldn't be escaped
std::fs::write(&edit_script, "print \x1b[1;31mred")?;
insta::assert_snapshot!(work_dir.run_jj(["diff", "--color=always", "--tool=fake-diff-editor"]),
@"
red
[EOF]
");
// Non-zero exit code isn't an error
std::fs::write(&edit_script, "print diff\0fail")?;
let output = work_dir.run_jj(["show", "--tool=fake-diff-editor"]);
insta::assert_snapshot!(output.normalize_stderr_exit_status(), @"
Commit ID: 0027b0000da4d2d2e6a450203ad7bb3f2b5dc624
Change ID: ylvkpnrzqnoowoytxnquwvuryrwnrmlp
Author : Test User <test.user@example.com> (2001-02-03 08:05:09)
Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
(no description set)
diff
[EOF]
------- stderr -------
Warning: Tool exited with exit status: 1 (run with --debug to see the exact invocation)
[EOF]
");
// --tool=:builtin shouldn't be ignored
let output = work_dir.run_jj(["diff", "--tool=:builtin"]);
insta::assert_snapshot!(output.strip_stderr_last_line(), @"
------- stderr -------
Error: Invalid builtin diff format: builtin
[EOF]
[exit status: 2]
");
Ok(())
}
#[test]
fn test_diff_do_chdir() -> TestResult {
let mut test_env = TestEnvironment::default();
test_env.set_up_fake_echo_merge_tool();
let edit_script = test_env.set_up_fake_diff_editor();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "file1\n");
std::fs::write(&edit_script, "print-current-dir")?;
assert_eq!(
work_dir
.run_jj([
"diff",
"--tool=fake-diff-editor",
"--config=merge-tools.fake-diff-editor.diff-do-chdir=false",
])
.to_string()
.lines()
.next()
.unwrap(),
"$TEST_ENV/repo"
);
assert!(
work_dir
.run_jj(["diff", "--tool=fake-diff-editor"])
.to_string()
.lines()
.next()
.unwrap()
!= "$TEST_ENV/repo"
);
insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-echo"]), @"
left right
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-echo",
"--config=merge-tools.fake-echo.diff-invocation-mode=file-by-file"]).normalize_backslash(), @"
left/file1 right/file1
[EOF]
");
Ok(())
}
#[test]
fn test_diff_external_available_width() {
let test_env = TestEnvironment::default();
test_env.set_up_fake_echo_merge_tool();
test_env.add_config("merge-tools.fake-echo.diff-args = ['$width']");
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "file1\n");
work_dir.run_jj(["new", "root()"]).success();
work_dir.write_file("file2", "file2\n");
// Directory diff
let output =
work_dir.run_jj_with(|cmd| cmd.args(["diff", "--tool=fake-echo"]).env("COLUMNS", "50"));
insta::assert_snapshot!(output, @"
50
[EOF]
");
// File-by-file diff
let output = work_dir.run_jj_with(|cmd| {
cmd.args(["diff", "--tool=fake-echo"])
.arg("--config=merge-tools.fake-echo.diff-invocation-mode=file-by-file")
.env("COLUMNS", "50")
});
insta::assert_snapshot!(output, @"
50
[EOF]
");
// Graph width should be subtracted
let output = work_dir.run_jj_with(|cmd| {
cmd.args(["log", "--tool=fake-echo", "-T''"])
.env("COLUMNS", "50")
});
insta::assert_snapshot!(output, @"
@ 47
│ ○ 45
├─╯
◆ 47
[EOF]
");
}
#[test]
fn test_diff_external_file_by_file_tool() -> TestResult {
let mut test_env = TestEnvironment::default();
let edit_script = test_env.set_up_fake_diff_editor();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "file1\n");
work_dir.write_file("file2", "file2\n");
work_dir.run_jj(["new"]).success();
work_dir.remove_file("file1");
work_dir.write_file("file2", "file2\nfile2\n");
work_dir.write_file("file3", "file3\n");
work_dir.write_file("file4", "file1\n");
std::fs::write(
edit_script,
"print ==\0print-files-before\0print --\0print-files-after",
)?;
// Enabled by default, looks up the merge-tools table
let configs: &[_] = &[
"--config=ui.diff-formatter=fake-diff-editor",
"--config=merge-tools.fake-diff-editor.diff-invocation-mode=file-by-file",
];
// diff without file patterns
insta::assert_snapshot!(work_dir.run_jj_with(|cmd| cmd.arg("diff").args(configs)), @"
==
file2
--
file2
==
file3
--
file3
==
file1
--
file4
[EOF]
");
// diff with file patterns
insta::assert_snapshot!(
work_dir.run_jj_with(|cmd| cmd.args(["diff", "file1"]).args(configs)), @"
==
file1
--
file1
[EOF]
");
insta::assert_snapshot!(
work_dir.run_jj_with(|cmd| cmd.args(["log", "-p"]).args(configs)), @"
@ ylvkpnrz test.user@example.com 2001-02-03 08:05:09 2eda6805
│ (no description set)
│ ==
│ file2
│ --
│ file2
│ ==
│ file3
│ --
│ file3
│ ==
│ file1
│ --
│ file4
○ qpvuntsm test.user@example.com 2001-02-03 08:05:08 923beb72
│ (no description set)
│ ==
│ file1
│ --
│ file1
│ ==
│ file2
│ --
│ file2
◆ zzzzzzzz root() 00000000
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj_with(|cmd| cmd.arg("show").args(configs)), @"
Commit ID: 2eda6805d636ec3da79dd50c890d5311c74ef8b5
Change ID: ylvkpnrzqnoowoytxnquwvuryrwnrmlp
Author : Test User <test.user@example.com> (2001-02-03 08:05:09)
Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
(no description set)
==
file2
--
file2
==
file3
--
file3
==
file1
--
file4
[EOF]
");
Ok(())
}
#[cfg(unix)]
#[test]
fn test_diff_external_tool_symlink() -> TestResult {
let mut test_env = TestEnvironment::default();
let edit_script = test_env.set_up_fake_diff_editor();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let external_file_path = test_env.env_root().join("external-file");
std::fs::write(&external_file_path, "")?;
let external_file_permissions = external_file_path.symlink_metadata()?.permissions();
std::os::unix::fs::symlink("non-existent1", work_dir.root().join("dead"))?;
std::os::unix::fs::symlink(&external_file_path, work_dir.root().join("file"))?;
work_dir.run_jj(["new"]).success();
work_dir.remove_file("dead");
std::os::unix::fs::symlink("non-existent2", work_dir.root().join("dead"))?;
work_dir.remove_file("file");
work_dir.write_file("file", "");
std::fs::write(
edit_script,
"print-files-before\0print --\0print-files-after",
)?;
// Shouldn't try to change permission of symlinks
insta::assert_snapshot!(work_dir.run_jj(["diff", "--tool=fake-diff-editor"]), @"
dead
file
--
dead
file
[EOF]
");
// External file should be intact
assert_eq!(
external_file_path.symlink_metadata()?.permissions(),
external_file_permissions
);
Ok(())
}
#[test]
fn test_diff_external_tool_conflict_marker_style() -> TestResult {
let mut test_env = TestEnvironment::default();
let edit_script = test_env.set_up_fake_diff_editor();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let file_path = "file";
// Create a conflict
work_dir.write_file(
file_path,
indoc! {"
line 1
line 2
line 3
line 4
line 5
"},
);
work_dir.run_jj(["commit", "-m", "base"]).success();
work_dir.write_file(
file_path,
indoc! {"
line 1
line 2.1
line 2.2
line 3
line 4.1
line 5
"},
);
work_dir.run_jj(["describe", "-m", "side-a"]).success();
work_dir
.run_jj(["new", "subject(base)", "-m", "side-b"])
.success();
work_dir.write_file(
file_path,
indoc! {"
line 1
line 2.3
line 3
line 4.2
line 4.3
line 5
"},
);
// Resolve one of the conflicts in the working copy
work_dir
.run_jj(["new", "subject(side-a)", "subject(side-b)"])
.success();
work_dir.write_file(
file_path,
indoc! {"
line 1
line 2.1
line 2.2
line 2.3
line 3
<<<<<<<
%%%%%%%
-line 4
+line 4.1
+++++++
line 4.2
line 4.3
>>>>>>>
line 5
"},
);
// Set up diff editor to use "snapshot" conflict markers
test_env.add_config(r#"merge-tools.fake-diff-editor.conflict-marker-style = "snapshot""#);
// We want to see whether the diff is using the correct conflict markers
std::fs::write(
&edit_script,
["files-before file", "files-after file", "dump file file"].join("\0"),
)?;
let output = work_dir.run_jj(["diff", "--tool", "fake-diff-editor"]);
insta::assert_snapshot!(output, @"");
// Conflicts should render using "snapshot" format
insta::assert_snapshot!(
std::fs::read_to_string(test_env.env_root().join("file"))?, @r#"
line 1
line 2.1
line 2.2
line 2.3
line 3
<<<<<<< conflict 1 of 1
+++++++ rlvkpnrz 74e448a1 "side-a"
line 4.1
------- qpvuntsm 9bd2e004 "base"
line 4
+++++++ osuskuln f12a16f8 "side-b"
line 4.2
line 4.3
>>>>>>> conflict 1 of 1 ends
line 5
"#);
Ok(())
}
#[test]
fn test_diff_stat() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "foo\n");
let output = work_dir.run_jj(["diff", "--stat"]);
insta::assert_snapshot!(output, @"
file1 | 1 +
1 file changed, 1 insertion(+), 0 deletions(-)
[EOF]
");
work_dir.run_jj(["new"]).success();
let output = work_dir.run_jj(["diff", "--stat"]);
insta::assert_snapshot!(output, @"
0 files changed, 0 insertions(+), 0 deletions(-)
[EOF]
");
work_dir.write_file("file1", "foo\nbar\n");
work_dir.run_jj(["new"]).success();
work_dir.write_file("file1", "bar\n");
let output = work_dir.run_jj(["diff", "--stat"]);
insta::assert_snapshot!(output, @"
file1 | 1 -
1 file changed, 0 insertions(+), 1 deletion(-)
[EOF]
");
}
#[test]
fn test_diff_stat_long_name_or_stat() {
let mut test_env = TestEnvironment::default();
test_env.add_env_var("COLUMNS", "30");
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let get_stat = |work_dir: &TestWorkDir, path_length: usize, stat_size: usize| {
work_dir.run_jj(["new", "root()"]).success();
let ascii_name = "1234567890".chars().cycle().take(path_length).join("");
let han_name = "一二三四五六七八九十"
.chars()
.cycle()
.take(path_length)
.join("");
let content = "content line\n".repeat(stat_size);
work_dir.write_file(ascii_name, &content);
work_dir.write_file(han_name, &content);
work_dir.run_jj(["diff", "--stat"])
};
insta::assert_snapshot!(get_stat(&work_dir, 1, 1), @"
1 | 1 +
一 | 1 +
2 files changed, 2 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 1, 10), @"
1 | 10 ++++++++++
一 | 10 ++++++++++
2 files changed, 20 insertions(+), 0 deletions(-)
[EOF]
");
// 30 column display width means right edge is
// ... here ->|
insta::assert_snapshot!(get_stat(&work_dir, 1, 100), @"
1 | 100 +++++++++++++++++++++
一 | 100 +++++++++++++++++++++
2 files changed, 200 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 10, 1), @"
1234567890 | 1 +
...四五六七八九十 | 1 +
2 files changed, 2 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 10, 10), @"
1234567890 | 10 ++++++++
...五六七八九十 | 10 ++++++++
2 files changed, 20 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 10, 100), @"
1234567890 | 100 +++++++
...五六七八九十 | 100 +++++++
2 files changed, 200 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 50, 1), @"
...78901234567890 | 1 +
...四五六七八九十 | 1 +
2 files changed, 2 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 50, 10), @"
...8901234567890 | 10 ++++++++
...五六七八九十 | 10 ++++++++
2 files changed, 20 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 50, 100), @"
...8901234567890 | 100 +++++++
...五六七八九十 | 100 +++++++
2 files changed, 200 insertions(+), 0 deletions(-)
[EOF]
");
// Lengths around where we introduce the ellipsis
// 30 column display width means right edge is
// ... here ->|
insta::assert_snapshot!(get_stat(&work_dir, 13, 100), @"
1234567890123 | 100 +++++++
...八九十一二三 | 100 +++++++
2 files changed, 200 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 14, 100), @"
12345678901234 | 100 +++++++
...九十一二三四 | 100 +++++++
2 files changed, 200 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 15, 100), @"
123456789012345 | 100 +++++++
...十一二三四五 | 100 +++++++
2 files changed, 200 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 16, 100), @"
1234567890123456 | 100 +++++++
...一二三四五六 | 100 +++++++
2 files changed, 200 insertions(+), 0 deletions(-)
[EOF]
");
// Very narrow terminal (doesn't have to fit, just don't crash)
test_env.add_env_var("COLUMNS", "10");
let work_dir = test_env.work_dir("repo");
insta::assert_snapshot!(get_stat(&work_dir, 10, 10), @"
... | 10 ++
... | 10 ++
2 files changed, 20 insertions(+), 0 deletions(-)
[EOF]
");
test_env.add_env_var("COLUMNS", "3");
let work_dir = test_env.work_dir("repo");
insta::assert_snapshot!(get_stat(&work_dir, 10, 10), @"
... | 10 ++
... | 10 ++
2 files changed, 20 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 3, 10), @"
123 | 10 ++
... | 10 ++
2 files changed, 20 insertions(+), 0 deletions(-)
[EOF]
");
insta::assert_snapshot!(get_stat(&work_dir, 1, 10), @"
1 | 10 +++
一 | 10 +++
2 files changed, 20 insertions(+), 0 deletions(-)
[EOF]
");
}
/// Verify that diff --stat always shows at least one `+` or `-` even when the
/// file is mostly the other.
#[test]
fn test_diff_stat_rounding() {
let mut test_env = TestEnvironment::default();
test_env.add_env_var("COLUMNS", "40");
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("mostly_adds.txt", b"x\n");
work_dir.write_file("mostly_removes.txt", b"x\n".repeat(300));
work_dir.run_jj(["new"]).success();
work_dir.write_file("mostly_adds.txt", b"y\n".repeat(100));
work_dir.write_file("mostly_removes.txt", b"y\n");
work_dir.write_file("only_adds.txt", b"y\n".repeat(10));
let output = work_dir.run_jj(["diff", "--stat"]);
insta::assert_snapshot!(output, @"
mostly_adds.txt | 101 ++++-
mostly_removes.txt | 301 +--------------
only_adds.txt | 10 +
3 files changed, 111 insertions(+), 301 deletions(-)
[EOF]
");
// very narrow terminal, with both adds and deletes
test_env.add_env_var("COLUMNS", "3");
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["diff", "--stat"]);
insta::assert_snapshot!(output, @"
.. | 101 +-
.. | 301 +-
.. | 10 +
3 files changed, 111 insertions(+), 301 deletions(-)
[EOF]
");
}
#[test]
fn test_diff_binary() {
let mut test_env = TestEnvironment::default();
test_env.add_env_var("COLUMNS", "40");
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("binary_removed.png", b"\x89PNG\r\n\x1a\nabcdefg\0");
work_dir.write_file("binary_modified.png", b"\x89PNG\r\n\x1a\n0123456\0");
work_dir.write_file("binary_modified_to_text.png", b"\x89PNG\r\n\x1a\n0123456\0");
work_dir.run_jj(["new"]).success();
work_dir.remove_file("binary_removed.png");
work_dir.write_file("binary_modified.png", b"\x89PNG\r\n\x1a\n012345x\0");
// this file's contents became a valid text file
work_dir.write_file("binary_modified_to_text.png", "foo\nbar\n");
work_dir.write_file("binary_added.png", b"\x89PNG\r\n\x1a\nxyz\0");
// try a file that's valid UTF-8 but contains control characters
work_dir.write_file("binary_valid_utf8.png", b"\0\0\0");
let output = work_dir.run_jj(["diff"]);
insta::assert_snapshot!(output, @"
Added regular file binary_added.png:
(binary)
Modified regular file binary_modified.png:
(binary)
Modified regular file binary_modified_to_text.png:
(binary)
Removed regular file binary_removed.png:
(binary)
Added regular file binary_valid_utf8.png:
(binary)
[EOF]
");
let output = work_dir.run_jj(["diff", "--git"]);
insta::assert_snapshot!(output, @"
diff --git a/binary_added.png b/binary_added.png
new file mode 100644
index 0000000000..deacfbc286
Binary files /dev/null and b/binary_added.png differ
diff --git a/binary_modified.png b/binary_modified.png
index 7f036ce788..f666e11aeb 100644
Binary files a/binary_modified.png and b/binary_modified.png differ
diff --git a/binary_modified_to_text.png b/binary_modified_to_text.png
index 7f036ce788..3bd1f0e297 100644
Binary files a/binary_modified_to_text.png and b/binary_modified_to_text.png differ
diff --git a/binary_removed.png b/binary_removed.png
deleted file mode 100644
index 2b65b23c22..0000000000
Binary files a/binary_removed.png and /dev/null differ
diff --git a/binary_valid_utf8.png b/binary_valid_utf8.png
new file mode 100644
index 0000000000..4227ca4e87
Binary files /dev/null and b/binary_valid_utf8.png differ
[EOF]
");
let output = work_dir.run_jj(["diff", "--stat"]);
// Rightmost display column ->|
insta::assert_snapshot!(output, @"
binary_added.png | (binary) +12 bytes
binary_modified.png | (binary)
...fied_to_text.png | (binary) -8 bytes
binary_removed.png | (binary) -16 bytes
...y_valid_utf8.png | (binary) +3 bytes
5 files changed, 0 insertions(+), 0 deletions(-)
[EOF]
");
}
/// Test diff --stat output width for diffs that have different cases of right
/// side text: solely "(binary)", a mixture of text and binary diffs, and binary
/// size changes.
#[test]
fn test_diff_stat_binary_and_text() {
let mut test_env = TestEnvironment::default();
test_env.add_env_var("COLUMNS", "40");
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("binary_with_elided_long_file_name.png", b"\x001");
work_dir.run_jj(["new"]).success();
work_dir.write_file("binary_with_elided_long_file_name.png", b"\x002");
// Diff with a modified binary file with no change in size.
let output = work_dir.run_jj(["diff", "--stat"]);
// Rightmost display column ->|
insta::assert_snapshot!(output, @"
..._elided_long_file_name.png | (binary)
1 file changed, 0 insertions(+), 0 deletions(-)
[EOF]
");
// With a text file included, more space is used for the +++ part.
work_dir.write_file("text_with_elided_long_file_name.txt", b"a\n".repeat(100));
let output = work_dir.run_jj(["diff", "--stat"]);
// Rightmost display column ->|
insta::assert_snapshot!(output, @"
...d_long_file_name.png | (binary)
...d_long_file_name.txt | 100 ++++++++++
2 files changed, 100 insertions(+), 0 deletions(-)
[EOF]
");
// If the binary file size changed, the right side must be wide enough for that
// text.
work_dir.write_file("binary_with_elided_long_file_name.png", b"\x0033");
let output = work_dir.run_jj(["diff", "--stat"]);
// Rightmost display column ->|
insta::assert_snapshot!(output, @"
...ong_file_name.png | (binary) +1 bytes
...ong_file_name.txt | 100 +++++++++++++
2 files changed, 100 insertions(+), 0 deletions(-)
[EOF]
");
}
#[test]
fn test_diff_revisions() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
//
//
//
// E
// |\
// C D
// |/
// B
// |
// A
create_commit(&work_dir, "A", &[]);
create_commit(&work_dir, "B", &["A"]);
create_commit(&work_dir, "C", &["B"]);
create_commit(&work_dir, "D", &["B"]);
create_commit(&work_dir, "E", &["C", "D"]);
let diff_revisions = |expression: &str| -> CommandOutput {
work_dir.run_jj(["diff", "--name-only", "-r", expression])
};
// Can diff a single revision
insta::assert_snapshot!(diff_revisions("B"), @"
B
[EOF]
");
// Can diff a merge
insta::assert_snapshot!(diff_revisions("E"), @"
E
[EOF]
");
// A gap in the range is not allowed (yet at least)
insta::assert_snapshot!(diff_revisions("A|C"), @"
------- stderr -------
Error: Cannot diff revsets with gaps in.
Hint: Revision 03751a746f4e would need to be in the set.
[EOF]
[exit status: 1]
");
// A merge into the chain is not allowed
// We could decide to support this case
insta::assert_snapshot!(diff_revisions("C|E"), @"
------- stderr -------
Error: Cannot diff revsets with gaps in.
Hint: Revision 00ab726847b2 would need to be in the set.
[EOF]
[exit status: 1]
");
// Can diff a linear chain
insta::assert_snapshot!(diff_revisions("A::C"), @"
A
B
C
[EOF]
");
// Can diff a chain with an internal merge
insta::assert_snapshot!(diff_revisions("B::E"), @"
B
C
D
E
[EOF]
");
// Can diff a set with multiple roots
insta::assert_snapshot!(diff_revisions("C|D|E"), @"
C
D
E
[EOF]
");
// Can diff a set with multiple heads
insta::assert_snapshot!(diff_revisions("B|C|D"), @"
B
C
D
[EOF]
");
// Can diff a set with multiple root and multiple heads
insta::assert_snapshot!(diff_revisions("B|C"), @"
B
C
[EOF]
");
}