1#[derive(Debug, Clone, Default)]
7pub struct ParsedDiff {
8 pub file_a: Option<String>,
9 pub file_b: Option<String>,
10 pub hunks: Vec<DiffHunk>,
11}
12
13#[must_use]
17pub fn hunk_exclusion_ranges(hunks: &[DiffHunk]) -> Vec<(i64, i64)> {
18 let mut ranges: Vec<(i64, i64)> = Vec::new();
19 for h in hunks {
20 if h.new_count > 0 {
21 ranges.push((
22 i64::from(h.new_start),
23 i64::from(h.new_start + h.new_count.saturating_sub(1)),
24 ));
25 }
26 }
27 ranges.sort_by_key(|r| r.0);
28 let mut merged: Vec<(i64, i64)> = Vec::new();
30 for (s, e) in ranges {
31 if let Some(last) = merged.last_mut() {
32 if s <= last.1 + 1 {
33 last.1 = last.1.max(e);
34 } else {
35 merged.push((s, e));
36 }
37 } else {
38 merged.push((s, e));
39 }
40 }
41 merged
42}
43
44#[derive(Debug, Clone)]
46pub struct DiffHunk {
47 pub header: String,
49 pub old_start: u32,
51 pub old_count: u32,
53 pub new_start: u32,
55 pub new_count: u32,
57 pub lines: Vec<DiffLine>,
59}
60
61#[derive(Debug, Clone)]
63pub struct DiffLine {
64 pub kind: DiffLineKind,
65 pub old_line: Option<u32>,
67 pub new_line: Option<u32>,
69 pub content: String,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum DiffLineKind {
76 Context,
77 Added,
78 Removed,
79}
80
81impl ParsedDiff {
82 #[must_use]
84 pub fn parse(diff: &str) -> Self {
85 let mut result = Self::default();
86 let mut lines = diff.lines().peekable();
87
88 while let Some(line) = lines.peek() {
90 if line.starts_with("---") {
91 result.file_a = line.strip_prefix("--- ").map(|s| {
92 s.strip_prefix("a/").unwrap_or(s).to_string()
94 });
95 lines.next();
96 } else if line.starts_with("+++") {
97 result.file_b = line.strip_prefix("+++ ").map(|s| {
98 s.strip_prefix("b/").unwrap_or(s).to_string()
100 });
101 lines.next();
102 } else if line.starts_with("@@") {
103 break;
104 } else {
105 lines.next(); }
107 }
108
109 while let Some(line) = lines.next() {
111 if line.starts_with("@@") {
112 if let Some(hunk) = Self::parse_hunk(line, &mut lines) {
113 result.hunks.push(hunk);
114 }
115 }
116 }
117
118 result
119 }
120
121 fn parse_hunk(
122 header: &str,
123 lines: &mut std::iter::Peekable<std::str::Lines<'_>>,
124 ) -> Option<DiffHunk> {
125 let header_str = header.to_string();
128
129 let parts: Vec<&str> = header.split_whitespace().collect();
130 if parts.len() < 3 {
131 return None;
132 }
133
134 let (old_start, old_count) = Self::parse_range(parts[1].trim_start_matches('-'))?;
135 let (new_start, new_count) = Self::parse_range(parts[2].trim_start_matches('+'))?;
136
137 let mut hunk = DiffHunk {
138 header: header_str,
139 old_start,
140 old_count,
141 new_start,
142 new_count,
143 lines: Vec::new(),
144 };
145
146 let mut old_line = old_start;
147 let mut new_line = new_start;
148
149 while let Some(line) = lines.peek() {
150 if line.starts_with("@@") || line.starts_with("diff ") {
151 break;
152 }
153
154 let line = lines.next().unwrap_or_default();
155
156 let (kind, content) = if let Some(content) = line.strip_prefix('+') {
157 (DiffLineKind::Added, content)
158 } else if let Some(content) = line.strip_prefix('-') {
159 (DiffLineKind::Removed, content)
160 } else if let Some(content) = line.strip_prefix(' ') {
161 (DiffLineKind::Context, content)
162 } else if line.is_empty() {
163 (DiffLineKind::Context, "")
165 } else if line.starts_with('\\') {
166 continue;
168 } else {
169 (DiffLineKind::Context, line)
171 };
172
173 let diff_line = match kind {
174 DiffLineKind::Added => {
175 let dl = DiffLine {
176 kind,
177 old_line: None,
178 new_line: Some(new_line),
179 content: content.to_string(),
180 };
181 new_line += 1;
182 dl
183 }
184 DiffLineKind::Removed => {
185 let dl = DiffLine {
186 kind,
187 old_line: Some(old_line),
188 new_line: None,
189 content: content.to_string(),
190 };
191 old_line += 1;
192 dl
193 }
194 DiffLineKind::Context => {
195 let dl = DiffLine {
196 kind,
197 old_line: Some(old_line),
198 new_line: Some(new_line),
199 content: content.to_string(),
200 };
201 old_line += 1;
202 new_line += 1;
203 dl
204 }
205 };
206
207 hunk.lines.push(diff_line);
208 }
209
210 Some(hunk)
211 }
212
213 fn parse_range(s: &str) -> Option<(u32, u32)> {
214 if let Some((start, count)) = s.split_once(',') {
215 Some((start.parse().ok()?, count.parse().ok()?))
216 } else {
217 let start = s.parse().ok()?;
219 Some((start, 1))
220 }
221 }
222
223 #[must_use]
225 pub fn total_lines(&self) -> usize {
226 self.hunks.iter().map(|h| h.lines.len()).sum()
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_parse_simple_diff() {
236 let diff = r#"diff --git a/src/main.rs b/src/main.rs
237index abc123..def456 100644
238--- a/src/main.rs
239+++ b/src/main.rs
240@@ -1,5 +1,7 @@
241 fn main() {
242- println!("Hello");
243+ println!("Hello, world!");
244+ println!("Goodbye!");
245 }
246"#;
247
248 let parsed = ParsedDiff::parse(diff);
249
250 assert_eq!(parsed.file_a, Some("src/main.rs".to_string()));
251 assert_eq!(parsed.file_b, Some("src/main.rs".to_string()));
252 assert_eq!(parsed.hunks.len(), 1);
253
254 let hunk = &parsed.hunks[0];
255 assert_eq!(hunk.old_start, 1);
256 assert_eq!(hunk.old_count, 5);
257 assert_eq!(hunk.new_start, 1);
258 assert_eq!(hunk.new_count, 7);
259
260 assert_eq!(hunk.lines.len(), 5);
262 assert_eq!(hunk.lines[0].kind, DiffLineKind::Context);
263 assert_eq!(hunk.lines[1].kind, DiffLineKind::Removed);
264 assert_eq!(hunk.lines[2].kind, DiffLineKind::Added);
265 assert_eq!(hunk.lines[3].kind, DiffLineKind::Added);
266 assert_eq!(hunk.lines[4].kind, DiffLineKind::Context);
267 }
268
269 #[test]
270 fn test_line_numbers() {
271 let diff = r#"--- a/test.txt
272+++ b/test.txt
273@@ -10,3 +10,4 @@
274 context
275-removed
276+added1
277+added2
278"#;
279
280 let parsed = ParsedDiff::parse(diff);
281 let lines = &parsed.hunks[0].lines;
282
283 assert_eq!(lines[0].old_line, Some(10));
285 assert_eq!(lines[0].new_line, Some(10));
286
287 assert_eq!(lines[1].old_line, Some(11));
289 assert_eq!(lines[1].new_line, None);
290
291 assert_eq!(lines[2].old_line, None);
293 assert_eq!(lines[2].new_line, Some(11));
294
295 assert_eq!(lines[3].old_line, None);
297 assert_eq!(lines[3].new_line, Some(12));
298 }
299}