1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
//! What:
//! The unified-diff parser that replay's subsumption reads:
//! `git diff-tree -p` output over single-level trees named by decimal index,
//! split into hunks per entry name.
//! Why:
//! Subsumption decides from hunk lines and their markers;
//! the parser keeps every byte exact,
//! so a hunk line round-trips to the blob bytes it came from.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! parseIndexedPatch(text: string): ReadonlyMap<string, readonly PatchHunk[]>
//! ```
/// `BTreeMap` keeps the parsed sections in a deterministic order for tests.
use std::collections::BTreeMap;
/// Owned byte strings for lines.
use std::fmt;
/// What:
/// One patch line:
/// its marker (`b' '`, `b'-'`, `b'+'`) and its bytes.
/// Why:
/// The reverse check and the containment check both read the markers.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type PatchLine = { op: ' ' | '-' | '+'; text: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PatchLine {
/// Context, removal, or addition.
pub op: u8,
/// Line bytes with their newline,
/// without it for an incomplete last line.
pub text: Vec<u8>,
}
/// What:
/// One unified-diff hunk:
/// its first old and new line and its lines in order.
/// Why:
/// A zero-based start is derived from `oldStart` by the callers that need it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type PatchHunk = { oldStart: number; newStart: number; lines: readonly PatchLine[] };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PatchHunk {
/// First line of the old side, `0` for an empty old side.
pub old_start: i64,
/// First line of the new side, `0` for an empty new side.
pub new_start: i64,
/// Hunk lines in order.
pub lines: Vec<PatchLine>,
}
/// What:
/// Patch output that does not have the shape `git diff-tree -p` writes.
/// Why:
/// Callers fail the replay rather than guess at a truncated or foreign patch.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class ReplayPatchFormatError extends Error {}
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReplayPatchFormatError(pub String);
impl fmt::Display for ReplayPatchFormatError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
return write!(formatter, "{}", self.0);
}
}
/// Section header of one file in `diff-tree -p` output.
const SECTION_HEADER: &[u8] = b"diff --git a/";
/// Hunk header prefix.
const HUNK_HEADER: &[u8] = b"@@ -";
/// Marker of a `\ No newline at end of file` line.
const INCOMPLETE_MARKER: u8 = b'\\';
/// What:
/// Split bytes into lines,
/// each keeping its newline;
/// an unterminated last line stays as it is.
/// Why:
/// Hunk bodies and blob contents are compared line by line,
/// newline included, the way Git's `xdiff` compares them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// splitKeepingNewlines('a\nb'); // ['a\n', 'b']
/// ```
pub fn split_keeping_newlines(text: &[u8]) -> Vec<Vec<u8>> {
// `mut` allows collecting lines while walking the bytes once.
let mut lines: Vec<Vec<u8>> = Vec::new();
let mut start: usize = 0;
while start < text.len() {
let stop: usize = match text[start..].iter().position(|byte| { return *byte == b'\n'; }) {
Some(offset) => start + offset + 1,
None => text.len(),
};
lines.push(text[start..stop].to_vec());
start = stop;
}
return lines;
}
/// What:
/// Parse one non-negative decimal number,
/// rejecting anything but ASCII digits.
/// Why:
/// Git prints only digits in hunk ranges;
/// any other byte means the patch is not one Git wrote.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// Number(text) // must be a safe integer
/// ```
fn parse_number(text: &[u8]) -> Option<i64> {
if text.is_empty() || !text.iter().all(|byte| { return byte.is_ascii_digit(); }) {
return None;
}
// `to_str` is safe here: every byte is an ASCII digit.
let digits: &str = std::str::from_utf8(text).ok()?;
return digits.parse::<i64>().ok();
}
/// What:
/// Parse one `-start[,count]` or `+start[,count]` range;
/// a missing count is `1`.
/// Why:
/// Hunk headers name both sides this way,
/// and the counts tell the body parser how many lines to read.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// parseRange('-3,2'); // { start: 3, count: 2 }
/// ```
fn parse_range(range: &[u8]) -> Result<(i64, i64), ReplayPatchFormatError> {
let body: &[u8] = &range[1..];
let (start_text, count_text): (&[u8], &[u8]) = match body.iter().position(|byte| { return *byte == b','; }) {
Some(comma) => (&body[..comma], &body[comma + 1..]),
None => (body, b"1"),
};
return match (parse_number(start_text), parse_number(count_text)) {
(Some(start), Some(count)) => Ok((start, count)),
_ => Err(ReplayPatchFormatError(format!(
"malformed hunk range {}",
String::from_utf8_lossy(range)
))),
};
}
/// What:
/// Drop the newline of the last parsed line,
/// as a following `\ No newline at end of file` marker says.
/// Why:
/// An incomplete last line compares as a prefix of the file line that has the newline.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// markIncomplete(parsed); // throws when no line precedes the marker
/// ```
fn mark_incomplete(parsed: &mut [PatchLine]) -> Result<(), ReplayPatchFormatError> {
let last: &mut PatchLine = match parsed.last_mut() {
Some(line) => line,
None => {
return Err(ReplayPatchFormatError(String::from(
"an incomplete-line marker precedes every hunk line",
)));
}
};
if last.text.last() == Some(&b'\n') {
last.text.pop();
}
return Ok(());
}
/// What:
/// Parse one hunk starting at its header line;
/// returns the hunk and the index of the line after it.
/// Why:
/// Every body line is read once,
/// counted down from the header's old and new counts,
/// so a truncated or overlong hunk fails instead of swallowing the next section.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// parseHunk({ lines, start }); // { hunk, next }
/// ```
fn parse_hunk(lines: &[Vec<u8>], start: usize) -> Result<(PatchHunk, usize), ReplayPatchFormatError> {
let header: &[u8] = &lines[start];
let end: usize = match find_bytes(header, b" @@", 4) {
Some(found) => found,
None => {
return Err(ReplayPatchFormatError(format!(
"malformed hunk header {}",
String::from_utf8_lossy(header).trim_end()
)));
}
};
let ranges: &[u8] = &header[3..end];
let (old_range, new_range): (&[u8], &[u8]) = match ranges.iter().position(|byte| { return *byte == b' '; }) {
Some(space) => (&ranges[..space], &ranges[space + 1..]),
None => {
return Err(ReplayPatchFormatError(format!(
"malformed hunk header {}",
String::from_utf8_lossy(header).trim_end()
)));
}
};
let (old_start, old_count): (i64, i64) = parse_range(old_range)?;
let (new_start, new_count): (i64, i64) = parse_range(new_range)?;
// `mut` allows the counts and the cursor to count down while the body is read.
let mut parsed: Vec<PatchLine> = Vec::new();
let mut old_left: i64 = old_count;
let mut new_left: i64 = new_count;
let mut cursor: usize = start + 1;
while old_left > 0 || new_left > 0 {
let line: &[u8] = match lines.get(cursor) {
Some(found) => found,
None => {
return Err(ReplayPatchFormatError(format!(
"hunk {} is truncated",
String::from_utf8_lossy(header).trim_end()
)));
}
};
cursor += 1;
let marker: u8 = line_marker(line)?;
if marker == INCOMPLETE_MARKER {
mark_incomplete(&mut parsed)?;
continue;
}
if marker != b'+' {
old_left -= 1;
}
if marker != b'-' {
new_left -= 1;
}
let text: Vec<u8> = if line == b"\n" { b"\n".to_vec() } else { line[1..].to_vec() };
parsed.push(PatchLine { op: marker, text });
}
if lines.get(cursor).is_some_and(|line| { return line.starts_with(b"\\"); }) {
mark_incomplete(&mut parsed)?;
cursor += 1;
}
return Ok((
PatchHunk {
old_start,
new_start,
lines: parsed,
},
cursor,
));
}
/// What:
/// The marker of one hunk body line:
/// a bare newline is an empty context line,
/// and an incomplete-line marker is reported as `\`.
/// Why:
/// A line without one of the four markers cannot belong to a hunk body.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// lineMarker('\n'); // ' '
/// ```
fn line_marker(line: &[u8]) -> Result<u8, ReplayPatchFormatError> {
let first: u8 = if line == b"\n" { b' ' } else { line.first().copied().unwrap_or(0) };
if matches!(first, b' ' | b'-' | b'+' | b'\\') {
return Ok(first);
}
return Err(ReplayPatchFormatError(format!(
"hunk line {} has no diff marker",
String::from_utf8_lossy(line)
)));
}
/// What:
/// The first index at or after `from` where `needle` starts in `haystack`.
/// Why:
/// The hunk header's range end is searched from after the `@@ -` prefix.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// haystack.indexOf(needle, from)
/// ```
fn find_bytes(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
if haystack.len() < needle.len() {
return None;
}
return (from..=haystack.len() - needle.len()).find(|index| { return &haystack[*index..*index + needle.len()] == needle; });
}
/// What:
/// Parse `git diff-tree -p` output over index-named single-level trees
/// into hunks per entry name;
/// an entry without a section has no change.
/// Why:
/// Subsumption reads each candidate's landed and prepared hunks by its index name,
/// so no patch header carries a user path.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// parseIndexedPatch(text); // ReadonlyMap<string, readonly PatchHunk[]>
/// ```
pub fn parse_indexed_patch(
text: &[u8],
) -> Result<BTreeMap<Vec<u8>, Vec<PatchHunk>>, ReplayPatchFormatError> {
let lines: Vec<Vec<u8>> = split_keeping_newlines(text);
// `mut` allows the sections and the cursor to advance.
let mut files: BTreeMap<Vec<u8>, Vec<PatchHunk>> = BTreeMap::new();
let mut section: Option<Vec<u8>> = None;
let mut hunks: Vec<PatchHunk> = Vec::new();
let mut cursor: usize = 0;
while cursor < lines.len() {
let line: &[u8] = &lines[cursor];
if line.starts_with(HUNK_HEADER) {
let (hunk, next): (PatchHunk, usize) = parse_hunk(&lines, cursor)?;
hunks.push(hunk);
cursor = next;
continue;
}
if line.starts_with(SECTION_HEADER) {
if let Some(name) = section.take() {
files.insert(name, std::mem::take(&mut hunks));
}
hunks.clear();
let rest: &[u8] = &line[SECTION_HEADER.len()..];
let name_end: usize = match rest.iter().position(|byte| { return *byte == b' '; }) {
Some(space) => space,
None => {
return Err(ReplayPatchFormatError(format!(
"section header {} has no destination",
String::from_utf8_lossy(line).trim_end()
)));
}
};
section = Some(rest[..name_end].to_vec());
}
cursor += 1;
}
if let Some(name) = section {
files.insert(name, hunks);
}
return Ok(files);
}
#[cfg(test)]
#[path = "replay_patch_tests.rs"]
mod tests;