Skip to main content

differential_engine/
apply.rs

1//! Byte-exact hunk applier.
2//!
3//! CRLF asymmetry, by design: this module is byte-faithful — `\r` stays attached
4//! to line content and round-trips exactly. Only the shape normaliser
5//! (`shape.rs`) is CRLF-agnostic. The two modules disagree on purpose.
6
7use crate::model::Hunk;
8
9/// Apply a subset of one file's hunks to its base content. Hunks are disjoint
10/// under `-U0`.
11///
12/// An absent file's base is ONE EMPTY LINE, not zero lines: `b"".split(b'\n')`
13/// yields `[b""]`, and that trailing empty element is what encodes "ends with a
14/// newline". Dropping it loses the final byte of every created file.
15pub fn apply_hunks(base: Option<&[u8]>, hunks: &[&Hunk]) -> Vec<u8> {
16    let lines: Vec<&[u8]> = match base {
17        Some(b) => b.split(|&c| c == b'\n').collect(),
18        None => vec![b"".as_slice()],
19    };
20
21    let mut order: Vec<&Hunk> = hunks.to_vec();
22    order.sort_by_key(|h| (h.old_start, h.old_count));
23
24    let mut out: Vec<&[u8]> = Vec::with_capacity(lines.len());
25    let mut pos = 0usize;
26    // Set when the applied hunk set includes the EOF-covering hunk (only that
27    // hunk can carry a no-newline marker); the value is the NEW side's state.
28    let mut eof_nonl_new: Option<bool> = None;
29    for h in order {
30        // old_count == 0 means "insert after line old_start"; otherwise the hunk
31        // replaces starting at old_start (1-based).
32        let start = if h.old_count == 0 {
33            h.old_start as usize
34        } else {
35            (h.old_start as usize) - 1
36        };
37        out.extend_from_slice(&lines[pos..start]);
38        out.extend(h.added.iter().map(Vec::as_slice));
39        pos = start + h.old_count as usize;
40        if h.nonl_old || h.nonl_new {
41            eof_nonl_new = Some(h.nonl_new);
42        }
43    }
44    out.extend_from_slice(&lines[pos..]);
45
46    match eof_nonl_new {
47        // New content ends without a newline: drop the empty tail element so
48        // the join does not reintroduce one.
49        Some(true) => {
50            if out.last().is_some_and(|l| l.is_empty()) {
51                out.pop();
52            }
53        }
54        // The edit ADDED the final newline (old side had the marker, new side
55        // does not): the base's line list has no empty tail element, so the
56        // join needs one appended to produce it.
57        Some(false) => out.push(b""),
58        None => {}
59    }
60    out.join(b"\n".as_slice())
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    fn hunk(os: u32, oc: u32, ns: u32, nc: u32, add: &[&[u8]], nonl_new: bool) -> Hunk {
68        Hunk {
69            file: 0,
70            old_start: os,
71            old_count: oc,
72            new_start: ns,
73            new_count: nc,
74            removed: vec![],
75            added: add.iter().map(|l| l.to_vec()).collect(),
76            nonl_old: false,
77            nonl_new,
78        }
79    }
80
81    #[test]
82    fn created_file_with_trailing_newline() {
83        let h = hunk(0, 0, 1, 2, &[b"a", b"b"], false);
84        assert_eq!(apply_hunks(None, &[&h]), b"a\nb\n");
85    }
86
87    #[test]
88    fn created_file_without_trailing_newline() {
89        let h = hunk(0, 0, 1, 2, &[b"a", b"b"], true);
90        assert_eq!(apply_hunks(None, &[&h]), b"a\nb");
91    }
92
93    #[test]
94    fn replace_middle_line() {
95        let h = hunk(2, 1, 2, 1, &[b"B"], false);
96        assert_eq!(apply_hunks(Some(b"a\nb\nc\n"), &[&h]), b"a\nB\nc\n");
97    }
98
99    #[test]
100    fn insert_at_top_of_file() {
101        // @@ -0,0 +1,1 @@ — insertion before line 1.
102        let h = hunk(0, 0, 1, 1, &[b"first"], false);
103        assert_eq!(apply_hunks(Some(b"a\n"), &[&h]), b"first\na\n");
104    }
105
106    #[test]
107    fn delete_to_empty_file() {
108        let h = hunk(1, 2, 0, 0, &[], false);
109        assert_eq!(apply_hunks(Some(b"a\nb\n"), &[&h]), b"");
110    }
111
112    #[test]
113    fn whole_file_delete_of_nonl_file() {
114        // Base has no trailing newline; deleting everything must still be b"".
115        // Real git marks the old side: `-only` + `\ No newline at end of file`.
116        let mut h = hunk(1, 1, 0, 0, &[], false);
117        h.nonl_old = true;
118        assert_eq!(apply_hunks(Some(b"only"), &[&h]), b"");
119    }
120
121    #[test]
122    fn delete_final_nonl_line_restores_trailing_newline() {
123        // Base "a\nlast" (no final newline); deleting `last` leaves "a\n".
124        let mut h = hunk(2, 1, 1, 0, &[], false);
125        h.nonl_old = true;
126        assert_eq!(apply_hunks(Some(b"a\nlast"), &[&h]), b"a\n");
127    }
128
129    #[test]
130    fn newline_removed_from_final_line() {
131        // -last / +last with nonl_new: content identical, exactly 1 byte shorter.
132        let mut h = hunk(2, 1, 2, 1, &[b"last"], true);
133        h.removed = vec![b"last".to_vec()];
134        assert_eq!(apply_hunks(Some(b"a\nlast\n"), &[&h]), b"a\nlast");
135    }
136
137    #[test]
138    fn newline_added_to_final_line() {
139        // `-last` + marker, `+last` without: the edit adds exactly one byte.
140        let mut h = hunk(2, 1, 2, 1, &[b"last"], false);
141        h.nonl_old = true;
142        assert_eq!(apply_hunks(Some(b"a\nlast"), &[&h]), b"a\nlast\n");
143    }
144
145    #[test]
146    fn multiple_disjoint_hunks_apply_in_order_regardless_of_input_order() {
147        let h1 = hunk(1, 1, 1, 1, &[b"A"], false);
148        let h2 = hunk(3, 1, 3, 1, &[b"C"], false);
149        let base = b"a\nb\nc\n";
150        assert_eq!(apply_hunks(Some(base), &[&h2, &h1]), b"A\nb\nC\n");
151    }
152
153    #[test]
154    fn crlf_is_byte_faithful() {
155        let h = hunk(1, 1, 1, 1, &[b"new\r"], false);
156        assert_eq!(
157            apply_hunks(Some(b"old\r\nkeep\r\n"), &[&h]),
158            b"new\r\nkeep\r\n"
159        );
160    }
161
162    #[test]
163    fn typechange_delete_plus_create_on_same_base() {
164        let del = hunk(1, 2, 0, 0, &[], false);
165        let mut create = hunk(0, 0, 1, 1, &[b"target"], true);
166        create.nonl_new = true;
167        assert_eq!(
168            apply_hunks(Some(b"real\ncontent\n"), &[&del, &create]),
169            b"target"
170        );
171    }
172}