forked from mirrors/jj
diff: add option to display complex color-words diffs without inlining
In this patch, I use the number of adds<->removes alternation as a threshold, which approximates the visual complexity of diff hunks. I don't think user can choose the threshold intuitively, but we need a config knob to try out some. I set `max-inline-alternation = 3` locally. 0 and 1 mean "disable inlining" and "inline adds-only/removes-only lines" respectively. I've added "diff.<format>" config namespace assuming "ui.diff" will be reorganized as "ui.diff-formatter" or something. #3327 Some other metrics I've tried: ``` // Per-line alternation. This also works well, but can't measure complexity of // changes across lines. fn count_max_diff_alternation_per_line(diff_lines: &[DiffLine]) -> usize { diff_lines .iter() .map(|line| { let sides = line.hunks.iter().map(|&(side, _)| side); sides .filter(|&side| side != DiffLineHunkSide::Both) .dedup() // omit e.g. left->both->left .count() }) .max() .unwrap_or(0) } // Per-line occupancy of changes. Large diffs don't always look complex. fn max_diff_token_ratio_per_line(diff_lines: &[DiffLine]) -> f32 { diff_lines .iter() .filter_map(|line| { let [both_len, left_len, right_len] = line.hunks.iter().fold([0, 0, 0], |mut acc, (side, data)| { let index = match side { DiffLineHunkSide::Both => 0, DiffLineHunkSide::Left => 1, DiffLineHunkSide::Right => 2, }; acc[index] += data.len(); acc }); // left/right-only change is readable (left_len != 0 && right_len != 0).then(|| { let diff_len = left_len + right_len; let total_len = both_len + left_len + right_len; (diff_len as f32) / (total_len as f32) }) }) .reduce(f32::max) .unwrap_or(0.0) } // Total occupancy of changes. Large diffs don't always look complex. fn total_change_ratio(diff_lines: &[DiffLine]) -> f32 { let (diff_len, total_len) = diff_lines .iter() .flat_map(|line| &line.hunks) .fold((0, 0), |(diff_len, total_len), (side, data)| { let l = data.len(); match side { DiffLineHunkSide::Both => (diff_len, total_len + l), DiffLineHunkSide::Left => (diff_len + l, total_len + l), DiffLineHunkSide::Right => (diff_len + l, total_len + l), } }); (diff_len as f32) / (total_len as f32) } ```
This commit is contained in:
parent
be9b7ed88f
commit
a83dadd5a9
7 changed files with 725 additions and 7 deletions
|
@ -26,6 +26,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
`--color-words`, `--git`, `--stat`, `--summary`, `--types`, and external diff
|
||||
tools in file-by-file mode.
|
||||
|
||||
* Color-words diff has gained [an option to display complex changes as separate
|
||||
lines](docs/config.md#color-words-diff-options).
|
||||
|
||||
* A tilde (`~`) at the start of the path will now be expanded to the user's home
|
||||
directory when configuring a `signing.key` for SSH commit signing.
|
||||
|
||||
|
|
|
@ -1345,8 +1345,10 @@ fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, T
|
|||
let path_converter = language.path_converter;
|
||||
let template = (self_property, context_property)
|
||||
.map(move |(diff, context)| {
|
||||
// TODO: load defaults from UserSettings?
|
||||
let options = diff_util::ColorWordsOptions {
|
||||
context: context.unwrap_or(diff_util::DEFAULT_CONTEXT_LINES),
|
||||
max_inline_alternation: None,
|
||||
};
|
||||
diff.into_formatted(move |formatter, store, tree_diff| {
|
||||
diff_util::show_color_words_diff(
|
||||
|
|
|
@ -276,6 +276,22 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
"type": "object",
|
||||
"description": "Builtin diff formats settings",
|
||||
"properties": {
|
||||
"color-words": {
|
||||
"type": "object",
|
||||
"description": "Options for color-words diffs",
|
||||
"properties": {
|
||||
"max-inline-alternation": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of removed/added word alternation to inline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
"type": "object",
|
||||
"description": "Settings for git behavior (when using git backend)",
|
||||
|
|
|
@ -5,6 +5,9 @@ amend = ["squash"]
|
|||
co = ["checkout"]
|
||||
unamend = ["unsquash"]
|
||||
|
||||
[diff.color-words]
|
||||
max-inline-alternation = -1
|
||||
|
||||
[ui]
|
||||
# TODO: delete ui.allow-filesets in jj 0.26+
|
||||
allow-filesets = true
|
||||
|
|
|
@ -407,15 +407,28 @@ fn collect_copied_sources<'a>(
|
|||
pub struct ColorWordsOptions {
|
||||
/// Number of context lines to show.
|
||||
pub context: usize,
|
||||
/// Maximum number of removed/added word alternation to inline.
|
||||
pub max_inline_alternation: Option<usize>,
|
||||
}
|
||||
|
||||
impl ColorWordsOptions {
|
||||
fn from_settings_and_args(
|
||||
_settings: &UserSettings,
|
||||
settings: &UserSettings,
|
||||
args: &DiffFormatArgs,
|
||||
) -> Result<Self, config::ConfigError> {
|
||||
let config = settings.config();
|
||||
let max_inline_alternation = {
|
||||
let key = "diff.color-words.max-inline-alternation";
|
||||
match config.get_int(key)? {
|
||||
-1 => None, // unlimited
|
||||
n => Some(usize::try_from(n).map_err(|err| {
|
||||
config::ConfigError::Message(format!("invalid {key}: {err}"))
|
||||
})?),
|
||||
}
|
||||
};
|
||||
Ok(ColorWordsOptions {
|
||||
context: args.context.unwrap_or(DEFAULT_CONTEXT_LINES),
|
||||
max_inline_alternation,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -467,13 +480,35 @@ fn show_color_words_diff_hunks(
|
|||
)?;
|
||||
}
|
||||
DiffHunk::Different(contents) => {
|
||||
let word_diff = Diff::by_word(&contents);
|
||||
let mut diff_line_iter =
|
||||
DiffLineIterator::with_line_number(word_diff.hunks(), line_number);
|
||||
for diff_line in diff_line_iter.by_ref() {
|
||||
show_color_words_diff_line(formatter, &diff_line)?;
|
||||
let word_diff_hunks = Diff::by_word(&contents).hunks().collect_vec();
|
||||
let can_inline = match options.max_inline_alternation {
|
||||
None => true, // unlimited
|
||||
Some(0) => false, // no need to count alternation
|
||||
Some(max_num) => {
|
||||
let groups = split_diff_hunks_by_matching_newline(&word_diff_hunks);
|
||||
groups.map(count_diff_alternation).max().unwrap_or(0) <= max_num
|
||||
}
|
||||
};
|
||||
if can_inline {
|
||||
let mut diff_line_iter =
|
||||
DiffLineIterator::with_line_number(word_diff_hunks.iter(), line_number);
|
||||
for diff_line in diff_line_iter.by_ref() {
|
||||
show_color_words_diff_line(formatter, &diff_line)?;
|
||||
}
|
||||
line_number = diff_line_iter.next_line_number();
|
||||
} else {
|
||||
let (left_lines, right_lines) = unzip_diff_hunks_to_lines(&word_diff_hunks);
|
||||
for tokens in &left_lines {
|
||||
show_color_words_line_number(formatter, Some(line_number.left), None)?;
|
||||
show_color_words_single_sided_line(formatter, tokens, "removed")?;
|
||||
line_number.left += 1;
|
||||
}
|
||||
for tokens in &right_lines {
|
||||
show_color_words_line_number(formatter, None, Some(line_number.right))?;
|
||||
show_color_words_single_sided_line(formatter, tokens, "added")?;
|
||||
line_number.right += 1;
|
||||
}
|
||||
}
|
||||
line_number = diff_line_iter.next_line_number();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -544,6 +579,7 @@ fn show_color_words_line_number(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Prints `diff_line` which may contain tokens originating from both sides.
|
||||
fn show_color_words_diff_line(
|
||||
formatter: &mut dyn Formatter,
|
||||
diff_line: &DiffLine,
|
||||
|
@ -578,6 +614,56 @@ fn show_color_words_diff_line(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Prints left/right-only line tokens with the given label.
|
||||
fn show_color_words_single_sided_line(
|
||||
formatter: &mut dyn Formatter,
|
||||
tokens: &[(DiffTokenType, &[u8])],
|
||||
label: &str,
|
||||
) -> io::Result<()> {
|
||||
formatter.with_label(label, |formatter| show_diff_line_tokens(formatter, tokens))?;
|
||||
let (_, data) = tokens.last().expect("diff line must not be empty");
|
||||
if !data.ends_with(b"\n") {
|
||||
writeln!(formatter)?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Counts number of diff-side alternation, ignoring matching hunks.
|
||||
///
|
||||
/// This function is meant to measure visual complexity of diff hunks. It's easy
|
||||
/// to read hunks containing some removed or added words, but is getting harder
|
||||
/// as more removes and adds interleaved.
|
||||
///
|
||||
/// For example,
|
||||
/// - `[matching]` -> 0
|
||||
/// - `[left]` -> 1
|
||||
/// - `[left, matching, left]` -> 1
|
||||
/// - `[matching, left, right, matching, right]` -> 2
|
||||
/// - `[left, right, matching, right, left]` -> 3
|
||||
fn count_diff_alternation(diff_hunks: &[DiffHunk]) -> usize {
|
||||
diff_hunks
|
||||
.iter()
|
||||
.filter_map(|hunk| match hunk {
|
||||
DiffHunk::Matching(_) => None,
|
||||
DiffHunk::Different(contents) => Some(contents),
|
||||
})
|
||||
// Map non-empty diff side to index (0: left, 1: right)
|
||||
.flat_map(|contents| contents.iter().positions(|content| !content.is_empty()))
|
||||
// Omit e.g. left->(matching->)*left
|
||||
.dedup()
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Splits hunks into slices of contiguous changed lines.
|
||||
fn split_diff_hunks_by_matching_newline<'a, 'b>(
|
||||
diff_hunks: &'a [DiffHunk<'b>],
|
||||
) -> impl Iterator<Item = &'a [DiffHunk<'b>]> {
|
||||
diff_hunks.split_inclusive(|hunk| match hunk {
|
||||
DiffHunk::Matching(content) => content.contains(&b'\n'),
|
||||
DiffHunk::Different(_) => false,
|
||||
})
|
||||
}
|
||||
|
||||
struct FileContent {
|
||||
/// false if this file is likely text; true if it is likely binary.
|
||||
is_binary: bool,
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use indoc::indoc;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::common::{escaped_fake_diff_editor_path, strip_last_line, TestEnvironment};
|
||||
|
@ -739,6 +740,507 @@ fn test_diff_hunks() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff_color_words_inlining_threshold() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
|
||||
let repo_path = test_env.env_root().join("repo");
|
||||
|
||||
let render_diff = |max_alternation: i32, args: &[&str]| {
|
||||
let config = format!("diff.color-words.max-inline-alternation={max_alternation}");
|
||||
test_env.jj_cmd_success(
|
||||
&repo_path,
|
||||
&[&["diff", "--config-toml", &config], args].concat(),
|
||||
)
|
||||
};
|
||||
|
||||
let file1_path = "file1-single-line";
|
||||
let file2_path = "file2-multiple-lines-in-single-hunk";
|
||||
let file3_path = "file3-changes-across-lines";
|
||||
std::fs::write(
|
||||
repo_path.join(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
|
||||
"},
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
repo_path.join(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
|
||||
"},
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
repo_path.join(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
|
||||
"},
|
||||
)
|
||||
.unwrap();
|
||||
test_env.jj_cmd_ok(&repo_path, &["new"]);
|
||||
std::fs::write(
|
||||
repo_path.join(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
|
||||
"},
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
repo_path.join(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
|
||||
"},
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
repo_path.join(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
|
||||
"},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// inline all by default
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["diff"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
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
|
||||
"###);
|
||||
|
||||
// 0: no inlining
|
||||
insta::assert_snapshot!(render_diff(0, &[]), @r###"
|
||||
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
|
||||
"###);
|
||||
|
||||
// 1: inline adds-only or removes-only lines
|
||||
insta::assert_snapshot!(render_diff(1, &[]), @r###"
|
||||
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
|
||||
"###);
|
||||
|
||||
// 2: inline up to adds + removes lines
|
||||
insta::assert_snapshot!(render_diff(2, &[]), @r###"
|
||||
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
|
||||
"###);
|
||||
|
||||
// 3: inline up to adds + removes + adds lines
|
||||
insta::assert_snapshot!(render_diff(3, &[]), @r###"
|
||||
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
|
||||
"###);
|
||||
|
||||
// 4: inline up to adds + removes + adds + removes lines
|
||||
insta::assert_snapshot!(render_diff(4, &[]), @r###"
|
||||
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
|
||||
"###);
|
||||
|
||||
// context words in added/removed lines should be labeled as such
|
||||
insta::assert_snapshot!(render_diff(2, &["--color=always"]), @r###"
|
||||
[38;5;3mModified regular file file1-single-line:[39m
|
||||
[38;5;1m 1[39m [38;5;2m 1[39m: == adds ==
|
||||
[38;5;1m 2[39m [38;5;2m 2[39m: a [4m[38;5;2mX [24m[39mb [4m[38;5;2mY Z [24m[39mc
|
||||
[38;5;1m 3[39m [38;5;2m 3[39m: == removes ==
|
||||
[38;5;1m 4[39m [38;5;2m 4[39m: a [4m[38;5;1mb [24m[39mc [4m[38;5;1md e [24m[39mf[4m[38;5;1m g[24m[39m
|
||||
[38;5;1m 5[39m [38;5;2m 5[39m: == adds + removes ==
|
||||
[38;5;1m 6[39m [38;5;2m 6[39m: a [4m[38;5;2mX [24m[39mb [4m[38;5;1mc [24m[39md[4m[38;5;1m e[24m[39m
|
||||
[38;5;1m 7[39m [38;5;2m 7[39m: == adds + removes + adds ==
|
||||
[38;5;1m 8[39m : [38;5;1ma b [4mc [24md [4me[24m[39m
|
||||
[38;5;2m 8[39m: [38;5;2ma [4mX [24mb d [4mY[24m[39m
|
||||
[38;5;1m 9[39m [38;5;2m 9[39m: == adds + removes + adds + removes ==
|
||||
[38;5;1m 10[39m : [38;5;1ma b [4mc [24md e[4m f g[24m[39m
|
||||
[38;5;2m 10[39m: [4m[38;5;2mX [24ma [4mY [24mb d [4mZ [24me[39m
|
||||
[38;5;3mModified regular file file2-multiple-lines-in-single-hunk:[39m
|
||||
[38;5;1m 1[39m [38;5;2m 1[39m: == adds; removes; adds + removes ==
|
||||
[38;5;1m 2[39m [38;5;2m 2[39m: a [4m[38;5;2mX [24m[39mb [4m[38;5;2mY Z [24m[39mc
|
||||
[38;5;1m 3[39m [38;5;2m 3[39m: a [4m[38;5;1mb [24m[39mc [4m[38;5;1md e [24m[39mf[4m[38;5;1m g[24m[39m
|
||||
[38;5;1m 4[39m [38;5;2m 4[39m: a [4m[38;5;2mX [24m[39mb [4m[38;5;1mc [24m[39md[4m[38;5;1m e[24m[39m
|
||||
[38;5;1m 5[39m [38;5;2m 5[39m: == adds + removes + adds; adds + removes + adds + removes ==
|
||||
[38;5;1m 6[39m : [38;5;1ma b [4mc [24md [4me[24m[39m
|
||||
[38;5;1m 7[39m : [38;5;1ma b [4mc [24md e[4m f g[24m[39m
|
||||
[38;5;2m 6[39m: [38;5;2ma [4mX [24mb d [4mY[24m[39m
|
||||
[38;5;2m 7[39m: [4m[38;5;2mX [24ma [4mY [24mb d [4mZ [24me[39m
|
||||
[38;5;3mModified regular file file3-changes-across-lines:[39m
|
||||
[38;5;1m 1[39m [38;5;2m 1[39m: == adds ==
|
||||
[38;5;1m 2[39m [38;5;2m 2[39m: a [4m[38;5;2mX [24m[39mb[4m[38;5;2m[24m[39m
|
||||
[38;5;1m 2[39m [38;5;2m 3[39m: [4m[38;5;2mY Z[24m[39m c
|
||||
[38;5;1m 3[39m [38;5;2m 4[39m: == removes ==
|
||||
[38;5;1m 4[39m [38;5;2m 5[39m: a [4m[38;5;1mb [24m[39mc [4m[38;5;1md[24m[39m
|
||||
[38;5;1m 5[39m [38;5;2m 5[39m: [4m[38;5;1me [24m[39mf[4m[38;5;1m g[24m[39m
|
||||
[38;5;1m 6[39m [38;5;2m 6[39m: == adds + removes ==
|
||||
[38;5;1m 7[39m [38;5;2m 7[39m: a[4m[38;5;2m[24m[39m
|
||||
[38;5;1m 7[39m [38;5;2m 8[39m: [4m[38;5;2mX[24m[39m b [4m[38;5;1mc[24m[39m
|
||||
[38;5;1m 8[39m [38;5;2m 8[39m: d[4m[38;5;1m e[24m[39m
|
||||
[38;5;1m 9[39m [38;5;2m 9[39m: == adds + removes + adds ==
|
||||
[38;5;1m 10[39m : [38;5;1ma b [4mc[24m[39m
|
||||
[38;5;1m 11[39m : [38;5;1md[4m e[24m[39m
|
||||
[38;5;2m 10[39m: [38;5;2ma [4mX [24mb d[4m[24m[39m
|
||||
[38;5;2m 11[39m: [4m[38;5;2mY[24m[39m
|
||||
[38;5;1m 12[39m [38;5;2m 12[39m: == adds + removes + adds + removes ==
|
||||
[38;5;1m 13[39m : [38;5;1ma b[4m[24m[39m
|
||||
[38;5;1m 14[39m : [4m[38;5;1mc[24m d e[4m f g[24m[39m
|
||||
[38;5;2m 13[39m: [4m[38;5;2mX [24ma [4mY [24mb d[4m[24m[39m
|
||||
[38;5;2m 14[39m: [4m[38;5;2mZ[24m e[39m
|
||||
"###);
|
||||
insta::assert_snapshot!(render_diff(2, &["--color=debug"]), @r###"
|
||||
[38;5;3m<<diff header::Modified regular file file1-single-line:>>[39m
|
||||
[38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: == adds ==>>
|
||||
[38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 2>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b >>[4m[38;5;2m<<diff added token::Y Z >>[24m[39m<<diff::c>>
|
||||
[38;5;1m<<diff removed line_number:: 3>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 3>>[39m<<diff::: == removes ==>>
|
||||
[38;5;1m<<diff removed line_number:: 4>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 4>>[39m<<diff::: a >>[4m[38;5;1m<<diff removed token::b >>[24m[39m<<diff::c >>[4m[38;5;1m<<diff removed token::d e >>[24m[39m<<diff::f>>[4m[38;5;1m<<diff removed token:: g>>[24m[39m<<diff::>>
|
||||
[38;5;1m<<diff removed line_number:: 5>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 5>>[39m<<diff::: == adds + removes ==>>
|
||||
[38;5;1m<<diff removed line_number:: 6>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 6>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b >>[4m[38;5;1m<<diff removed token::c >>[24m[39m<<diff::d>>[4m[38;5;1m<<diff removed token:: e>>[24m[39m<<diff::>>
|
||||
[38;5;1m<<diff removed line_number:: 7>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 7>>[39m<<diff::: == adds + removes + adds ==>>
|
||||
[38;5;1m<<diff removed line_number:: 8>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c >>[24m<<diff removed::d >>[4m<<diff removed token::e>>[24m<<diff removed::>>[39m
|
||||
<<diff:: >>[38;5;2m<<diff added line_number:: 8>>[39m<<diff::: >>[38;5;2m<<diff added::a >>[4m<<diff added token::X >>[24m<<diff added::b d >>[4m<<diff added token::Y>>[24m<<diff added::>>[39m
|
||||
[38;5;1m<<diff removed line_number:: 9>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 9>>[39m<<diff::: == adds + removes + adds + removes ==>>
|
||||
[38;5;1m<<diff removed line_number:: 10>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c >>[24m<<diff removed::d e>>[4m<<diff removed token:: f g>>[24m<<diff removed::>>[39m
|
||||
<<diff:: >>[38;5;2m<<diff added line_number:: 10>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::X >>[24m<<diff added::a >>[4m<<diff added token::Y >>[24m<<diff added::b d >>[4m<<diff added token::Z >>[24m<<diff added::e>>[39m
|
||||
[38;5;3m<<diff header::Modified regular file file2-multiple-lines-in-single-hunk:>>[39m
|
||||
[38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: == adds; removes; adds + removes ==>>
|
||||
[38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 2>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b >>[4m[38;5;2m<<diff added token::Y Z >>[24m[39m<<diff::c>>
|
||||
[38;5;1m<<diff removed line_number:: 3>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 3>>[39m<<diff::: a >>[4m[38;5;1m<<diff removed token::b >>[24m[39m<<diff::c >>[4m[38;5;1m<<diff removed token::d e >>[24m[39m<<diff::f>>[4m[38;5;1m<<diff removed token:: g>>[24m[39m<<diff::>>
|
||||
[38;5;1m<<diff removed line_number:: 4>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 4>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b >>[4m[38;5;1m<<diff removed token::c >>[24m[39m<<diff::d>>[4m[38;5;1m<<diff removed token:: e>>[24m[39m<<diff::>>
|
||||
[38;5;1m<<diff removed line_number:: 5>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 5>>[39m<<diff::: == adds + removes + adds; adds + removes + adds + removes ==>>
|
||||
[38;5;1m<<diff removed line_number:: 6>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c >>[24m<<diff removed::d >>[4m<<diff removed token::e>>[24m<<diff removed::>>[39m
|
||||
[38;5;1m<<diff removed line_number:: 7>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c >>[24m<<diff removed::d e>>[4m<<diff removed token:: f g>>[24m<<diff removed::>>[39m
|
||||
<<diff:: >>[38;5;2m<<diff added line_number:: 6>>[39m<<diff::: >>[38;5;2m<<diff added::a >>[4m<<diff added token::X >>[24m<<diff added::b d >>[4m<<diff added token::Y>>[24m<<diff added::>>[39m
|
||||
<<diff:: >>[38;5;2m<<diff added line_number:: 7>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::X >>[24m<<diff added::a >>[4m<<diff added token::Y >>[24m<<diff added::b d >>[4m<<diff added token::Z >>[24m<<diff added::e>>[39m
|
||||
[38;5;3m<<diff header::Modified regular file file3-changes-across-lines:>>[39m
|
||||
[38;5;1m<<diff removed line_number:: 1>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 1>>[39m<<diff::: == adds ==>>
|
||||
[38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 2>>[39m<<diff::: a >>[4m[38;5;2m<<diff added token::X >>[24m[39m<<diff::b>>[4m[38;5;2m<<diff added token::>>[24m[39m
|
||||
[38;5;1m<<diff removed line_number:: 2>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 3>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::Y Z>>[24m[39m<<diff:: c>>
|
||||
[38;5;1m<<diff removed line_number:: 3>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 4>>[39m<<diff::: == removes ==>>
|
||||
[38;5;1m<<diff removed line_number:: 4>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 5>>[39m<<diff::: a >>[4m[38;5;1m<<diff removed token::b >>[24m[39m<<diff::c >>[4m[38;5;1m<<diff removed token::d>>[24m[39m
|
||||
[38;5;1m<<diff removed line_number:: 5>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 5>>[39m<<diff::: >>[4m[38;5;1m<<diff removed token::e >>[24m[39m<<diff::f>>[4m[38;5;1m<<diff removed token:: g>>[24m[39m<<diff::>>
|
||||
[38;5;1m<<diff removed line_number:: 6>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 6>>[39m<<diff::: == adds + removes ==>>
|
||||
[38;5;1m<<diff removed line_number:: 7>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 7>>[39m<<diff::: a>>[4m[38;5;2m<<diff added token::>>[24m[39m
|
||||
[38;5;1m<<diff removed line_number:: 7>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 8>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::X>>[24m[39m<<diff:: b >>[4m[38;5;1m<<diff removed token::c>>[24m[39m
|
||||
[38;5;1m<<diff removed line_number:: 8>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 8>>[39m<<diff::: d>>[4m[38;5;1m<<diff removed token:: e>>[24m[39m<<diff::>>
|
||||
[38;5;1m<<diff removed line_number:: 9>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 9>>[39m<<diff::: == adds + removes + adds ==>>
|
||||
[38;5;1m<<diff removed line_number:: 10>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b >>[4m<<diff removed token::c>>[24m[39m
|
||||
[38;5;1m<<diff removed line_number:: 11>>[39m<<diff:: : >>[38;5;1m<<diff removed::d>>[4m<<diff removed token:: e>>[24m<<diff removed::>>[39m
|
||||
<<diff:: >>[38;5;2m<<diff added line_number:: 10>>[39m<<diff::: >>[38;5;2m<<diff added::a >>[4m<<diff added token::X >>[24m<<diff added::b d>>[4m<<diff added token::>>[24m[39m
|
||||
<<diff:: >>[38;5;2m<<diff added line_number:: 11>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::Y>>[24m<<diff added::>>[39m
|
||||
[38;5;1m<<diff removed line_number:: 12>>[39m<<diff:: >>[38;5;2m<<diff added line_number:: 12>>[39m<<diff::: == adds + removes + adds + removes ==>>
|
||||
[38;5;1m<<diff removed line_number:: 13>>[39m<<diff:: : >>[38;5;1m<<diff removed::a b>>[4m<<diff removed token::>>[24m[39m
|
||||
[38;5;1m<<diff removed line_number:: 14>>[39m<<diff:: : >>[4m[38;5;1m<<diff removed token::c>>[24m<<diff removed:: d e>>[4m<<diff removed token:: f g>>[24m<<diff removed::>>[39m
|
||||
<<diff:: >>[38;5;2m<<diff added line_number:: 13>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::X >>[24m<<diff added::a >>[4m<<diff added token::Y >>[24m<<diff added::b d>>[4m<<diff added token::>>[24m[39m
|
||||
<<diff:: >>[38;5;2m<<diff added line_number:: 14>>[39m<<diff::: >>[4m[38;5;2m<<diff added token::Z>>[24m<<diff added:: e>>[39m
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff_missing_newline() {
|
||||
let test_env = TestEnvironment::default();
|
||||
|
@ -892,6 +1394,89 @@ fn test_color_words_diff_missing_newline() {
|
|||
8 : h
|
||||
9 : I
|
||||
"###);
|
||||
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&repo_path,
|
||||
&[
|
||||
"log",
|
||||
"--config-toml=diff.color-words.max-inline-alternation=0",
|
||||
"-Tdescription",
|
||||
"-pr::@-",
|
||||
"--no-graph",
|
||||
"--reversed",
|
||||
],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
=== 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
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -200,6 +200,29 @@ can override the default style with the following keys:
|
|||
ui.diff.format = "git"
|
||||
```
|
||||
|
||||
#### Color-words diff options
|
||||
|
||||
In color-words diffs, changed words are displayed inline by default. Because
|
||||
it's difficult to read a diff line with many removed/added words, there's a
|
||||
threshold to switch to traditional separate-line format.
|
||||
|
||||
* `max-inline-alternation`: Maximum number of removed/added word alternation to
|
||||
inline. For example, `<added> ... <added>` sequence has 1 alternation, so the
|
||||
line will be inline if `max-inline-alternation >= 1`. `<added> ... <removed>
|
||||
... <added>` sequence has 3 alternation.
|
||||
|
||||
* `0`: disable inlining, making `--color-words` more similar to `--git`
|
||||
* `1`: inline removes-only or adds-only lines
|
||||
* `2`, `3`, ..: inline up to `2`, `3`, .. alternation
|
||||
* `-1`: inline all lines (default)
|
||||
|
||||
**This parameter is experimental.** The definition is subject to change.
|
||||
|
||||
```toml
|
||||
[diff.color-words]
|
||||
max-inline-alternation = 3
|
||||
```
|
||||
|
||||
### Generating diffs by external command
|
||||
|
||||
If `ui.diff.tool` is set, the specified diff command will be called instead of
|
||||
|
|
Loading…
Reference in a new issue