diff --git a/CHANGELOG.md b/CHANGELOG.md index d0402fd874..afde2a7890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### New features +<<<<<<< Conflict 1 of 1 ++++++++ Contents of side #1 * The following diff formats now include information about copies and moves if - supported by the backend (the Git backend does): `--color-words`, `--summary` + supported by the backend (the Git backend does): `--color-words`, `--stat`, + `--summary`. ### Fixed bugs diff --git a/Cargo.lock b/Cargo.lock index 6df57da61b..c7569b6ac7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1905,6 +1905,7 @@ dependencies = [ "tracing", "tracing-chrome", "tracing-subscriber", + "unicode-segmentation", "unicode-width", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a1d0523b94..3059269fe8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -93,6 +93,7 @@ toml_edit = { workspace = true } tracing = { workspace = true } tracing-chrome = { workspace = true } tracing-subscriber = { workspace = true } +unicode-segmentation = "1.11.0" unicode-width = { workspace = true } [target.'cfg(unix)'.dependencies] diff --git a/cli/src/diff_util.rs b/cli/src/diff_util.rs index 6b3a5e9f37..508acdbf34 100644 --- a/cli/src/diff_util.rs +++ b/cli/src/diff_util.rs @@ -284,8 +284,7 @@ impl<'a> DiffRenderer<'a> { show_diff_summary(formatter, tree_diff, path_converter, copy_records, to_tree)?; } DiffFormat::Stat => { - let no_copy_tracking = Default::default(); - let tree_diff = from_tree.diff_stream(to_tree, matcher, &no_copy_tracking); + let tree_diff = from_tree.diff_stream(to_tree, matcher, copy_records); show_diff_stat(formatter, store, tree_diff, path_converter, width)?; } DiffFormat::Types => { @@ -1208,6 +1207,7 @@ struct DiffStat { path: String, added: usize, removed: usize, + is_deletion: bool, } fn get_diff_stat( @@ -1235,6 +1235,7 @@ fn get_diff_stat( path, added, removed, + is_deletion: right_content.contents.is_empty(), } } @@ -1246,21 +1247,28 @@ pub fn show_diff_stat( display_width: usize, ) -> Result<(), DiffRenderError> { let mut stats: Vec = vec![]; + let mut unresolved_renames = HashSet::::new(); let mut max_path_width = 0; let mut max_diffs = 0; let mut diff_stream = materialized_diff_stream(store, tree_diff); async { while let Some(MaterializedTreeDiffEntry { - source: _, // TODO handle copy tracking - target: repo_path, + source: left_path, + target: right_path, value: diff, }) = diff_stream.next().await { let (left, right) = diff?; - let path = path_converter.format_file_path(&repo_path); - let left_content = diff_content(&repo_path, left)?; - let right_content = diff_content(&repo_path, right)?; + let left_content = diff_content(&left_path, left)?; + let right_content = diff_content(&right_path, right)?; + + let left_ui_path = path_converter.format_file_path(&left_path); + let right_ui_path = path_converter.format_file_path(&right_path); + if left_ui_path != right_ui_path { + unresolved_renames.insert(left_ui_path.clone()); + } + let path = text_util::render_copied_path(&left_ui_path, &right_ui_path); max_path_width = max(max_path_width, path.width()); let stat = get_diff_stat(path, &left_content, &right_content); max_diffs = max(max_diffs, stat.added + stat.removed); @@ -1285,10 +1293,15 @@ pub fn show_diff_stat( let mut total_added = 0; let mut total_removed = 0; - let total_files = stats.len(); + let mut total_files = 0; for stat in &stats { + if stat.is_deletion && unresolved_renames.contains(&stat.path) { + continue; + } + total_added += stat.added; total_removed += stat.removed; + total_files += 1; let bar_added = (stat.added as f64 * factor).ceil() as usize; let bar_removed = (stat.removed as f64 * factor).ceil() as usize; // replace start of path with ellipsis if the path is too long diff --git a/cli/src/text_util.rs b/cli/src/text_util.rs index 59874a0a47..7126987412 100644 --- a/cli/src/text_util.rs +++ b/cli/src/text_util.rs @@ -15,6 +15,8 @@ use std::borrow::Cow; use std::{cmp, io}; +use itertools::Itertools; +use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthChar as _; use crate::formatter::{FormatRecorder, Formatter}; @@ -35,6 +37,33 @@ pub fn split_email(email: &str) -> (&str, Option<&str>) { } } +pub fn render_copied_path(source: &str, target: &str) -> String { + if source == target { + return target.into(); + } + let prefix = UnicodeSegmentation::split_word_bound_indices(source) + .zip(UnicodeSegmentation::split_word_bound_indices(target)) + .take_while_inclusive(|((_, s), (_, t))| s == t) + .last() + .map(|((s, _), (t, _))| (s, t)) + .unwrap_or((0, 0)); + let suffix = UnicodeSegmentation::split_word_bound_indices(source) + .rev() + .zip(UnicodeSegmentation::split_word_bound_indices(target).rev()) + .take_while(|((_, s), (_, t))| s == t) + .last() + .map(|((s, _), (t, _))| (s, t)) + .unwrap_or((source.len(), target.len())); + + format!( + "{}{{{} => {}}}{}", + &source[0..prefix.0], + &source[prefix.0..suffix.0.max(prefix.0)], + &target[prefix.1..suffix.1.max(prefix.1)], + &source[suffix.0..] + ) +} + /// Shortens `text` to `max_width` by removing leading characters. `ellipsis` is /// added if the `text` gets truncated. /// @@ -629,4 +658,30 @@ mod tests { "foo\n", ); } + + #[test] + fn test_render_copied_path() { + assert_eq!( + render_copied_path("one/two/three", "one/two/three"), + "one/two/three" + ); + assert_eq!( + render_copied_path("two/three", "four/three"), + "{two => four}/three" + ); + assert_eq!( + render_copied_path("one/two/three", "one/four/three"), + "one/{two => four}/three" + ); + assert_eq!( + render_copied_path("one/two/three", "one/three"), + "one/{two => }/three" + ); + assert_eq!( + render_copied_path("one/two", "one/four"), + "one/{two => four}" + ); + assert_eq!(render_copied_path("two", "four"), "{two => four}"); + assert_eq!(render_copied_path("file1", "file2"), "{file1 => file2}"); + } } diff --git a/cli/tests/test_diff_command.rs b/cli/tests/test_diff_command.rs index 63b8af6df9..288a087d4a 100644 --- a/cli/tests/test_diff_command.rs +++ b/cli/tests/test_diff_command.rs @@ -186,10 +186,9 @@ fn test_diff_basic() { let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--stat"]); insta::assert_snapshot!(stdout, @r###" - file1 | 1 - - file2 | 3 ++- - file3 | 1 + - 3 files changed, 3 insertions(+), 2 deletions(-) + file2 | 3 ++- + {file1 => file3} | 0 + 2 files changed, 2 insertions(+), 1 deletion(-) "###); // Filter by glob pattern