1use crate::model::Hunk;
8
9pub 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 let mut eof_nonl_new: Option<bool> = None;
29 for h in order {
30 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 Some(true) => {
50 if out.last().is_some_and(|l| l.is_empty()) {
51 out.pop();
52 }
53 }
54 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 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 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 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 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 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}