diff --git a/gitmirror/crates/gitmirror-xrpc/src/format_patch.rs b/gitmirror/crates/gitmirror-xrpc/src/format_patch.rs index 1521f3cac..e2e6bc358 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/format_patch.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/format_patch.rs @@ -1557,6 +1557,24 @@ mod tests { } } + fn edit_reads_like_git(old: &str, new: &str) { + let dir = init(); + let root = dir.path(); + write(root, "file.c", old); + let base = commit(root, "old"); + write(root, "file.c", new); + let tip = commit(root, "new"); + assert_reads_like_git(root, base, tip); + } + + #[test] + fn lines_are_counted_over_the_whole_file_like_git() { + edit_reads_like_git( + "\n *\n\n info_class->info_type = GI_INFO_TYPE_UNRESOLVED;\n}\n", + " * version 2 of the License, or (at your option) any later version.\n *\n *\n */\n\n info_class->info_type = GI_INFO_TYPE_UNRESOLVED;\n}\n", + ); + } + #[test] fn similarity_floors_to_a_percent_like_git() { for max in 1..=2000u32 { diff --git a/knot2/third_party/gix-imara-diff/src/histogram.rs b/knot2/third_party/gix-imara-diff/src/histogram.rs index 09c5f59b0..6aba6486c 100644 --- a/knot2/third_party/gix-imara-diff/src/histogram.rs +++ b/knot2/third_party/gix-imara-diff/src/histogram.rs @@ -100,7 +100,7 @@ impl Histogram { // we are diffing two extremely large repetitive files // this is a worst case for histogram diff with O(N^2) performance // fallback to myers to maintain linear time complexity - myers::diff(before, after, removed, added, false); + myers::diff((before, after), before, after, removed, added, false); return; } } diff --git a/knot2/third_party/gix-imara-diff/src/lib.rs b/knot2/third_party/gix-imara-diff/src/lib.rs index 5d38a117c..aee0c141e 100644 --- a/knot2/third_party/gix-imara-diff/src/lib.rs +++ b/knot2/third_party/gix-imara-diff/src/lib.rs @@ -271,6 +271,7 @@ impl Diff { self.added.clear(); self.removed.resize(before.len(), false); self.added.resize(after.len(), false); + let whole = (before, after); let common_prefix = strip_common_prefix(&mut before, &mut after) as usize; let common_postfix = strip_common_postfix(&mut before, &mut after); let range = common_prefix..self.removed.len() - common_postfix as usize; @@ -279,8 +280,8 @@ impl Diff { let added = &mut self.added[range]; match algorithm { Algorithm::Histogram => histogram::diff(before, after, removed, added, num_tokens), - Algorithm::Myers => myers::diff(before, after, removed, added, false), - Algorithm::MyersMinimal => myers::diff(before, after, removed, added, true), + Algorithm::Myers => myers::diff(whole, before, after, removed, added, false), + Algorithm::MyersMinimal => myers::diff(whole, before, after, removed, added, true), } } diff --git a/knot2/third_party/gix-imara-diff/src/myers.rs b/knot2/third_party/gix-imara-diff/src/myers.rs index ca9dd0272..6e8b858c2 100644 --- a/knot2/third_party/gix-imara-diff/src/myers.rs +++ b/knot2/third_party/gix-imara-diff/src/myers.rs @@ -16,15 +16,24 @@ mod slice; /// /// # Parameters /// +/// * `whole` - The files to count occurrences in, before their common prefix and suffix were stripped. +/// Histogram's myers fallback passes its own sub-slices. /// * `before` - The token sequence from the first file /// * `after` - The token sequence from the second file /// * `removed` - Output array marking removed tokens /// * `added` - Output array marking added tokens /// * `minimal` - If true, disables heuristics to guarantee a minimal diff -pub fn diff(before: &[Token], after: &[Token], removed: &mut [bool], added: &mut [bool], minimal: bool) { +pub fn diff( + whole: (&[Token], &[Token]), + before: &[Token], + after: &[Token], + removed: &mut [bool], + added: &mut [bool], + minimal: bool, +) { // Preprocess the files by removing parts of the file that are not contained in the other file at all. // This process remaps the token indices so we have to account for that during the rest of the diff - let (before, after) = preprocess::preprocess(before, after, removed, added); + let (before, after) = preprocess::preprocess(whole, before, after, removed, added); // Perform the actual diff Myers::new(before.tokens.len(), after.tokens.len()).run( diff --git a/knot2/third_party/gix-imara-diff/src/myers/preprocess.rs b/knot2/third_party/gix-imara-diff/src/myers/preprocess.rs index 081404fb4..49187bce9 100644 --- a/knot2/third_party/gix-imara-diff/src/myers/preprocess.rs +++ b/knot2/third_party/gix-imara-diff/src/myers/preprocess.rs @@ -9,59 +9,56 @@ use crate::myers::sqrt; /// This optimization reduces the problem size for the Myers algorithm, improving performance /// for files with many unique tokens. pub fn preprocess<'a>( + whole: (&[Token], &[Token]), before: &[Token], after: &[Token], removed: &'a mut [bool], added: &'a mut [bool], ) -> (PreprocessedFile, PreprocessedFile) { - let (occurrences_before, occurrences_after) = token_occurrences(before, after); + let (occurrences_before, occurrences_after) = token_occurrences(whole, before, after); let file1 = PreprocessedFile::new(&occurrences_before, before, removed); let file2 = PreprocessedFile::new(&occurrences_after, after, added); (file1, file2) } -fn token_occurrences(file1: &[Token], file2: &[Token]) -> (Vec, Vec) { +/// Only the middles `file1` and `file2` are classified, against counts and limits from `whole`. +fn token_occurrences( + whole: (&[Token], &[Token]), + file1: &[Token], + file2: &[Token], +) -> (Vec, Vec) { const MAX_EQLIMIT: u32 = 1024; // compute the limit after which tokens are treated as `Occurrences::COMMON` - let eqlimit1 = sqrt(file1.len()).min(MAX_EQLIMIT); - let eqlimit2 = sqrt(file2.len()).min(MAX_EQLIMIT); + let eqlimit1 = sqrt(whole.0.len()).min(MAX_EQLIMIT); + let eqlimit2 = sqrt(whole.1.len()).min(MAX_EQLIMIT); // first collect how often each token occurs in a file - let mut occurrences1 = Vec::new(); - for token in file1 { - let bucket = token.0 as usize; - if bucket >= occurrences1.len() { - occurrences1.resize(bucket + 1, 0u32); - } - occurrences1[bucket] += 1; - } - - // do the same thing for - let mut occurrences2 = Vec::new(); - let token_occurrences2: Vec<_> = file2 - .iter() - .map(|token| { + let count = |file: &[Token]| { + let mut occurrences = Vec::new(); + for token in file { let bucket = token.0 as usize; - if bucket >= occurrences2.len() { - occurrences2.resize(bucket + 1, 0); + if bucket >= occurrences.len() { + occurrences.resize(bucket + 1, 0u32); } - occurrences2[bucket] += 1; - let occurrences1 = *occurrences1.get(bucket).unwrap_or(&0); - Occurrences::from_occurrences(occurrences1, eqlimit2) - }) - .collect(); - - let token_occurrences1: Vec<_> = file1 - .iter() - .map(|token| { - let bucket = token.0 as usize; - let occurrences2 = *occurrences2.get(bucket).unwrap_or(&0); - Occurrences::from_occurrences(occurrences2, eqlimit1) - }) - .collect(); - - (token_occurrences1, token_occurrences2) + occurrences[bucket] += 1; + } + occurrences + }; + let (occurrences1, occurrences2) = (count(whole.0), count(whole.1)); + + let classify = |file: &[Token], other: &[u32], eqlimit: u32| -> Vec<_> { + file.iter() + .map(|token| { + let occurrences = *other.get(token.0 as usize).unwrap_or(&0); + Occurrences::from_occurrences(occurrences, eqlimit) + }) + .collect() + }; + ( + classify(file1, &occurrences2, eqlimit1), + classify(file2, &occurrences1, eqlimit2), + ) } /// Categorizes how frequently a token appears in a file. @@ -180,7 +177,8 @@ fn should_prune_common_line(token_status: &[Occurrences], pos: usize) -> bool { #[cfg(test)] mod tests { - use super::{Occurrences, should_prune_common_line}; + use super::{Occurrences, should_prune_common_line, token_occurrences}; + use crate::intern::Token; #[test] fn common_line_pruning_ignores_distant_context() { @@ -196,4 +194,14 @@ mod tests { "only the last 100 items before the current line should contribute to the backward scan" ); } + + #[test] + fn occurrences_count_the_whole_files() { + let (x, z) = (Token(0), Token(1)); + let whole1 = vec![x; 9]; + let mut whole2 = vec![x; 8]; + whole2.push(z); + let (occurrences1, _) = token_occurrences((&whole1, &whole2), &whole1[8..], &whole2[8..]); + assert!(matches!(occurrences1[0], Occurrences::Common)); + } }