sley_diff_merge/line_diff.rs
1//! Line-level diff algorithms (Myers, patience, histogram).
2
3use std::collections::HashMap;
4
5/// A single line of a blob, slicing into the original buffer.
6///
7/// `content` includes the line's own trailing newline byte when present;
8/// `has_newline` records whether this line ended with `\n` in the source. Only
9/// the final line of a blob can have `has_newline == false` (a file with "no
10/// newline at end of file"). Comparing two `DiffLine`s for equality compares
11/// both the bytes and the trailing-newline flag, so a line that gained or lost
12/// its terminating newline is treated as a real change, matching git.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct DiffLine<'a> {
15 /// The raw bytes of the line, including the trailing `\n` if it had one.
16 pub content: &'a [u8],
17 /// Whether the line was terminated by a newline in the source blob.
18 pub has_newline: bool,
19}
20
21impl<'a> DiffLine<'a> {
22 /// The line bytes without any trailing newline.
23 pub fn bytes_without_newline(&self) -> &'a [u8] {
24 if self.has_newline {
25 self.content.strip_suffix(b"\n").unwrap_or(self.content)
26 } else {
27 self.content
28 }
29 }
30}
31
32/// Split a blob into lines, preserving the exact bytes of each line.
33///
34/// Each returned [`DiffLine`] borrows from `blob`; its `content` includes the
35/// terminating `\n`. The returned vector is empty for an empty blob. A blob
36/// whose final byte is not `\n` yields a final line with `has_newline ==
37/// false` — git's "\ No newline at end of file" case.
38pub fn split_lines(blob: &[u8]) -> Vec<DiffLine<'_>> {
39 let mut lines = Vec::new();
40 let mut start = 0usize;
41 let len = blob.len();
42 let mut idx = 0usize;
43 while idx < len {
44 if blob[idx] == b'\n' {
45 lines.push(DiffLine {
46 content: &blob[start..=idx],
47 has_newline: true,
48 });
49 idx += 1;
50 start = idx;
51 } else {
52 idx += 1;
53 }
54 }
55 if start < len {
56 lines.push(DiffLine {
57 content: &blob[start..len],
58 has_newline: false,
59 });
60 }
61 lines
62}
63
64/// A run-length entry in a Myers edit script.
65///
66/// Each variant carries the number of consecutive lines it applies to:
67/// - [`DiffOp::Equal`] — `n` lines common to both `old` and `new`.
68/// - [`DiffOp::Delete`] — `n` lines present in `old` but not `new`.
69/// - [`DiffOp::Insert`] — `n` lines present in `new` but not `old`.
70///
71/// Walking the script in order and consuming `old`/`new` lines accordingly
72/// reconstructs `new` from `old`.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum DiffOp {
75 /// `n` lines are identical in both sequences.
76 Equal(usize),
77 /// `n` lines are removed from the old sequence.
78 Delete(usize),
79 /// `n` lines are added in the new sequence.
80 Insert(usize),
81}
82
83/// Compute a minimal line-level edit script transforming `old` into `new`
84/// using Myers' O(ND) difference algorithm.
85///
86/// Lines are compared for equality by their full bytes (see [`DiffLine`]). The
87/// result is a coalesced sequence of [`DiffOp`] runs; consecutive ops of the
88/// same kind are merged so the script is compact. The script is a standard
89/// (shortest-edit-script) diff: the number of `Delete` + `Insert` lines is
90/// minimal.
91pub fn myers_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
92 // Trim a common prefix and suffix first. This keeps the O(ND) search small
93 // for the typical case of a localized edit and does not affect minimality.
94 let n_total = old.len();
95 let m_total = new.len();
96 let mut prefix = 0usize;
97 while prefix < n_total && prefix < m_total && old[prefix] == new[prefix] {
98 prefix += 1;
99 }
100 let mut suffix = 0usize;
101 while suffix < n_total - prefix
102 && suffix < m_total - prefix
103 && old[n_total - 1 - suffix] == new[m_total - 1 - suffix]
104 {
105 suffix += 1;
106 }
107
108 let old_mid = &old[prefix..n_total - suffix];
109 let new_mid = &new[prefix..m_total - suffix];
110
111 let mut ops: Vec<DiffOp> = Vec::new();
112 if prefix > 0 {
113 ops.push(DiffOp::Equal(prefix));
114 }
115 myers_core(old_mid, new_mid, &mut ops);
116 if suffix > 0 {
117 ops.push(DiffOp::Equal(suffix));
118 }
119 coalesce_ops(ops)
120}
121
122/// Classic forward Myers O(ND) shortest-edit-script search over the trimmed
123/// sub-problem, followed by a backtrack through the stored traces.
124///
125/// `old`/`new` are the trimmed (no common prefix/suffix) line slices. Per-line
126/// ops are appended to `out` in order; they are coalesced by the caller. This
127/// is the algorithm from Myers' 1986 paper, which yields a shortest edit script
128/// (minimal number of insertions + deletions).
129fn myers_core(old: &[DiffLine<'_>], new: &[DiffLine<'_>], out: &mut Vec<DiffOp>) {
130 let n = old.len() as isize;
131 let m = new.len() as isize;
132 if n == 0 {
133 if m > 0 {
134 out.push(DiffOp::Insert(m as usize));
135 }
136 return;
137 }
138 if m == 0 {
139 out.push(DiffOp::Delete(n as usize));
140 return;
141 }
142
143 let max = (n + m) as usize;
144 let offset = max as isize; // shift so diagonal k maps to index (k + offset)
145 let width = 2 * max + 1;
146 // v[k + offset] holds the furthest-reaching x on diagonal k for the current d.
147 let mut v = vec![0isize; width];
148 // Save a snapshot of v after each d so we can backtrack the chosen path.
149 let mut trace: Vec<Vec<isize>> = Vec::new();
150
151 let mut found_d: Option<usize> = None;
152 'search: for d in 0..=(max as isize) {
153 trace.push(v.clone());
154 let mut k = -d;
155 while k <= d {
156 let kidx = (k + offset) as usize;
157 // Decide whether we arrived here by moving down (insert, from k+1)
158 // or right (delete, from k-1). Prefer the move that reaches further.
159 let mut x = if k == -d
160 || (k != d && v[(k - 1 + offset) as usize] < v[(k + 1 + offset) as usize])
161 {
162 // Move down: x stays, y increases (insertion from new).
163 v[(k + 1 + offset) as usize]
164 } else {
165 // Move right: x increases (deletion from old).
166 v[(k - 1 + offset) as usize] + 1
167 };
168 let mut y = x - k;
169 // Follow the diagonal (matching lines) as far as possible.
170 while x < n && y < m && old[x as usize] == new[y as usize] {
171 x += 1;
172 y += 1;
173 }
174 v[kidx] = x;
175 if x >= n && y >= m {
176 found_d = Some(d as usize);
177 break 'search;
178 }
179 k += 2;
180 }
181 }
182
183 // A shortest edit path always exists, so found_d is set; if somehow not,
184 // fall back to a delete-all/insert-all script (still correct, not minimal).
185 let Some(d_end) = found_d else {
186 out.push(DiffOp::Delete(n as usize));
187 out.push(DiffOp::Insert(m as usize));
188 return;
189 };
190
191 backtrack(n, m, &trace, d_end, offset, out);
192}
193
194/// Reconstruct the edit script from the saved Myers traces.
195///
196/// Walks backward from `(n, m)` to `(0, 0)`, emitting per-line `Delete`,
197/// `Insert`, and `Equal` ops, then reverses them into forward order before
198/// appending to `out`. `n`/`m` are the lengths of the (trimmed) old/new slices.
199fn backtrack(
200 n: isize,
201 m: isize,
202 trace: &[Vec<isize>],
203 d_end: usize,
204 offset: isize,
205 out: &mut Vec<DiffOp>,
206) {
207 let mut x = n;
208 let mut y = m;
209 let mut rev: Vec<DiffOp> = Vec::new();
210
211 for d in (0..=d_end).rev() {
212 let v = &trace[d];
213 let k = x - y;
214 // Determine the predecessor diagonal, mirroring the forward step rule.
215 let prev_k = if k == -(d as isize)
216 || (k != d as isize && v[(k - 1 + offset) as usize] < v[(k + 1 + offset) as usize])
217 {
218 k + 1 // came from a down move (insert)
219 } else {
220 k - 1 // came from a right move (delete)
221 };
222 let prev_x = v[(prev_k + offset) as usize];
223 let prev_y = prev_x - prev_k;
224
225 // Emit the diagonal (equal) moves taken after reaching the predecessor.
226 while x > prev_x && y > prev_y {
227 rev.push(DiffOp::Equal(1));
228 x -= 1;
229 y -= 1;
230 }
231 if d > 0 {
232 if x == prev_x {
233 // Down move: an insertion of new[prev_y].
234 rev.push(DiffOp::Insert(1));
235 } else {
236 // Right move: a deletion of old[prev_x].
237 rev.push(DiffOp::Delete(1));
238 }
239 x = prev_x;
240 y = prev_y;
241 }
242 }
243
244 rev.reverse();
245 out.extend(rev);
246}
247
248/// Merge adjacent ops of the same kind so the script is compact.
249fn coalesce_ops(ops: Vec<DiffOp>) -> Vec<DiffOp> {
250 let mut out: Vec<DiffOp> = Vec::with_capacity(ops.len());
251 for op in ops {
252 match (out.last_mut(), op) {
253 (Some(DiffOp::Equal(prev)), DiffOp::Equal(n)) => *prev += n,
254 (Some(DiffOp::Delete(prev)), DiffOp::Delete(n)) => *prev += n,
255 (Some(DiffOp::Insert(prev)), DiffOp::Insert(n)) => *prev += n,
256 _ => out.push(op),
257 }
258 }
259 out
260}
261
262// ===========================================================================
263// Whitespace-ignoring line comparison (git xdiff's XDF_WHITESPACE_FLAGS).
264//
265// git's xdiff compares two records (lines, including the trailing `\n`) for
266// equality under whitespace-ignore flags via `xdl_recmatch`. Rather than
267// re-implement the Myers core to take a custom equality predicate, we map each
268// flavour to a *canonicalization* of the line bytes that produces identical
269// output iff `xdl_recmatch` would return 1, then diff on the canonicalized
270// lines while emitting the original bytes. This is exact: it is a behavioural
271// port of `xdiff/xutils.c:xdl_recmatch` and `xdl_blankline`.
272// ===========================================================================
273
274/// Whitespace-ignore flags for line comparison, mirroring git's
275/// `XDF_WHITESPACE_FLAGS` (`-w`, `-b`, `--ignore-space-at-eol`,
276/// `--ignore-cr-at-eol`). Only one of the whitespace flavours is honoured per
277/// git's precedence (`-w` ⊃ `-b` ⊃ `--ignore-space-at-eol` ⊃
278/// `--ignore-cr-at-eol`); when several are set, the strongest wins, matching
279/// the cascade in `xdl_recmatch`.
280#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
281pub struct WsIgnore {
282 /// `-w` / `--ignore-all-space`: ignore all whitespace when comparing lines.
283 pub all_space: bool,
284 /// `-b` / `--ignore-space-change`: ignore changes in amount of whitespace.
285 pub space_change: bool,
286 /// `--ignore-space-at-eol`: ignore whitespace at end of line.
287 pub space_at_eol: bool,
288 /// `--ignore-cr-at-eol`: ignore a carriage-return at end of line.
289 pub cr_at_eol: bool,
290}
291
292impl WsIgnore {
293 /// No whitespace-ignore flavour active (the exact, byte-for-byte comparison).
294 pub const EMPTY: Self = Self {
295 all_space: false,
296 space_change: false,
297 space_at_eol: false,
298 cr_at_eol: false,
299 };
300
301 /// True when no whitespace-ignore flavour is active.
302 pub fn is_empty(&self) -> bool {
303 !(self.all_space || self.space_change || self.space_at_eol || self.cr_at_eol)
304 }
305}
306
307/// `XDL_ISSPACE` — git uses C `isspace` over the unsigned byte (space, `\t`,
308/// `\n`, `\r`, `\x0b` vertical tab, `\x0c` form feed).
309#[inline]
310fn xdl_isspace(c: u8) -> bool {
311 matches!(c, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
312}
313
314/// Canonicalize a line's bytes (including any trailing `\n`) for whitespace-
315/// insensitive comparison, exactly mirroring `xdl_recmatch`'s acceptance set:
316/// two original lines are equal under `ignore` iff their canonical forms are
317/// byte-identical.
318///
319/// * `all_space` (`-w`): drop every whitespace byte.
320/// * `space_change` (`-b`): collapse each run of whitespace to a single `' '`
321/// and strip trailing whitespace (a run on one side matches a run on the
322/// other regardless of length; leading/internal whitespace must still align,
323/// trailing whitespace is dropped entirely).
324/// * `space_at_eol`: strip trailing whitespace only.
325/// * `cr_at_eol`: drop a single `\r` immediately before a terminating `\n`.
326///
327/// Exposed crate-internally so the change-compaction pass in [`crate::render`]
328/// can compare lines for sliding under the exact same equality the line-level
329/// diff uses (git's `recs_match` on the whitespace-canonicalized record).
330pub(crate) fn canonicalize_line_for_match(line: &[u8], ignore: WsIgnore) -> Vec<u8> {
331 canonicalize_line(line, ignore)
332}
333
334pub(crate) fn canonicalize_line(line: &[u8], ignore: WsIgnore) -> Vec<u8> {
335 if ignore.all_space {
336 return line.iter().copied().filter(|&c| !xdl_isspace(c)).collect();
337 }
338 if ignore.space_change {
339 let mut out = Vec::with_capacity(line.len());
340 let mut i = 0usize;
341 while i < line.len() {
342 if xdl_isspace(line[i]) {
343 // Collapse the whole whitespace run to a single space.
344 while i < line.len() && xdl_isspace(line[i]) {
345 i += 1;
346 }
347 out.push(b' ');
348 } else {
349 out.push(line[i]);
350 i += 1;
351 }
352 }
353 // Strip a trailing collapsed-space (trailing whitespace is ignored).
354 if out.last() == Some(&b' ') {
355 out.pop();
356 }
357 return out;
358 }
359 if ignore.space_at_eol {
360 let mut end = line.len();
361 while end > 0 && xdl_isspace(line[end - 1]) {
362 end -= 1;
363 }
364 return line[..end].to_vec();
365 }
366 if ignore.cr_at_eol {
367 // Drop a `\r` directly before a terminating `\n`.
368 if let Some(stripped) = line.strip_suffix(b"\n") {
369 if let Some(without_cr) = stripped.strip_suffix(b"\r") {
370 let mut out = without_cr.to_vec();
371 out.push(b'\n');
372 return out;
373 }
374 } else if let Some(without_cr) = line.strip_suffix(b"\r") {
375 // Incomplete final line: a bare trailing `\r` is also ignored.
376 return without_cr.to_vec();
377 }
378 return line.to_vec();
379 }
380 line.to_vec()
381}
382
383/// `xdl_blankline`: a line is "blank" when, after applying the active
384/// whitespace flags, it has no content. With no whitespace flags, git treats a
385/// record of size ≤ 1 (empty, or a lone `\n`) as blank; with flags, a line all
386/// of whose bytes are whitespace is blank.
387pub(crate) fn line_is_blank(line: &[u8], ignore: WsIgnore) -> bool {
388 if ignore.is_empty() {
389 line.len() <= 1
390 } else {
391 line.iter().all(|&c| xdl_isspace(c))
392 }
393}
394
395/// Compute a line-level edit script transforming `old` into `new`, comparing
396/// lines under the whitespace-ignore flags `ignore` while the returned ops
397/// still index the *original* lines position-for-position.
398///
399/// When `ignore.is_empty()`, this is identical to [`myers_diff_lines`]. With
400/// flags, lines are canonicalized (see [`canonicalize_line`]) for the equality
401/// test only; the ops consume the same number of old/new lines as the originals
402/// so the caller can render the original bytes.
403pub fn myers_diff_lines_ws(
404 old: &[DiffLine<'_>],
405 new: &[DiffLine<'_>],
406 ignore: WsIgnore,
407 algorithm: DiffAlgorithm,
408) -> Vec<DiffOp> {
409 if ignore.is_empty() {
410 return diff_lines_with_algorithm(old, new, algorithm);
411 }
412 let old_canon: Vec<Vec<u8>> = old
413 .iter()
414 .map(|l| canonicalize_line(l.content, ignore))
415 .collect();
416 let new_canon: Vec<Vec<u8>> = new
417 .iter()
418 .map(|l| canonicalize_line(l.content, ignore))
419 .collect();
420 let old_lines: Vec<DiffLine<'_>> = old_canon
421 .iter()
422 .map(|c| DiffLine {
423 content: c.as_slice(),
424 has_newline: true,
425 })
426 .collect();
427 let new_lines: Vec<DiffLine<'_>> = new_canon
428 .iter()
429 .map(|c| DiffLine {
430 content: c.as_slice(),
431 has_newline: true,
432 })
433 .collect();
434 diff_lines_with_algorithm(&old_lines, &new_lines, algorithm)
435}
436
437// ===========================================================================
438// Alternative diff algorithms: patience and histogram.
439//
440// Both share the recursive "anchor and recurse" shape used by git's xdiff
441// implementations of `--patience` and `--histogram`:
442//
443// 1. trim the common prefix and suffix of the current line range,
444// 2. pick one or more common lines that are confidently aligned (the
445// "anchors") according to the algorithm's rule,
446// 3. recurse on the gaps to the left of, between, and to the right of the
447// anchors,
448// 4. when no anchor can be found, fall back to the Myers shortest-edit-script
449// search for that range so the result is still a valid LCS-correct diff.
450//
451// They operate purely on slices of [`DiffLine`]s and emit the same coalesced
452// [`DiffOp`] run sequence as [`myers_diff_lines`], so any caller can swap
453// algorithms freely. The two functions differ only in the anchor-selection
454// rule in steps 2/3.
455// ===========================================================================
456
457/// A hashable key for a line, used to bucket equal lines when finding anchors.
458///
459/// Mirrors [`DiffLine`]'s `PartialEq`: two lines are the same iff their bytes
460/// and their trailing-newline flag match. Keying on this tuple lets us hash
461/// lines without changing the public [`DiffLine`] type.
462type LineKey<'a> = (&'a [u8], bool);
463
464#[inline]
465fn line_key<'a>(line: &DiffLine<'a>) -> LineKey<'a> {
466 (line.content, line.has_newline)
467}
468
469/// Compute a line-level edit script transforming `old` into `new` using the
470/// patience diff algorithm (Bram Cohen's algorithm, as in `git diff
471/// --patience`).
472///
473/// Patience diff anchors on lines that occur *exactly once* in both `old` and
474/// `new`; it aligns those unique lines via a longest-increasing-subsequence
475/// ("patience sorting") pass and recurses into the gaps, falling back to Myers
476/// when a gap has no unique common line. The result is a valid LCS-correct edit
477/// script with the same shape as [`myers_diff_lines`]: walking it reconstructs
478/// `new` from `old`, and every [`DiffOp::Equal`] run covers genuinely equal
479/// lines. Patience tends to produce more human-readable hunks than Myers when
480/// blocks of lines are moved or repeated, though it is not guaranteed to be a
481/// shortest edit script.
482pub fn patience_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
483 patience_diff_lines_anchored(old, new, &[])
484}
485
486/// As [`patience_diff_lines`], but pins lines whose content has any of `anchors`
487/// as a byte prefix into the common subsequence (git's `--anchored=<text>`).
488///
489/// Mirrors xdiff's `xpatience.c`: an anchor line that is unique in both ranges is
490/// forced to remain aligned (so *other* lines are moved instead), taken greedily
491/// in old-side order; an anchor that would break the increasing order with an
492/// already-pinned anchor is dropped. Anchors that are non-unique or absent have
493/// no effect, exactly as in git. With `anchors` empty this is plain patience.
494pub fn patience_diff_lines_anchored(
495 old: &[DiffLine<'_>],
496 new: &[DiffLine<'_>],
497 anchors: &[Vec<u8>],
498) -> Vec<DiffOp> {
499 let mut ops: Vec<DiffOp> = Vec::new();
500 patience_recurse(old, new, 0, old.len(), 0, new.len(), anchors, &mut ops);
501 coalesce_ops(ops)
502}
503
504/// Compute a line-level edit script transforming `old` into `new` using the
505/// histogram diff algorithm (as in `git diff --histogram`, derived from JGit).
506///
507/// Histogram diff is a patience-style unique-anchor algorithm with a fallback:
508/// it builds an occurrence histogram of `old` and, scanning `new`, picks the
509/// longest run of matching lines whose `old` line has the *fewest* occurrences
510/// (preferring truly unique lines, like patience, but still able to anchor on
511/// low-frequency lines when no globally-unique line exists). It then recurses
512/// on the regions on either side of that run, falling back to Myers only when
513/// no common line exists in a region. The result is a valid LCS-correct edit
514/// script with the same shape as [`myers_diff_lines`].
515pub fn histogram_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
516 let mut ops: Vec<DiffOp> = Vec::new();
517 histogram_recurse(old, new, 0, old.len(), 0, new.len(), &mut ops);
518 coalesce_ops(ops)
519}
520
521/// Dispatch to the line-diff implementation selected by `algorithm`.
522///
523/// All variants return the same coalesced [`DiffOp`] run sequence as
524/// [`myers_diff_lines`], so callers can switch algorithms without changing how
525/// they consume the result.
526///
527/// - [`DiffAlgorithm::Myers`] and [`DiffAlgorithm::Minimal`] use the Myers
528/// O(ND) shortest-edit-script search ([`myers_diff_lines`]); that search is
529/// already minimal in deletions + insertions, so `Minimal` is an alias for
530/// it here rather than a distinct slower mode.
531/// - [`DiffAlgorithm::Patience`] uses [`patience_diff_lines`].
532/// - [`DiffAlgorithm::Histogram`] uses [`histogram_diff_lines`].
533pub fn diff_lines_with_algorithm(
534 old: &[DiffLine<'_>],
535 new: &[DiffLine<'_>],
536 algorithm: DiffAlgorithm,
537) -> Vec<DiffOp> {
538 match algorithm {
539 DiffAlgorithm::Myers | DiffAlgorithm::Minimal => myers_diff_lines(old, new),
540 DiffAlgorithm::Patience => patience_diff_lines(old, new),
541 DiffAlgorithm::Histogram => histogram_diff_lines(old, new),
542 }
543}
544
545/// Emit ops for an empty-on-one-side range; returns `true` if it handled it.
546///
547/// Covers the recursion base cases where one side of `old[a0..a1]` /
548/// `new[b0..b1]` is empty: a pure deletion, a pure insertion, or nothing at
549/// all. Used by both the patience and histogram recursions before they look
550/// for an anchor.
551fn emit_trivial_range(a0: usize, a1: usize, b0: usize, b1: usize, out: &mut Vec<DiffOp>) -> bool {
552 let old_len = a1 - a0;
553 let new_len = b1 - b0;
554 if old_len == 0 && new_len == 0 {
555 return true;
556 }
557 if old_len == 0 {
558 out.push(DiffOp::Insert(new_len));
559 return true;
560 }
561 if new_len == 0 {
562 out.push(DiffOp::Delete(old_len));
563 return true;
564 }
565 false
566}
567
568/// Trim the common prefix/suffix of `old[a0..a1]` vs `new[b0..b1]`.
569///
570/// Emits an `Equal` for the matched prefix immediately, returns the inner
571/// (still-differing) range, and reports the matched-suffix length so the caller
572/// can emit its `Equal` *after* it has processed the inner range. This keeps
573/// the per-range work proportional to the actual edit, mirroring the prefix /
574/// suffix trim in [`myers_diff_lines`].
575fn trim_common(
576 old: &[DiffLine<'_>],
577 new: &[DiffLine<'_>],
578 mut a0: usize,
579 mut a1: usize,
580 mut b0: usize,
581 mut b1: usize,
582 out: &mut Vec<DiffOp>,
583) -> (usize, usize, usize, usize, usize) {
584 let mut prefix = 0usize;
585 while a0 < a1 && b0 < b1 && old[a0] == new[b0] {
586 a0 += 1;
587 b0 += 1;
588 prefix += 1;
589 }
590 if prefix > 0 {
591 out.push(DiffOp::Equal(prefix));
592 }
593 let mut suffix = 0usize;
594 while a1 > a0 && b1 > b0 && old[a1 - 1] == new[b1 - 1] {
595 a1 -= 1;
596 b1 -= 1;
597 suffix += 1;
598 }
599 (a0, a1, b0, b1, suffix)
600}
601
602/// Recursive patience-diff worker over `old[a0..a1]` vs `new[b0..b1]`.
603///
604/// `anchors` carries the `--anchored=<text>` prefixes (empty for plain
605/// patience); they are re-evaluated at every recursion level, since a line that
606/// is non-unique in the whole file can become unique within a sub-range.
607#[allow(clippy::too_many_arguments)]
608fn patience_recurse(
609 old: &[DiffLine<'_>],
610 new: &[DiffLine<'_>],
611 a0: usize,
612 a1: usize,
613 b0: usize,
614 b1: usize,
615 anchors: &[Vec<u8>],
616 out: &mut Vec<DiffOp>,
617) {
618 if emit_trivial_range(a0, a1, b0, b1, out) {
619 return;
620 }
621 let (a0, a1, b0, b1, suffix) = trim_common(old, new, a0, a1, b0, b1, out);
622 if !emit_trivial_range(a0, a1, b0, b1, out) {
623 match patience_anchors(old, new, a0, a1, b0, b1, anchors) {
624 Some(aligned) => {
625 // Walk the aligned anchors in order, recursing into each gap
626 // before emitting the anchor line as Equal.
627 let mut cur_a = a0;
628 let mut cur_b = b0;
629 for (ai, bi) in aligned {
630 patience_recurse(old, new, cur_a, ai, cur_b, bi, anchors, out);
631 out.push(DiffOp::Equal(1));
632 cur_a = ai + 1;
633 cur_b = bi + 1;
634 }
635 // Tail after the last anchor.
636 patience_recurse(old, new, cur_a, a1, cur_b, b1, anchors, out);
637 }
638 // No unique common line in this range: defer to Myers, which always
639 // yields a valid (and minimal) script for the leftover block.
640 None => myers_core(&old[a0..a1], &new[b0..b1], out),
641 }
642 }
643 if suffix > 0 {
644 out.push(DiffOp::Equal(suffix));
645 }
646}
647
648/// Find the patience anchors for `old[a0..a1]` vs `new[b0..b1]`.
649///
650/// An anchor is a line that occurs exactly once in `old[a0..a1]` and exactly
651/// once in `new[b0..b1]`. The matched (old_index, new_index) pairs are reduced
652/// to their longest increasing subsequence by new-index (the patience-sort LCS)
653/// so the returned anchors are strictly increasing in *both* indices and can be
654/// used as split points. Returns `None` when there are no such unique common
655/// lines (the caller then falls back to Myers).
656fn patience_anchors(
657 old: &[DiffLine<'_>],
658 new: &[DiffLine<'_>],
659 a0: usize,
660 a1: usize,
661 b0: usize,
662 b1: usize,
663 anchors: &[Vec<u8>],
664) -> Option<Vec<(usize, usize)>> {
665 // Count occurrences and remember the (single) position of each line in each
666 // side's range. `count > 1` poisons the position so we can ignore it.
667 struct Occ {
668 count: usize,
669 pos: usize,
670 }
671 let mut in_old: HashMap<LineKey<'_>, Occ> = HashMap::new();
672 for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
673 in_old
674 .entry(line_key(line))
675 .and_modify(|o| o.count += 1)
676 .or_insert(Occ { count: 1, pos: i });
677 }
678 let mut in_new: HashMap<LineKey<'_>, Occ> = HashMap::new();
679 for (j, line) in new.iter().enumerate().take(b1).skip(b0) {
680 in_new
681 .entry(line_key(line))
682 .and_modify(|o| o.count += 1)
683 .or_insert(Occ { count: 1, pos: j });
684 }
685
686 // Collect lines unique in both, ordered by their position in `old`.
687 let mut pairs: Vec<(usize, usize)> = Vec::new();
688 for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
689 let key = line_key(line);
690 let Some(o) = in_old.get(&key) else { continue };
691 if o.count != 1 || o.pos != i {
692 continue;
693 }
694 // A line unique in both ranges is a candidate anchor.
695 if let Some(n) = in_new.get(&key)
696 && n.count == 1
697 {
698 pairs.push((i, n.pos));
699 }
700 }
701 if pairs.is_empty() {
702 return None;
703 }
704
705 // Patience sort: longest increasing subsequence of new-indices. `pairs` is
706 // already sorted by old-index, so an LIS by new-index yields a set of
707 // anchors increasing in both coordinates. With `--anchored` text(s) present,
708 // pin the matching (unique-in-both) lines into the subsequence instead.
709 let lis = if anchors.is_empty() {
710 longest_increasing_by_new(&pairs)
711 } else {
712 let is_anchor: Vec<bool> = pairs
713 .iter()
714 .map(|&(_, nj)| line_matches_anchor(new[nj].content, anchors))
715 .collect();
716 longest_increasing_by_new_anchored(&pairs, &is_anchor)
717 };
718 if lis.is_empty() { None } else { Some(lis) }
719}
720
721/// Whether `line` begins with any of the `--anchored` prefixes (git's
722/// `is_anchor`: a byte-prefix `strncmp` against the line's content, trailing
723/// newline included). An empty anchor prefix matches every line, matching git.
724fn line_matches_anchor(line: &[u8], anchors: &[Vec<u8>]) -> bool {
725 anchors.iter().any(|anchor| line.starts_with(anchor))
726}
727
728/// Longest increasing subsequence of `pairs` (sorted by old-index) keyed on the
729/// new-index, returned as the chosen (old_index, new_index) pairs in order.
730///
731/// This is the patience-sorting core: standard O(k log k) LIS with predecessor
732/// links so the actual subsequence (not just its length) is recovered. Because
733/// the input is pre-sorted by old-index and the new-indices are distinct, the
734/// result is strictly increasing in both coordinates.
735fn longest_increasing_by_new(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
736 if pairs.is_empty() {
737 return Vec::new();
738 }
739 // tails[len-1] = index into `pairs` of the smallest possible tail value of
740 // an increasing subsequence of length `len`.
741 let mut tails: Vec<usize> = Vec::new();
742 // prev[i] = index into `pairs` of the predecessor of pairs[i] in its LIS.
743 let mut prev: Vec<Option<usize>> = vec![None; pairs.len()];
744
745 for i in 0..pairs.len() {
746 let val = pairs[i].1;
747 // Binary search for the first tail whose new-index is >= val.
748 let mut lo = 0usize;
749 let mut hi = tails.len();
750 while lo < hi {
751 let mid = lo + (hi - lo) / 2;
752 if pairs[tails[mid]].1 < val {
753 lo = mid + 1;
754 } else {
755 hi = mid;
756 }
757 }
758 if lo > 0 {
759 prev[i] = Some(tails[lo - 1]);
760 }
761 if lo == tails.len() {
762 tails.push(i);
763 } else {
764 tails[lo] = i;
765 }
766 }
767
768 // Reconstruct by following predecessor links from the last tail.
769 let mut result: Vec<(usize, usize)> = Vec::with_capacity(tails.len());
770 let mut cur = tails.last().copied();
771 while let Some(i) = cur {
772 result.push(pairs[i]);
773 cur = prev[i];
774 }
775 result.reverse();
776 result
777}
778
779/// Longest increasing subsequence of `pairs` (sorted by old-index, keyed on the
780/// new-index) that is *forced* to pass through every includible anchor.
781///
782/// A direct port of git's anchored `find_longest_common_sequence`
783/// (xdiff/xpatience.c): entries are processed in old-index order and placed into
784/// the patience-sort `sequence` by their new-index. When an anchor entry
785/// (`is_anchor[i]`) is placed at position `k`, `anchor_i` is pinned to `k` and
786/// the running length is forced to `k + 1`; thereafter positions `<= anchor_i`
787/// can never be overridden, so the result must contain that anchor. A later
788/// anchor whose placement would fall at or before `anchor_i` is skipped, exactly
789/// matching git's greedy handling of mutually-incompatible anchors.
790fn longest_increasing_by_new_anchored(
791 pairs: &[(usize, usize)],
792 is_anchor: &[bool],
793) -> Vec<(usize, usize)> {
794 if pairs.is_empty() {
795 return Vec::new();
796 }
797 // sequence[k] = index into `pairs` of the smallest-new-index tail of an
798 // increasing subsequence of length k+1; `prev` links to the predecessor.
799 let mut sequence: Vec<usize> = Vec::with_capacity(pairs.len());
800 let mut prev: Vec<Option<usize>> = vec![None; pairs.len()];
801 let mut longest: usize = 0;
802 let mut anchor_i: isize = -1;
803 for (e, &(_, val)) in pairs.iter().enumerate() {
804 // i = largest position in sequence[0..longest] whose new-index < val,
805 // or -1 if none (git's fast-path + `binary_search`).
806 let i: isize = if longest == 0 || val > pairs[sequence[longest - 1]].1 {
807 longest as isize - 1
808 } else {
809 let mut lo = 0usize;
810 let mut hi = longest;
811 while lo < hi {
812 let mid = lo + (hi - lo) / 2;
813 if pairs[sequence[mid]].1 < val {
814 lo = mid + 1;
815 } else {
816 hi = mid;
817 }
818 }
819 lo as isize - 1
820 };
821 prev[e] = if i < 0 {
822 None
823 } else {
824 Some(sequence[i as usize])
825 };
826 let pos = (i + 1) as usize;
827 if (pos as isize) <= anchor_i {
828 continue;
829 }
830 if pos == sequence.len() {
831 sequence.push(e);
832 } else {
833 sequence[pos] = e;
834 }
835 if is_anchor[e] {
836 anchor_i = pos as isize;
837 longest = pos + 1;
838 } else if pos == longest {
839 longest += 1;
840 }
841 }
842 if longest == 0 {
843 return Vec::new();
844 }
845 let mut result: Vec<(usize, usize)> = Vec::with_capacity(longest);
846 let mut cur = Some(sequence[longest - 1]);
847 while let Some(i) = cur {
848 result.push(pairs[i]);
849 cur = prev[i];
850 }
851 result.reverse();
852 result
853}
854
855/// Recursive histogram-diff worker over `old[a0..a1]` vs `new[b0..b1]`.
856fn histogram_recurse(
857 old: &[DiffLine<'_>],
858 new: &[DiffLine<'_>],
859 a0: usize,
860 a1: usize,
861 b0: usize,
862 b1: usize,
863 out: &mut Vec<DiffOp>,
864) {
865 if emit_trivial_range(a0, a1, b0, b1, out) {
866 return;
867 }
868 let (a0, a1, b0, b1, suffix) = trim_common(old, new, a0, a1, b0, b1, out);
869 if !emit_trivial_range(a0, a1, b0, b1, out) {
870 match histogram_region(old, new, a0, a1, b0, b1) {
871 Some(region) => {
872 // Recurse left of the matched run, emit the run as Equal, then
873 // recurse right of it.
874 histogram_recurse(old, new, a0, region.old_start, b0, region.new_start, out);
875 out.push(DiffOp::Equal(region.len));
876 histogram_recurse(
877 old,
878 new,
879 region.old_start + region.len,
880 a1,
881 region.new_start + region.len,
882 b1,
883 out,
884 );
885 }
886 // No common line at all in this range: hand it to Myers.
887 None => myers_core(&old[a0..a1], &new[b0..b1], out),
888 }
889 }
890 if suffix > 0 {
891 out.push(DiffOp::Equal(suffix));
892 }
893}
894
895/// The longest common run chosen by the histogram heuristic for one range.
896struct HistogramRegion {
897 old_start: usize,
898 new_start: usize,
899 len: usize,
900}
901
902/// Choose the histogram anchor run for `old[a0..a1]` vs `new[b0..b1]`.
903///
904/// Builds an occurrence histogram of the `old` range, then scans the `new`
905/// range. For each `new` line that also appears in `old`, it extends a matching
906/// run backward and forward and scores candidate alignments, preferring the run
907/// whose anchoring `old` line has the *fewest* occurrences (ties broken by run
908/// length, then by earliest position). This is the JGit/`git --histogram`
909/// heuristic: rare lines make the most reliable anchors. Returns `None` if no
910/// `new` line appears in the `old` range.
911fn histogram_region(
912 old: &[DiffLine<'_>],
913 new: &[DiffLine<'_>],
914 a0: usize,
915 a1: usize,
916 b0: usize,
917 b1: usize,
918) -> Option<HistogramRegion> {
919 // Occurrence count and the list of positions of each line within old[a0..a1].
920 let mut buckets: HashMap<LineKey<'_>, Vec<usize>> = HashMap::new();
921 for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
922 buckets.entry(line_key(line)).or_default().push(i);
923 }
924
925 let mut best: Option<HistogramRegion> = None;
926 // Lower occurrence count is better; among equal counts, longer run wins.
927 let mut best_count = usize::MAX;
928 let mut best_len = 0usize;
929
930 let mut bj = b0;
931 while bj < b1 {
932 let key = line_key(&new[bj]);
933 let Some(positions) = buckets.get(&key) else {
934 bj += 1;
935 continue;
936 };
937 let occ = positions.len();
938 // For every place this line sits in `old`, measure the maximal matching
939 // run that passes through (positions[*], bj).
940 let mut next_bj = bj + 1;
941 for &ai in positions {
942 // Extend backward while lines keep matching and we stay in range.
943 let mut start_a = ai;
944 let mut start_b = bj;
945 while start_a > a0 && start_b > b0 && old[start_a - 1] == new[start_b - 1] {
946 start_a -= 1;
947 start_b -= 1;
948 }
949 // Extend forward from the run start.
950 let mut len = 0usize;
951 while start_a + len < a1
952 && start_b + len < b1
953 && old[start_a + len] == new[start_b + len]
954 {
955 len += 1;
956 }
957 // Score this run by the rarest occurrence count along it; using the
958 // anchor line's own count is the standard, cheaper approximation.
959 let run_count = occ;
960 let better = run_count < best_count || (run_count == best_count && len > best_len);
961 if better && len > 0 {
962 best_count = run_count;
963 best_len = len;
964 best = Some(HistogramRegion {
965 old_start: start_a,
966 new_start: start_b,
967 len,
968 });
969 // Skip past this matched run in `new` so we do not re-evaluate
970 // every interior line of the same run from scratch.
971 if start_b + len > next_bj {
972 next_bj = start_b + len;
973 }
974 }
975 }
976 bj = next_bj.max(bj + 1);
977 }
978
979 best
980}
981#[derive(Debug, Clone, Copy, PartialEq, Eq)]
982pub enum DiffAlgorithm {
983 Myers,
984 Minimal,
985 Patience,
986 Histogram,
987}