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
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LineKind {
Context,
Add,
Del,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Line {
pub kind: LineKind,
/// Content without the leading +/-/space marker and without the trailing '\n'.
/// A trailing '\r' (CRLF input) is preserved here. Stored as raw bytes so any input
/// encoding (or invalid UTF-8) round-trips unchanged.
pub text: Vec<u8>,
/// True when this line is followed by "\ No newline at end of file".
pub no_newline: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hunk {
pub old_start: u32,
pub old_lines: u32,
pub new_start: u32,
pub new_lines: u32,
/// Text after the second `@@` on the hunk header (without leading space). May be empty.
pub section: Vec<u8>,
pub lines: Vec<Line>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FileContent {
Text(Vec<Hunk>),
/// Binary patch body lines, stored verbatim (without trailing '\n').
Binary(Vec<Vec<u8>>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileDiff {
/// Raw header lines before the first hunk, verbatim, without trailing '\n'.
pub headers: Vec<Vec<u8>>,
pub old_path: Option<Vec<u8>>,
pub new_path: Option<Vec<u8>>,
pub content: FileContent,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Patch {
pub files: Vec<FileDiff>,
}
impl FileDiff {
/// Best-effort display path: new path, else old path, decoded lossily. Empty if neither.
/// For display and error messages only; the emitted diff keeps the original path bytes.
pub fn display_path(&self) -> String {
self.new_path
.as_deref()
.or(self.old_path.as_deref())
.map(|b| String::from_utf8_lossy(b).into_owned())
.unwrap_or_default()
}
}
/// (context, added, deleted) line counts over a slice of lines. Shared by the few places
/// that need per-kind tallies (`change_counts`, header recomputation in `split`, the
/// internal consistency check in `validate`) so the count logic lives in one spot.
pub(crate) fn count_kinds(lines: &[Line]) -> (u32, u32, u32) {
let mut ctx = 0;
let mut add = 0;
let mut del = 0;
for l in lines {
match l.kind {
LineKind::Context => ctx += 1,
LineKind::Add => add += 1,
LineKind::Del => del += 1,
}
}
(ctx, add, del)
}
impl Hunk {
/// (added, deleted) line counts.
pub fn change_counts(&self) -> (u32, u32) {
let (_, add, del) = count_kinds(&self.lines);
(add, del)
}
/// The sub-hunk's changed (`+`/`-`) lines in body order, each paired with its 1-based index
/// over `1..=changed` (additions and deletions share one numbering). Context lines are
/// excluded. This is the single source of truth for "changed lines of a sub-hunk": the
/// content id ([`crate::subhunk_id`]), `list --json`'s `changed_lines`, and the
/// `INDEX@L<set>` slice numbering are all built from it, so their numbering agrees by
/// construction.
pub fn changed_lines(&self) -> impl Iterator<Item = (usize, &Line)> {
self.lines
.iter()
.filter(|l| !matches!(l.kind, LineKind::Context))
.enumerate()
.map(|(idx, l)| (idx + 1, l))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn change_counts_counts_add_and_del() {
let h = Hunk {
old_start: 1,
old_lines: 2,
new_start: 1,
new_lines: 2,
section: Vec::new(),
lines: vec![
Line {
kind: LineKind::Context,
text: b"a".to_vec(),
no_newline: false,
},
Line {
kind: LineKind::Del,
text: b"b".to_vec(),
no_newline: false,
},
Line {
kind: LineKind::Add,
text: b"c".to_vec(),
no_newline: false,
},
],
};
assert_eq!(h.change_counts(), (1, 1));
}
}