1use super::options::{DiffAlgorithm, DiffOptions};
4use super::structured::StructuredFileDiff;
5use super::{count_changes_with_algorithm, DiffEntry, DiffStatus};
6
7#[must_use]
13pub fn line_change_counts(old_content: &str, new_content: &str, options: &DiffOptions) -> LineStat {
14 let (old_content, new_content) = if options.algorithm == DiffAlgorithm::Myers {
15 changed_region(old_content, new_content)
16 } else {
17 (old_content, new_content)
18 };
19 if options.algorithm == DiffAlgorithm::Myers
20 && !old_content.as_bytes().contains(&b'\r')
21 && !new_content.as_bytes().contains(&b'\r')
22 {
23 return myers_lf_counts(old_content, new_content);
24 }
25 let (insertions, deletions) = count_changes_with_algorithm(
26 old_content,
27 new_content,
28 options.algorithm.similar_algorithm(),
29 options.algorithm.uses_git_histogram(),
30 );
31 LineStat {
32 insertions,
33 deletions,
34 }
35}
36
37fn myers_lf_counts(old: &str, new: &str) -> LineStat {
38 if old.is_empty() || new.is_empty() {
39 return LineStat {
40 insertions: new.split_inclusive('\n').count(),
41 deletions: old.split_inclusive('\n').count(),
42 };
43 }
44 let (old_ids, new_ids) = {
49 let old_lines: Vec<_> = old.split_inclusive('\n').collect();
50 let new_lines: Vec<_> = new.split_inclusive('\n').collect();
51 let mut interner =
52 crate::line_interner::LineInterner::with_capacity(old_lines.len() + new_lines.len());
53 (
54 interner.intern_all(&old_lines),
55 interner.intern_all(&new_lines),
56 )
57 };
58 let mut stat = LineStat::default();
59 for op in similar::capture_diff_slices(similar::Algorithm::Myers, &old_ids, &new_ids) {
60 match op {
61 similar::DiffOp::Delete { old_len, .. } => stat.deletions += old_len,
62 similar::DiffOp::Insert { new_len, .. } => stat.insertions += new_len,
63 similar::DiffOp::Replace {
64 old_len, new_len, ..
65 } => {
66 stat.deletions += old_len;
67 stat.insertions += new_len;
68 }
69 similar::DiffOp::Equal { .. } => {}
70 }
71 }
72 stat
73}
74
75fn changed_region<'a>(old: &'a str, new: &'a str) -> (&'a str, &'a str) {
79 let prefix: usize = old
80 .split_inclusive('\n')
81 .zip(new.split_inclusive('\n'))
82 .take_while(|(a, b)| a == b)
83 .map(|(line, _)| line.len())
84 .sum();
85 let (old, new) = (&old[prefix..], &new[prefix..]);
86 let suffix: usize = old
87 .split_inclusive('\n')
88 .rev()
89 .zip(new.split_inclusive('\n').rev())
90 .take_while(|(a, b)| a == b)
91 .map(|(line, _)| line.len())
92 .sum();
93 (&old[..old.len() - suffix], &new[..new.len() - suffix])
94}
95
96#[non_exhaustive]
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
99pub enum DiffDetail {
100 #[default]
102 Patch,
103 Stat,
105 NameStatus,
107 NameOnly,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113pub struct LineStat {
114 pub insertions: usize,
116 pub deletions: usize,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub struct DiffTotals {
123 pub files_changed: usize,
125 pub insertions: usize,
127 pub deletions: usize,
129}
130
131impl DiffTotals {
132 #[must_use]
134 pub fn from_files(files: &[FileDiff]) -> Self {
135 let mut insertions = 0usize;
136 let mut deletions = 0usize;
137 for file in files {
138 if let Some(stat) = file.stat {
139 insertions += stat.insertions;
140 deletions += stat.deletions;
141 }
142 }
143 Self {
144 files_changed: files.len(),
145 insertions,
146 deletions,
147 }
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct FileDiff {
154 pub path: String,
156 pub old_path: Option<String>,
158 pub status: DiffStatus,
160 pub old_mode: Option<String>,
162 pub new_mode: Option<String>,
164 pub binary: bool,
166 pub similarity: Option<u32>,
168 pub stat: Option<LineStat>,
170 pub structured: Option<StructuredFileDiff>,
172 pub combined: Option<crate::combined_diff_patch::CombinedStructuredTextDiff>,
174 pub combined_status: Option<String>,
176}
177
178impl DiffEntry {
179 #[must_use]
181 pub fn rename_similarity(&self) -> Option<u32> {
182 match self.status {
183 DiffStatus::Renamed | DiffStatus::Copied => self.score,
184 _ => None,
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn reference_counts(old: &str, new: &str) -> LineStat {
194 let (insertions, deletions) =
195 count_changes_with_algorithm(old, new, similar::Algorithm::Myers, false);
196 LineStat {
197 insertions,
198 deletions,
199 }
200 }
201
202 #[test]
203 fn myers_stat_preserves_line_boundaries_and_repeated_lines() {
204 let options = DiffOptions::default();
205 let texts = [
206 "",
207 "a",
208 "a\n",
209 "a\na",
210 "a\na\n",
211 "a\nb\na\n",
212 "b\na\nb\n",
213 "\n",
214 "\n\n",
215 "a\r",
216 "a\r\n",
217 "a\rb\n",
218 "é\n猫",
219 "é\n猫\n",
220 "a\n\r\nb\n",
221 ];
222 for old in texts {
223 for new in texts {
224 assert_eq!(
225 line_change_counts(old, new, &options),
226 reference_counts(old, new),
227 "old={old:?}, new={new:?}"
228 );
229 }
230 }
231
232 let mut seed = 4687u64;
235 for iteration in 0..500 {
236 let mut text = || {
237 (0..160)
238 .map(|_| {
239 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
240 match seed >> 61 {
241 0..=3 => "repeat\n",
242 4 => "é\r\n",
243 5 => "\n",
244 6 => "x\ry\n",
245 _ => "tail\n",
246 }
247 })
248 .collect::<String>()
249 };
250 let (mut old, mut new) = (text(), text());
251 if iteration % 2 == 0 {
252 old = old.replace('\r', "");
253 new = new.replace('\r', "");
254 }
255 let prefix = "unchanged\n".repeat(120);
256 let old = format!("{prefix}{old}{prefix}");
257 let new = format!("{prefix}{new}{prefix}");
258 assert_eq!(
259 line_change_counts(&old, &new, &options),
260 reference_counts(&old, &new)
261 );
262 }
263 }
264
265 #[test]
266 fn myers_stat_large_file_small_edit() {
267 let prefix = "unchanged prefix\n".repeat(10_000);
268 let suffix = "unchanged suffix\n".repeat(10_000);
269 let old = format!("{prefix}old\n{suffix}");
270 let new = format!("{prefix}new\nextra\n{suffix}");
271 assert_eq!(changed_region(&old, &new), ("old\n", "new\nextra\n"));
272 assert_eq!(
273 line_change_counts(&old, &new, &DiffOptions::default()),
274 LineStat {
275 insertions: 2,
276 deletions: 1,
277 }
278 );
279 assert_eq!(changed_region(&old, &old), ("", ""));
280 }
281}