Skip to main content

git_policy_cli/
replay_reverse_apply.rs

1//! What:
2//!  The exact emulation of `git apply --reverse --check` for one text file:
3//!       the subsumption test that asks whether a landed change already sits in the prepared bytes.
4//! Why:
5//!  Git's `apply.c` matching (`apply_one_fragment`, `find_pos`, `match_fragment`) with its defaults:
6//!       no context reduction,
7//!       no whitespace fixing,
8//!       no overlapping hunks,
9//!       and anchoring at the file's start and end.
10//!       Replay must give the same answer as Git for the same bytes.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! reverseApplies({ hunks, prepared }): boolean
15//! ```
16
17/// Hunks and lines from `replay_patch`.
18use super::replay_patch::{PatchHunk, split_keeping_newlines};
19
20/// One image line:
21///       its bytes and whether an earlier hunk wrote it,
22///       so no later hunk may match it.
23/// Why:
24///   Git marks patched lines in the image and never matches them again.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// type ImageLine = { text: string; patched: boolean };
29/// ```
30#[derive(Clone, Debug, Eq, PartialEq)]
31struct ImageLine {
32    /// Line bytes.
33    text: Vec<u8>,
34    /// Written by an earlier hunk.
35    patched: bool,
36}
37
38/// What:
39///  The bytes Git's quick line hash ignores:
40///       spaces, tabs, newlines, vertical tabs, form feeds and carriage returns.
41/// Why:
42///   An incomplete last preimage line matches an image line with the same non-whitespace content.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// hashKey(text) // text without HASH_WHITESPACE
47/// ```
48fn hash_key(text: &[u8]) -> Vec<u8> {
49    return text
50        .iter()
51        .copied()
52        .filter(|byte| { return !matches!(*byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r'); })
53        .collect();
54}
55
56/// What:
57///  Whether one image line matches one preimage line,
58///       as Git's quick hash check and `memcmp` together decide it.
59/// Why:
60///   A preimage's incomplete last line only has to be a prefix of its image line
61///       whose non-whitespace content is equal,
62///       unless the hunk is anchored at the end, where the line must be equal.
63///
64/// In TS you'd write (pseudocode):
65/// ```ts
66/// lineMatches({ line, wanted, incompleteLast, matchEnd });
67/// ```
68fn line_matches(line: &ImageLine, wanted: &[u8], incomplete_last: bool, match_end: bool) -> bool {
69    if line.patched {
70        return false;
71    }
72    if !incomplete_last || match_end {
73        return line.text == wanted;
74    }
75    return line.text.starts_with(wanted) && hash_key(&line.text) == hash_key(wanted);
76}
77
78/// What:
79///  Git's `match_fragment` without whitespace fixing:
80///       whether the preimage matches the image starting at `at`.
81/// Why:
82///   The beginning and end anchors are checked before any line.
83///
84/// In TS you'd write (pseudocode):
85/// ```ts
86/// matchesAt({ image, preimage, at, matchBeginning, matchEnd });
87/// ```
88fn matches_at(
89    image: &[ImageLine],
90    preimage: &[Vec<u8>],
91    at: usize,
92    match_beginning: bool,
93    match_end: bool,
94) -> bool {
95    let end: usize = preimage.len() + at;
96    if end > image.len() || (match_end && end != image.len()) || (match_beginning && at != 0) {
97        return false;
98    }
99    return preimage.iter().enumerate().all(|(index, wanted)| {
100        let incomplete_last: bool = index + 1 == preimage.len() && !wanted.ends_with(b"\n");
101        return line_matches(&image[at + index], wanted, incomplete_last, match_end);
102    });
103}
104
105/// What:
106///  Git's `find_pos`:
107///       the expected line first,
108///       then one line further forwards and backwards in turn.
109/// Why:
110///   A hunk whose context moved a few lines still applies where Git finds it,
111///       so the subsumption answer matches Git's.
112///
113/// In TS you'd write (pseudocode):
114/// ```ts
115/// findPosition({ image, preimage, line, matchBeginning, matchEnd }); // first match or -1
116/// ```
117fn find_position(
118    image: &[ImageLine],
119    preimage: &[Vec<u8>],
120    line: i64,
121    match_beginning: bool,
122    match_end: bool,
123) -> Option<usize> {
124    let length: i64 = i64::try_from(image.len()).unwrap_or(i64::MAX);
125    let preimage_length: i64 = i64::try_from(preimage.len()).unwrap_or(i64::MAX);
126    let anchored: i64 = if match_beginning {
127        0
128    } else if match_end {
129        length - preimage_length
130    } else {
131        line
132    };
133    let first: i64 = anchored.max(0).min(length);
134    let matches = |at: i64| -> Option<usize> {
135        if at < 0 || at > length {
136            return None;
137        }
138        let index: usize = usize::try_from(at).ok()?;
139        return matches_at(image, preimage, index, match_beginning, match_end).then_some(index);
140    };
141    if let Some(found) = matches(first) {
142        return Some(found);
143    }
144    // `mut` lets the search widen by one line per side until it passes both ends of the image.
145    let mut distance: i64 = 1;
146    while first + distance <= length || first - distance >= 0 {
147        if let Some(found) = matches(first + distance) {
148            return Some(found);
149        }
150        if let Some(found) = matches(first - distance) {
151            return Some(found);
152        }
153        distance += 1;
154    }
155    return None;
156}
157
158/// What:
159///  Apply one hunk in reverse to the image:
160///       its new side (context and additions) must be found,
161///       and its old side (context and removals) takes its place.
162/// Why:
163///   `reverse_patches` swaps the sides before Git searches,
164///       so the search starts from the old side's line and anchors on the new side's start.
165///
166/// In TS you'd write (pseudocode):
167/// ```ts
168/// reverseHunk({ image, hunk }); // image or HUNK_REJECTED
169/// ```
170fn reverse_hunk(image: &[ImageLine], hunk: &PatchHunk) -> Option<Vec<ImageLine>> {
171    let preimage: Vec<Vec<u8>> = hunk
172        .lines
173        .iter()
174        .filter(|line| { return line.op != b'-'; })
175        .map(|line| { return line.text.clone(); })
176        .collect();
177    let postimage: Vec<ImageLine> = hunk
178        .lines
179        .iter()
180        .filter(|line| { return line.op != b'+'; })
181        .map(|line| { return ImageLine {
182            text: line.text.clone(),
183            patched: true,
184        }; })
185        .collect();
186    let trailing: usize = match hunk.lines.iter().rposition(|line| { return line.op != b' '; }) {
187        Some(last_change) => hunk.lines.len() - 1 - last_change,
188        None => hunk.lines.len(),
189    };
190    let line: i64 = if hunk.old_start == 0 { 0 } else { hunk.old_start - 1 };
191    let at: usize = find_position(
192        image,
193        &preimage,
194        line,
195        hunk.new_start <= 1,
196        trailing == 0,
197    )?;
198    // `mut` lets the reversed image be built in order.
199    let mut rebuilt: Vec<ImageLine> = image[..at].to_vec();
200    rebuilt.extend(postimage);
201    rebuilt.extend_from_slice(&image[at + preimage.len()..]);
202    return Some(rebuilt);
203}
204
205/// What:
206///  Whether every hunk of a landed change applies in reverse to the prepared bytes,
207///       in order, so the prepared bytes already contain the landed change.
208/// Why:
209///   Subsumption's strict check; the one-sided extension in `replay_containment` covers what this rejects.
210///
211/// In TS you'd write (pseudocode):
212/// ```ts
213/// reverseApplies({ hunks, prepared: 'a\nB\nc\nd\n' });
214/// ```
215pub fn reverse_applies(hunks: &[PatchHunk], prepared: &[u8]) -> bool {
216    // `mut` lets the image be replaced by each hunk's result.
217    let mut image: Vec<ImageLine> = split_keeping_newlines(prepared)
218        .into_iter()
219        .map(|text| { return ImageLine {
220            text,
221            patched: false,
222        }; })
223        .collect();
224    for hunk in hunks {
225        match reverse_hunk(&image, hunk) {
226            Some(next) => image = next,
227            None => return false,
228        }
229    }
230    return true;
231}
232
233#[cfg(test)]
234#[path = "replay_reverse_apply_tests.rs"]
235mod tests;