diff --git a/src/apply.rs b/src/apply.rs index 14c7330..b9484fa 100644 --- a/src/apply.rs +++ b/src/apply.rs @@ -26,6 +26,72 @@ impl fmt::Display for ApplyError { impl std::error::Error for ApplyError {} +/// Statistics for a single hunk application +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct HunkStats { + /// Number of lines added in this hunk + added: usize, + /// Number of lines deleted in this hunk + deleted: usize, + /// Number of context lines in this hunk + context: usize, +} + +/// Statistics about the changes made when applying a patch +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ApplyStats { + /// Total number of lines added + pub lines_added: usize, + /// Total number of lines deleted + pub lines_deleted: usize, + /// Total number of context lines (unchanged) + pub lines_context: usize, + /// Number of hunks successfully applied + pub hunks_applied: usize, +} + +impl ApplyStats { + /// Create new empty statistics + fn new() -> Self { + Self { + lines_added: 0, + lines_deleted: 0, + lines_context: 0, + hunks_applied: 0, + } + } + + /// Add statistics from a hunk + fn add_hunk(&mut self, hunk_stats: HunkStats) { + self.lines_added += hunk_stats.added; + self.lines_deleted += hunk_stats.deleted; + self.lines_context += hunk_stats.context; + self.hunks_applied += 1; + } + + /// Returns whether any changes were made + pub fn has_changes(&self) -> bool { + self.lines_added > 0 || self.lines_deleted > 0 + } +} + +/// Result of applying a patch with statistics +/// +/// # Examples +/// +/// ``` +/// use diffy::{apply, Diff}; +/// +/// let base = "line 1\nline 2\n"; +/// let patch_str = "--- a\n+++ b\n@@ -1,2 +1,2 @@\n line 1\n-line 2\n+line 2 modified\n"; +/// let diff = Diff::from_str(patch_str).unwrap(); +/// +/// let (content, stats) = apply(base, &diff).unwrap(); +/// assert_eq!(content, "line 1\nline 2 modified\n"); +/// assert!(stats.has_changes()); +/// ``` +pub type ApplyResult = Result<(T, ApplyStats), E>; + /// Configuration for patch application #[derive(Default, Debug, Clone)] pub struct ApplyConfig { @@ -224,7 +290,7 @@ where } /// Apply a `Diff` to a base image with default fuzzy matching -pub fn apply(base_image: &str, diff: &Diff<'_, str>) -> Result { +pub fn apply(base_image: &str, diff: &Diff<'_, str>) -> ApplyResult { apply_with_config(base_image, diff, &ApplyConfig::default()) } @@ -233,14 +299,19 @@ pub fn apply_with_config( base_image: &str, diff: &Diff<'_, str>, config: &ApplyConfig, -) -> Result { +) -> ApplyResult { let mut image: Vec<_> = LineIter::new(base_image) .map(ImageLine::Unpatched) .collect(); + let mut stats = ApplyStats::new(); + for (i, hunk) in diff.hunks().iter().enumerate() { - apply_hunk_with_config(&mut image, hunk, config) - .map_err(|_| ApplyError(i + 1, format!("{:#?}", hunk)))?; + let hunk_stats = match apply_hunk_with_config(&mut image, hunk, config) { + Ok(stats) => stats, + Err(_) => return Err(ApplyError(i + 1, format!("{:#?}", hunk))), + }; + stats.add_hunk(hunk_stats); } // TODO: Keep line ending as is like it was before. @@ -265,7 +336,7 @@ pub fn apply_with_config( LineEndHandling::EnsureLineEnding(line_end) => line_end, }); - Ok(image + let content = image .into_iter() .map(ImageLine::into_inner) .map(|(line, ending)| { @@ -275,11 +346,13 @@ pub fn apply_with_config( map_line_ending::<&str>(ending, preferred_line_ending) ) }) - .collect()) + .collect(); + + Ok((content, stats)) } /// Apply a non-utf8 `Diff` to a base image with default fuzzy matching -pub fn apply_bytes(base_image: &[u8], patch: &Diff<'_, [u8]>) -> Result, ApplyError> { +pub fn apply_bytes(base_image: &[u8], patch: &Diff<'_, [u8]>) -> ApplyResult, ApplyError> { apply_bytes_with_config(base_image, patch, &ApplyConfig::default()) } @@ -288,14 +361,19 @@ pub fn apply_bytes_with_config( base_image: &[u8], diff: &Diff<'_, [u8]>, config: &ApplyConfig, -) -> Result, ApplyError> { +) -> ApplyResult, ApplyError> { let mut image: Vec<_> = LineIter::new(base_image) .map(ImageLine::Unpatched) .collect(); + let mut stats = ApplyStats::new(); + for (i, hunk) in diff.hunks().iter().enumerate() { - apply_hunk_with_config(&mut image, hunk, config) - .map_err(|_| ApplyError(i + 1, format!("{:#?}", hunk)))?; + let hunk_stats = match apply_hunk_with_config(&mut image, hunk, config) { + Ok(stats) => stats, + Err(_) => return Err(ApplyError(i + 1, format!("{:#?}", hunk))), + }; + stats.add_hunk(hunk_stats); } // TODO: Keep line ending as is like it was before. @@ -320,7 +398,7 @@ pub fn apply_bytes_with_config( LineEndHandling::EnsureLineEnding(line_end) => line_end, }); - Ok(image + let content = image .into_iter() .map(ImageLine::into_inner) .flat_map(|(line, ending)| { @@ -330,20 +408,35 @@ pub fn apply_bytes_with_config( ] .concat() }) - .collect()) + .collect(); + + Ok((content, stats)) } fn apply_hunk_with_config<'a, T>( image: &mut Vec>, hunk: &Hunk<'a, T>, config: &ApplyConfig, -) -> Result<(), ()> +) -> Result where T: PartialEq + FuzzyComparable + ?Sized + Text + ToOwned, { // Find position with fuzzy matching let (pos, fuzz_level) = find_position_fuzzy(image, hunk, config).ok_or(())?; + // Count changes in this hunk + let mut added = 0; + let mut deleted = 0; + let mut context = 0; + + for line in hunk.lines() { + match line { + Line::Insert(_) => added += 1, + Line::Delete(_) => deleted += 1, + Line::Context(_) => context += 1, + } + } + // update image if fuzz_level == 0 { // Exact match - replace all lines as before @@ -356,7 +449,11 @@ where apply_hunk_preserving_context(image, hunk, pos); } - Ok(()) + Ok(HunkStats { + added, + deleted, + context, + }) } /// Apply hunk while preserving original context lines (for fuzzy matching) @@ -726,9 +823,9 @@ mod test { let patch = crate::Diff::from_bytes(patch.as_bytes()).unwrap(); println!("Applied: {:#?}", patch); - let result = crate::apply_bytes(base_image.as_bytes(), &patch).unwrap(); + let (content, _stats) = crate::apply_bytes(base_image.as_bytes(), &patch).unwrap(); // take the first 50 lines for snapshot testing - let result = String::from_utf8(result) + let result = String::from_utf8(content) .unwrap() .lines() .take(50) @@ -740,7 +837,108 @@ mod test { fn assert_patch(old: &str, new: &str, patch: &str) { let diff = Diff::from_str(patch).unwrap(); - assert_eq!(Ok(new.to_string()), apply(old, &diff)); + let (content, _stats) = apply(old, &diff).unwrap(); + assert_eq!(new, content); + } + + #[test] + fn test_apply_result_statistics() { + let old = "line 1\nline 2\nline 3\n"; + let new = "line 1\nline 2 modified\nline 4\n"; + let patch = "\ +--- original ++++ modified +@@ -1,3 +1,3 @@ + line 1 +-line 2 +-line 3 ++line 2 modified ++line 4 +"; + let diff = Diff::from_str(patch).unwrap(); + let (content, stats) = apply(old, &diff).unwrap(); + + assert_eq!(content, new); + assert_eq!(stats.lines_added, 2); + assert_eq!(stats.lines_deleted, 2); + assert_eq!(stats.lines_context, 1); + assert_eq!(stats.hunks_applied, 1); + assert!(stats.has_changes()); + } + + #[test] + fn test_apply_result_no_changes() { + let old = "line 1\nline 2\n"; + let new = "line 1\nline 2\n"; + let patch = "\ +--- original ++++ modified +@@ -1,2 +1,2 @@ + line 1 + line 2 +"; + let diff = Diff::from_str(patch).unwrap(); + let (content, stats) = apply(old, &diff).unwrap(); + + assert_eq!(content, new); + assert_eq!(stats.lines_added, 0); + assert_eq!(stats.lines_deleted, 0); + assert_eq!(stats.lines_context, 2); + assert_eq!(stats.hunks_applied, 1); + assert!(!stats.has_changes()); + } + + #[test] + fn test_apply_result_multiple_hunks() { + let old = "line 1\nline 2\nline 3\nline 4\nline 5\n"; + let new = "line 1\nline 2 modified\nline 3\nline 4 modified\nline 5\n"; + let patch = "\ +--- original ++++ modified +@@ -1,2 +1,2 @@ + line 1 +-line 2 ++line 2 modified +@@ -4,2 +4,2 @@ +-line 4 ++line 4 modified + line 5 +"; + let diff = Diff::from_str(patch).unwrap(); + let (content, stats) = apply(old, &diff).unwrap(); + + assert_eq!(content, new); + assert_eq!(stats.lines_added, 2); + assert_eq!(stats.lines_deleted, 2); + assert_eq!(stats.lines_context, 2); + assert_eq!(stats.hunks_applied, 2); + assert!(stats.has_changes()); + } + + #[test] + fn test_detect_already_applied_patch() { + let old = "line 1\nline 2\nline 3\n"; + let patch = "\ +--- original ++++ modified +@@ -1,3 +1,3 @@ + line 1 +-line 2 ++line 2 modified + line 3 +"; + let diff = Diff::from_str(patch).unwrap(); + + // First application should succeed with changes + let (content, stats) = apply(old, &diff).unwrap(); + assert_eq!(content, "line 1\nline 2 modified\nline 3\n"); + assert!(stats.has_changes()); + assert_eq!(stats.lines_added, 1); + assert_eq!(stats.lines_deleted, 1); + + // Second application should fail because the patch expects "line 2" but finds "line 2 modified" + let result = apply(&content, &diff); + assert!(result.is_err(), "Applying the same patch twice should fail"); } #[test] diff --git a/src/diff/tests.rs b/src/diff/tests.rs index 4af2354..e88cc8c 100644 --- a/src/diff/tests.rs +++ b/src/diff/tests.rs @@ -341,11 +341,10 @@ macro_rules! assert_patch { assert_eq!(Diff::from_str(&patch_str).unwrap(), patch); assert_eq!(Diff::from_bytes($expected.as_bytes()).unwrap(), bpatch); assert_eq!(Diff::from_bytes(&patch_bytes).unwrap(), bpatch); - assert_eq!(apply($old, &patch).unwrap(), $new); - assert_eq!( - crate::apply_bytes($old.as_bytes(), &bpatch).unwrap(), - $new.as_bytes() - ); + let (content, _stats) = apply($old, &patch).unwrap(); + assert_eq!(content, $new); + let (bytes_content, _stats) = crate::apply_bytes($old.as_bytes(), &bpatch).unwrap(); + assert_eq!(bytes_content, $new.as_bytes()); }; ($old:ident, $new:ident, $expected:ident $(,)?) => { assert_patch!(DiffOptions::default(), $old, $new, $expected); @@ -545,11 +544,10 @@ fn without_no_newline_at_eof_message() { assert_eq!(patch_str, expected); assert_eq!(patch_bytes, patch_str.as_bytes()); assert_eq!(patch_bytes, expected.as_bytes()); - assert_eq!(apply(old, &patch).unwrap(), new); - assert_eq!( - crate::apply_bytes(old.as_bytes(), &bpatch).unwrap(), - new.as_bytes() - ); + let (content, _stats) = apply(old, &patch).unwrap(); + assert_eq!(content, new); + let (bytes_content, _stats) = crate::apply_bytes(old.as_bytes(), &bpatch).unwrap(); + assert_eq!(bytes_content, new.as_bytes()); } #[test] @@ -615,7 +613,8 @@ void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size } "; let git_patch = Diff::from_str(expected_git).unwrap(); - assert_eq!(apply(original, &git_patch).unwrap(), a); + let (content, _stats) = apply(original, &git_patch).unwrap(); + assert_eq!(content, a); let expected_diffy = "\ --- original @@ -686,11 +685,10 @@ fn suppress_blank_empty() { assert_eq!(patch_str, expected); assert_eq!(patch_bytes, patch_str.as_bytes()); assert_eq!(patch_bytes, expected.as_bytes()); - assert_eq!(apply(original, &patch).unwrap(), modified); - assert_eq!( - crate::apply_bytes(original.as_bytes(), &bpatch).unwrap(), - modified.as_bytes() - ); + let (content, _stats) = apply(original, &patch).unwrap(); + assert_eq!(content, modified); + let (bytes_content, _stats) = crate::apply_bytes(original.as_bytes(), &bpatch).unwrap(); + assert_eq!(bytes_content, modified.as_bytes()); // Note that there is no space " " on the line after 3 let expected_suppressed = "\ @@ -714,11 +712,10 @@ fn suppress_blank_empty() { assert_eq!(patch_str, expected_suppressed); assert_eq!(patch_bytes, patch_str.as_bytes()); assert_eq!(patch_bytes, expected_suppressed.as_bytes()); - assert_eq!(apply(original, &patch).unwrap(), modified); - assert_eq!( - crate::apply_bytes(original.as_bytes(), &bpatch).unwrap(), - modified.as_bytes() - ); + let (content2, _stats) = apply(original, &patch).unwrap(); + assert_eq!(content2, modified); + let (bytes_content2, _stats) = crate::apply_bytes(original.as_bytes(), &bpatch).unwrap(); + assert_eq!(bytes_content2, modified.as_bytes()); } // In the event that a patch has an invalid hunk range we want to ensure that when apply is @@ -764,14 +761,14 @@ Second: let now = std::time::Instant::now(); - let result = apply(original, &patch).unwrap(); + let (content, _stats) = apply(original, &patch).unwrap(); let elapsed = now.elapsed(); println!("{:?}", elapsed); assert!(elapsed < std::time::Duration::from_micros(600)); - assert_eq!(result, expected); + assert_eq!(content, expected); } #[test] @@ -792,8 +789,9 @@ fn reverse_empty_file() { } } - let re_reverse = apply(&apply("", &p).unwrap(), &reverse).unwrap(); - assert_eq!(re_reverse, ""); + let (first_content, _stats) = apply("", &p).unwrap(); + let (re_reverse_content, _stats) = apply(&first_content, &reverse).unwrap(); + assert_eq!(re_reverse_content, ""); } #[test] @@ -812,6 +810,7 @@ Kupluh, Indeed let p = create_patch(original, modified); let reverse = p.reverse(); - let re_reverse = apply(&apply(original, &p).unwrap(), &reverse).unwrap(); - assert_eq!(re_reverse, original); + let (first_content, _stats) = apply(original, &p).unwrap(); + let (re_reverse_content, _stats) = apply(&first_content, &reverse).unwrap(); + assert_eq!(re_reverse_content, original); } diff --git a/src/lib.rs b/src/lib.rs index 6b7e99d..dcdb986 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,7 +139,8 @@ //! until I find a more perfect Ideal. //! "; //! -//! assert_eq!(apply(base_image, &diff).unwrap(), expected); +//! let (content, _stats) = apply(base_image, &diff).unwrap(); +//! assert_eq!(content, expected); //! ``` //! //! ## Performing a Three-way Merge @@ -228,8 +229,8 @@ mod range; mod utils; pub use apply::{ - ApplyConfig, ApplyError, FuzzyConfig, LineEndHandling, apply, apply_bytes, - apply_bytes_with_config, apply_with_config, + ApplyConfig, ApplyError, ApplyResult, ApplyStats, FuzzyConfig, LineEndHandling, apply, + apply_bytes, apply_bytes_with_config, apply_with_config, }; pub use diff::{DiffOptions, create_patch, create_patch_bytes}; pub use line_end::*;