#[derive(Debug, Clone, Default)]
pub struct ParsedDiff {
pub file_a: Option<String>,
pub file_b: Option<String>,
pub hunks: Vec<DiffHunk>,
}
#[must_use]
pub fn hunk_exclusion_ranges(hunks: &[DiffHunk]) -> Vec<(i64, i64)> {
let mut ranges: Vec<(i64, i64)> = Vec::new();
for h in hunks {
if h.new_count > 0 {
ranges.push((
i64::from(h.new_start),
i64::from(h.new_start + h.new_count.saturating_sub(1)),
));
}
}
ranges.sort_by_key(|r| r.0);
let mut merged: Vec<(i64, i64)> = Vec::new();
for (s, e) in ranges {
if let Some(last) = merged.last_mut() {
if s <= last.1 + 1 {
last.1 = last.1.max(e);
} else {
merged.push((s, e));
}
} else {
merged.push((s, e));
}
}
merged
}
#[derive(Debug, Clone)]
pub struct DiffHunk {
pub header: String,
pub old_start: u32,
pub old_count: u32,
pub new_start: u32,
pub new_count: u32,
pub lines: Vec<DiffLine>,
}
#[derive(Debug, Clone)]
pub struct DiffLine {
pub kind: DiffLineKind,
pub old_line: Option<u32>,
pub new_line: Option<u32>,
pub content: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffLineKind {
Context,
Added,
Removed,
}
impl ParsedDiff {
#[must_use]
pub fn parse(diff: &str) -> Self {
let mut result = Self::default();
let mut lines = diff.lines().peekable();
while let Some(line) = lines.peek() {
if line.starts_with("---") {
result.file_a = line.strip_prefix("--- ").map(|s| {
s.strip_prefix("a/").unwrap_or(s).to_string()
});
lines.next();
} else if line.starts_with("+++") {
result.file_b = line.strip_prefix("+++ ").map(|s| {
s.strip_prefix("b/").unwrap_or(s).to_string()
});
lines.next();
} else if line.starts_with("@@") {
break;
} else {
lines.next(); }
}
while let Some(line) = lines.next() {
if line.starts_with("@@") {
if let Some(hunk) = Self::parse_hunk(line, &mut lines) {
result.hunks.push(hunk);
}
}
}
result
}
fn parse_hunk(
header: &str,
lines: &mut std::iter::Peekable<std::str::Lines<'_>>,
) -> Option<DiffHunk> {
let header_str = header.to_string();
let parts: Vec<&str> = header.split_whitespace().collect();
if parts.len() < 3 {
return None;
}
let (old_start, old_count) = Self::parse_range(parts[1].trim_start_matches('-'))?;
let (new_start, new_count) = Self::parse_range(parts[2].trim_start_matches('+'))?;
let mut hunk = DiffHunk {
header: header_str,
old_start,
old_count,
new_start,
new_count,
lines: Vec::new(),
};
let mut old_line = old_start;
let mut new_line = new_start;
while let Some(line) = lines.peek() {
if line.starts_with("@@") || line.starts_with("diff ") {
break;
}
let line = lines.next().unwrap_or_default();
let (kind, content) = if let Some(content) = line.strip_prefix('+') {
(DiffLineKind::Added, content)
} else if let Some(content) = line.strip_prefix('-') {
(DiffLineKind::Removed, content)
} else if let Some(content) = line.strip_prefix(' ') {
(DiffLineKind::Context, content)
} else if line.is_empty() {
(DiffLineKind::Context, "")
} else if line.starts_with('\\') {
continue;
} else {
(DiffLineKind::Context, line)
};
let diff_line = match kind {
DiffLineKind::Added => {
let dl = DiffLine {
kind,
old_line: None,
new_line: Some(new_line),
content: content.to_string(),
};
new_line += 1;
dl
}
DiffLineKind::Removed => {
let dl = DiffLine {
kind,
old_line: Some(old_line),
new_line: None,
content: content.to_string(),
};
old_line += 1;
dl
}
DiffLineKind::Context => {
let dl = DiffLine {
kind,
old_line: Some(old_line),
new_line: Some(new_line),
content: content.to_string(),
};
old_line += 1;
new_line += 1;
dl
}
};
hunk.lines.push(diff_line);
}
Some(hunk)
}
fn parse_range(s: &str) -> Option<(u32, u32)> {
if let Some((start, count)) = s.split_once(',') {
Some((start.parse().ok()?, count.parse().ok()?))
} else {
let start = s.parse().ok()?;
Some((start, 1))
}
}
#[must_use]
pub fn total_lines(&self) -> usize {
self.hunks.iter().map(|h| h.lines.len()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_diff() {
let diff = r#"diff --git a/src/main.rs b/src/main.rs
index abc123..def456 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,7 @@
fn main() {
- println!("Hello");
+ println!("Hello, world!");
+ println!("Goodbye!");
}
"#;
let parsed = ParsedDiff::parse(diff);
assert_eq!(parsed.file_a, Some("src/main.rs".to_string()));
assert_eq!(parsed.file_b, Some("src/main.rs".to_string()));
assert_eq!(parsed.hunks.len(), 1);
let hunk = &parsed.hunks[0];
assert_eq!(hunk.old_start, 1);
assert_eq!(hunk.old_count, 5);
assert_eq!(hunk.new_start, 1);
assert_eq!(hunk.new_count, 7);
assert_eq!(hunk.lines.len(), 5);
assert_eq!(hunk.lines[0].kind, DiffLineKind::Context);
assert_eq!(hunk.lines[1].kind, DiffLineKind::Removed);
assert_eq!(hunk.lines[2].kind, DiffLineKind::Added);
assert_eq!(hunk.lines[3].kind, DiffLineKind::Added);
assert_eq!(hunk.lines[4].kind, DiffLineKind::Context);
}
#[test]
fn test_line_numbers() {
let diff = r#"--- a/test.txt
+++ b/test.txt
@@ -10,3 +10,4 @@
context
-removed
+added1
+added2
"#;
let parsed = ParsedDiff::parse(diff);
let lines = &parsed.hunks[0].lines;
assert_eq!(lines[0].old_line, Some(10));
assert_eq!(lines[0].new_line, Some(10));
assert_eq!(lines[1].old_line, Some(11));
assert_eq!(lines[1].new_line, None);
assert_eq!(lines[2].old_line, None);
assert_eq!(lines[2].new_line, Some(11));
assert_eq!(lines[3].old_line, None);
assert_eq!(lines[3].new_line, Some(12));
}
}