Skip to main content

orch/engine/runner/
diff.rs

1use std::collections::BTreeMap;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct DiffHunk {
5    pub old_start: u32,
6    pub old_count: u32,
7    pub new_start: u32,
8    pub new_count: u32,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FileDiffSummary {
13    pub path: String,
14    pub hunks: Vec<DiffHunk>,
15}
16
17pub fn parse_unified_diff_hunks(diff: &str) -> Vec<FileDiffSummary> {
18    let mut by_file: BTreeMap<String, Vec<DiffHunk>> = BTreeMap::new();
19    let mut current_file: Option<String> = None;
20
21    for line in diff.lines() {
22        if let Some(path) = line.strip_prefix("+++ b/") {
23            current_file = Some(path.to_string());
24            by_file.entry(path.to_string()).or_default();
25            continue;
26        }
27
28        if let Some(rest) = line.strip_prefix("@@ -") {
29            let Some(file) = current_file.as_ref() else {
30                continue;
31            };
32            if let Some(hunk) = parse_hunk_header(rest) {
33                by_file.entry(file.clone()).or_default().push(hunk);
34            }
35        }
36    }
37
38    by_file
39        .into_iter()
40        .map(|(path, hunks)| FileDiffSummary { path, hunks })
41        .collect()
42}
43
44fn parse_hunk_header(rest: &str) -> Option<DiffHunk> {
45    let (old_part, remainder) = rest.split_once(" +")?;
46    let (new_part, _) = remainder.split_once(" @@")?;
47
48    let (old_start, old_count) = parse_range(old_part)?;
49    let (new_start, new_count) = parse_range(new_part)?;
50
51    Some(DiffHunk {
52        old_start,
53        old_count,
54        new_start,
55        new_count,
56    })
57}
58
59fn parse_range(part: &str) -> Option<(u32, u32)> {
60    if let Some((start, count)) = part.split_once(',') {
61        Some((start.parse().ok()?, count.parse().ok()?))
62    } else {
63        Some((part.parse().ok()?, 1))
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::parse_unified_diff_hunks;
70
71    #[test]
72    fn parses_multi_file_hunks() {
73        let diff = r#"diff --git a/src/a.rs b/src/a.rs
74index 1111111..2222222 100644
75--- a/src/a.rs
76+++ b/src/a.rs
77@@ -1,1 +1,2 @@
78+line
79@@ -10,2 +11,3 @@
80-line
81+line
82+line2
83diff --git a/src/b.rs b/src/b.rs
84index 3333333..4444444 100644
85--- a/src/b.rs
86+++ b/src/b.rs
87@@ -5 +5 @@
88-old
89+new
90"#;
91
92        let files = parse_unified_diff_hunks(diff);
93        assert_eq!(files.len(), 2);
94        assert_eq!(files[0].path, "src/a.rs");
95        assert_eq!(files[0].hunks.len(), 2);
96        assert_eq!(files[1].path, "src/b.rs");
97        assert_eq!(files[1].hunks.len(), 1);
98    }
99}