1use std::{io::BufRead, ops::Range};
2
3use anyhow::{Context, Result, bail};
4use git2::{Delta, Diff, Patch};
5use serde::Serialize;
6
7mod rename_names;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
10pub enum Kind {
11 Context,
12 Addition,
13 Deletion,
14}
15
16#[derive(Debug, Serialize)]
17pub struct SourceLine {
18 pub kind: Kind,
19 pub old: Option<u32>,
20 pub new: Option<u32>,
21 pub bytes: Vec<u8>,
22 pub no_newline: bool,
23 #[serde(skip)]
24 pub offset: usize,
25}
26
27#[derive(Debug, Serialize)]
28pub struct Hunk {
29 pub header: String,
30 pub old_start: u32,
31 pub new_start: u32,
32 pub lines: Range<usize>,
33}
34
35#[derive(Debug, Serialize)]
36pub struct FileDiff {
37 #[serde(skip)]
38 pub(crate) identity: PatchIdentity,
39 #[serde(skip)]
40 pub raw_path: Vec<u8>,
41 pub path: String,
42 pub old_path: String,
43 pub status: String,
44 pub old_mode: String,
45 pub new_mode: String,
46 pub additions: usize,
47 pub deletions: usize,
48 pub digits: usize,
49 pub hunks: Vec<Hunk>,
50}
51
52#[derive(Clone, Debug, Default)]
53pub(crate) struct PatchIdentity {
54 pub old_path: Vec<u8>,
55 pub new_path: Vec<u8>,
56 pub old_oid: Option<git2::Oid>,
57 pub new_oid: Option<git2::Oid>,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum RowKind<S = usize> {
62 Header,
63 Hunk(usize),
64 Gap,
65 Metadata,
66 Code { left: Option<S>, right: Option<S> },
67 NoNewline { left: bool, right: bool },
68}
69
70#[derive(Clone, Copy, Debug)]
71pub struct Row<S = usize> {
72 pub file: usize,
73 pub kind: RowKind<S>,
74}
75
76impl<S> Row<S> {
77 pub fn map<T>(self, mut source: impl FnMut(S) -> T) -> Row<T> {
78 Row {
79 file: self.file,
80 kind: match self.kind {
81 RowKind::Header => RowKind::Header,
82 RowKind::Hunk(h) => RowKind::Hunk(h),
83 RowKind::Gap => RowKind::Gap,
84 RowKind::Metadata => RowKind::Metadata,
85 RowKind::Code { left, right } => RowKind::Code {
86 left: left.map(&mut source),
87 right: right.map(source),
88 },
89 RowKind::NoNewline { left, right } => RowKind::NoNewline { left, right },
90 },
91 }
92 }
93}
94
95#[derive(Debug, Default, Serialize)]
96pub struct Document {
97 pub files: Vec<FileDiff>,
98 pub lines: Vec<SourceLine>,
99 #[serde(skip)]
100 pub split: Vec<Row>,
101 #[serde(skip)]
102 pub unified: Vec<Row>,
103 #[serde(skip)]
104 pub split_locations: Vec<usize>,
105 #[serde(skip)]
106 pub unified_locations: Vec<usize>,
107}
108
109impl Document {
110 pub fn parse(input: &[u8]) -> Result<Self> {
111 Self::read(std::io::Cursor::new(input))
112 }
113
114 pub fn read(input: impl BufRead) -> Result<Self> {
115 let mut doc = Self::default();
116 crate::ingest::visit_files(input, |_, unit| {
117 let mut part = Self::parse_file(unit)?;
118 let base = doc.lines.len();
119 for file in &mut part.files {
120 for hunk in &mut file.hunks {
121 hunk.lines = (hunk.lines.start + base)..(hunk.lines.end + base);
122 }
123 }
124 doc.lines.append(&mut part.lines);
125 doc.files.append(&mut part.files);
126 Ok(())
127 })?;
128 doc.index_rows();
129 Ok(doc)
130 }
131
132 pub fn parse_file(input: &[u8]) -> Result<Self> {
133 if input.iter().all(u8::is_ascii_whitespace) {
134 return Ok(Self::default());
135 }
136 if input
137 .split(|b| *b == b'\n')
138 .any(|l| l.starts_with(b"diff --cc ") || l.starts_with(b"diff --combined "))
139 {
140 bail!("Combined merge diffs are not supported. Supply a two-sided patch.");
141 }
142 let normalized = empty_file_headers(input);
143 let normalized = normalized.as_deref().unwrap_or(input);
144 let quoted = rename_names::quote_rename_names(normalized);
145 let diff = Diff::from_buffer(quoted.as_ref().map_or(normalized, |q| &q.bytes))
146 .context("Cannot parse this patch")?;
147 if diff.deltas().len() == 0 {
148 bail!("The input contains no recognized file changes.");
149 }
150 let mut doc = Self::default();
151 for (file_index, delta) in diff.deltas().enumerate() {
152 let old_path =
153 crate::text::safe_label(delta.old_file().path_bytes().unwrap_or(b"/dev/null"));
154 let path =
155 crate::text::safe_label(delta.new_file().path_bytes().unwrap_or(b"/dev/null"));
156 let mut file = FileDiff {
157 identity: PatchIdentity {
158 old_path: delta
159 .old_file()
160 .path_bytes()
161 .unwrap_or(b"/dev/null")
162 .to_vec(),
163 new_path: delta
164 .new_file()
165 .path_bytes()
166 .unwrap_or(b"/dev/null")
167 .to_vec(),
168 old_oid: (!delta.old_file().id().is_zero()).then(|| delta.old_file().id()),
169 new_oid: (!delta.new_file().id().is_zero()).then(|| delta.new_file().id()),
170 },
171 raw_path: if delta.status() == Delta::Deleted {
172 delta.old_file().path_bytes().unwrap_or(b"/dev/null")
173 } else {
174 delta.new_file().path_bytes().unwrap_or(b"/dev/null")
175 }
176 .to_vec(),
177 path: if delta.status() == Delta::Deleted {
178 old_path.clone()
179 } else {
180 path
181 },
182 old_path,
183 status: match delta.status() {
184 Delta::Added => "Added",
185 Delta::Deleted => "Deleted",
186 Delta::Renamed => "Renamed",
187 Delta::Copied => "Copied",
188 Delta::Typechange => "Type changed",
189 _ => "Modified",
190 }
191 .into(),
192 old_mode: format!("{:o}", u32::from(delta.old_file().mode())),
193 new_mode: format!("{:o}", u32::from(delta.new_file().mode())),
194 additions: 0,
195 deletions: 0,
196 digits: 3,
197 hunks: Vec::new(),
198 };
199 if let Some(patch) = Patch::from_diff(&diff, file_index)? {
200 let (mut old_end, mut new_end) = (0, 0);
201 for hunk_index in 0..patch.num_hunks() {
202 let (hunk, count) = patch.hunk(hunk_index)?;
203 let old_start = u64::from(hunk.old_start()) + u64::from(hunk.old_lines() == 0);
204 let new_start = u64::from(hunk.new_start()) + u64::from(hunk.new_lines() == 0);
205 if old_start < old_end || new_start < new_end {
206 bail!("Hunks overlap or appear out of source order");
207 }
208 old_end = old_start + u64::from(hunk.old_lines());
209 new_end = new_start + u64::from(hunk.new_lines());
210 let start = doc.lines.len();
211 let (mut old_count, mut new_count) = (0, 0);
212 for i in 0..count {
213 let line = patch.line_in_hunk(hunk_index, i)?;
214 let kind = match line.origin() {
215 ' ' => Kind::Context,
216 '+' => Kind::Addition,
217 '-' => Kind::Deletion,
218 '=' | '<' | '>' => {
219 if doc.lines.len() == start {
220 bail!("Newline marker without a source line in this hunk");
221 }
222 let previous = doc
223 .lines
224 .last_mut()
225 .context("Newline marker without a source line")?;
226 previous.no_newline = true;
227 if previous.bytes.last() == Some(&b'\n') {
228 previous.bytes.pop();
229 }
230 continue;
231 }
232 c => bail!("Unsupported patch line type: {c:?}"),
233 };
234 let old = line.old_lineno();
235 let new = line.new_lineno();
236 if let Some(n) = old {
237 if n == 0 || Some(n) != hunk.old_start().checked_add(old_count) {
238 bail!("Non-contiguous old source line");
239 }
240 old_count += 1;
241 }
242 if let Some(n) = new {
243 if n == 0 || Some(n) != hunk.new_start().checked_add(new_count) {
244 bail!("Non-contiguous new source line");
245 }
246 new_count += 1;
247 }
248 file.additions += usize::from(kind == Kind::Addition);
249 file.deletions += usize::from(kind == Kind::Deletion);
250 let largest = old.unwrap_or(0).max(new.unwrap_or(0)).max(1);
251 file.digits = file.digits.max(largest.ilog10() as usize + 1);
252 let offset = usize::try_from(line.content_offset())?;
253 let offset = match "ed {
254 Some(quoted) => quoted.original_offset(offset),
255 None => Some(offset),
256 }
257 .and_then(|offset| offset.checked_add(1))
258 .context("Source offset overflow")?;
259 let end = offset
260 .checked_add(line.content().len())
261 .context("Source length overflow")?;
262 anyhow::ensure!(
263 input.get(offset..end) == Some(line.content()),
264 "Parser source offset does not match the supplied patch"
265 );
266 doc.lines.push(SourceLine {
267 kind,
268 old,
269 new,
270 bytes: line.content().to_vec(),
271 no_newline: false,
272 offset,
273 });
274 }
275 if old_count != hunk.old_lines() || new_count != hunk.new_lines() {
276 bail!("Hunk counts do not agree with its source lines");
277 }
278 file.hunks.push(Hunk {
279 header: crate::text::safe_label(
280 hunk.header().strip_suffix(b"\n").unwrap_or(hunk.header()),
281 ),
282 old_start: hunk.old_start(),
283 new_start: hunk.new_start(),
284 lines: start..doc.lines.len(),
285 });
286 }
287 }
288 if delta.flags().is_binary() {
289 file.status = "Binary change".into();
290 }
291 doc.files.push(file);
292 }
293 Ok(doc)
294 }
295
296 fn push_both(&mut self, file: usize, kind: RowKind) {
297 self.split.push(Row { file, kind });
298 self.unified.push(Row { file, kind });
299 }
300
301 pub(crate) fn index_rows(&mut self) {
302 self.split_locations.resize(self.lines.len(), 0);
303 self.unified_locations.resize(self.lines.len(), 0);
304 for f in 0..self.files.len() {
305 self.push_both(f, RowKind::Header);
306 if self.files[f].hunks.is_empty() {
307 self.push_both(f, RowKind::Metadata);
308 }
309 let (mut old_end, mut new_end) = (1, 1);
310 for h in 0..self.files[f].hunks.len() {
311 let hunk = &self.files[f].hunks[h];
312 let range = hunk.lines.clone();
313 if hunk.old_start > old_end || hunk.new_start > new_end {
314 self.push_both(f, RowKind::Gap);
315 }
316 self.push_both(f, RowKind::Hunk(h));
317 let mut i = range.start;
318 while i < range.end {
319 if self.lines[i].kind == Kind::Context {
320 self.add_split(f, Some(i), Some(i));
321 self.add_unified(f, i);
322 i += 1;
323 } else {
324 let start = i;
325 while i < range.end && self.lines[i].kind != Kind::Context {
326 i += 1;
327 }
328 let left: Vec<_> = (start..i)
329 .filter(|n| self.lines[*n].kind == Kind::Deletion)
330 .collect();
331 let right: Vec<_> = (start..i)
332 .filter(|n| self.lines[*n].kind == Kind::Addition)
333 .collect();
334 for id in left.iter().chain(&right) {
335 self.add_unified(f, *id);
336 }
337 let offset = alignment_offset(&self.lines, &left, &right);
338 let left_pad = if right.len() > left.len() { offset } else { 0 };
339 let right_pad = if left.len() > right.len() { offset } else { 0 };
340 for row in 0..(left.len() + left_pad).max(right.len() + right_pad) {
341 let l = row.checked_sub(left_pad).and_then(|n| left.get(n)).copied();
342 let r = row
343 .checked_sub(right_pad)
344 .and_then(|n| right.get(n))
345 .copied();
346 self.add_split(f, l, r);
347 }
348 }
349 }
350 for line in &self.lines[range] {
351 if let Some(n) = line.old {
352 old_end = n.saturating_add(1);
353 }
354 if let Some(n) = line.new {
355 new_end = n.saturating_add(1);
356 }
357 }
358 }
359 }
360 }
361
362 fn add_split(&mut self, file: usize, left: Option<usize>, right: Option<usize>) {
363 for id in left.into_iter().chain(right) {
364 self.split_locations[id] = self.split.len();
365 }
366 self.split.push(Row {
367 file,
368 kind: RowKind::Code { left, right },
369 });
370 let l = left.is_some_and(|i| self.lines[i].no_newline);
371 let r = right.is_some_and(|i| self.lines[i].no_newline);
372 if l || r {
373 self.split.push(Row {
374 file,
375 kind: RowKind::NoNewline { left: l, right: r },
376 });
377 }
378 }
379
380 fn add_unified(&mut self, file: usize, id: usize) {
381 let line = &self.lines[id];
382 self.unified_locations[id] = self.unified.len();
383 let left = line.old.map(|_| id);
384 let right = line.new.map(|_| id);
385 self.unified.push(Row {
386 file,
387 kind: RowKind::Code { left, right },
388 });
389 if line.no_newline {
390 self.unified.push(Row {
391 file,
392 kind: RowKind::NoNewline {
393 left: left.is_some(),
394 right: right.is_some(),
395 },
396 });
397 }
398 }
399}
400
401fn empty_file_headers(input: &[u8]) -> Option<Vec<u8>> {
402 let mut lines = input.split_inclusive(|b| *b == b'\n');
403 let (header, mode, index) = (lines.next()?, lines.next()?, lines.next()?);
404 if lines.next().is_some() {
405 return None;
406 }
407 let added = mode.starts_with(b"new file mode ");
408 let deleted = mode.starts_with(b"deleted file mode ");
409 if !(added || deleted) || !index.starts_with(b"index ") {
410 return None;
411 }
412 let header = header.strip_prefix(b"diff --git ")?.strip_suffix(b"\n")?;
413 let (old, new) = split_identical_paths(header)?;
414 let ids = index.strip_prefix(b"index ")?.strip_suffix(b"\n")?;
415 let (old_id, new_id) = std::str::from_utf8(ids).ok()?.split_once("..")?;
416 let empty = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391";
417 let matches_empty = |id: &str| id.len() >= 7 && empty.starts_with(id);
418 let zero = |id: &str| id.len() >= 7 && id.bytes().all(|b| b == b'0');
419 if !((added && zero(old_id) && matches_empty(new_id))
420 || (deleted && matches_empty(old_id) && zero(new_id)))
421 {
422 return None;
423 }
424 let mut result = input.to_vec();
425 result.extend_from_slice(b"--- ");
426 result.extend_from_slice(if added { b"/dev/null" } else { old });
427 result.extend_from_slice(b"\n+++ ");
428 result.extend_from_slice(if deleted { b"/dev/null" } else { new });
429 result.extend_from_slice(b"\n");
430 Some(result)
431}
432
433fn split_identical_paths(header: &[u8]) -> Option<(&[u8], &[u8])> {
434 let middle = header.len() / 2;
435 if header.get(middle) != Some(&b' ') {
436 return None;
437 }
438 let (old, new) = (&header[..middle], &header[middle + 1..]);
439 let unquoted = old.strip_prefix(b"a/").zip(new.strip_prefix(b"b/"));
440 let quoted = old.strip_prefix(b"\"a/").zip(new.strip_prefix(b"\"b/"));
441 if unquoted.or(quoted).is_some_and(|(a, b)| a == b) {
442 Some((old, new))
443 } else {
444 None
445 }
446}
447
448pub(crate) fn alignment_offset(lines: &[SourceLine], left: &[usize], right: &[usize]) -> usize {
450 let pairs = left.len().min(right.len());
451 let extra = left.len().abs_diff(right.len());
452 if !alignment_needed(left.len(), right.len()) {
453 return 0;
454 }
455 if left
456 .iter()
457 .chain(right)
458 .any(|i| lines[*i].bytes.len() > 4096)
459 {
460 return 0;
461 }
462 let mut a = Vec::with_capacity(
463 left.iter()
464 .map(|&i| lines[i].bytes.len())
465 .max()
466 .unwrap_or(0),
467 );
468 let mut b = Vec::with_capacity(
469 right
470 .iter()
471 .map(|&i| lines[i].bytes.len())
472 .max()
473 .unwrap_or(0),
474 );
475 let mut score = |offset: usize| -> f64 {
476 (0..pairs)
477 .map(|p| {
478 let left_bytes =
479 &lines[left[p + if left.len() > right.len() { offset } else { 0 }]].bytes;
480 let right_bytes =
481 &lines[right[p + if right.len() > left.len() { offset } else { 0 }]].bytes;
482 a.clear();
483 b.clear();
484 a.extend(
485 left_bytes
486 .iter()
487 .copied()
488 .filter(|c| !c.is_ascii_whitespace()),
489 );
490 b.extend(
491 right_bytes
492 .iter()
493 .copied()
494 .filter(|c| !c.is_ascii_whitespace()),
495 );
496 let prefix = a.iter().zip(&b).take_while(|(x, y)| x == y).count();
497 let suffix = a[prefix..]
498 .iter()
499 .rev()
500 .zip(b[prefix..].iter().rev())
501 .take_while(|(x, y)| x == y)
502 .count();
503 (prefix + suffix) as f64 / a.len().max(b.len()).max(1) as f64
504 })
505 .sum()
506 };
507 let mut best = score(0) + pairs as f64 * 0.5;
508 let mut offset = 0;
509 for n in 1..=extra {
510 let s = score(n);
511 if s > best {
512 best = s;
513 offset = n;
514 }
515 }
516 offset
517}
518
519pub(crate) fn alignment_needed(old: usize, new: usize) -> bool {
520 let pairs = old.min(new);
521 let extra = old.abs_diff(new);
522 pairs != 0 && extra != 0 && pairs.saturating_mul(extra + 1) <= 4096
523}