Skip to main content

gn_core/
diff.rs

1use crate::note::Note;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct HunkAnchor {
5    pub file: String,
6    pub line_start: u32,
7    pub line_end: u32,
8    pub commit: String,
9}
10
11impl HunkAnchor {
12    pub fn matches_note(&self, note: &Note) -> bool {
13        if note.commit != self.commit {
14            return false;
15        }
16        if let Some(ref f) = note.file {
17            if f != &self.file {
18                return false;
19            }
20        }
21        if let (Some(ns), Some(ne)) = (note.line_start, note.line_end) {
22            // Checks if the hunk overlaps with the note
23            if ne < self.line_start || ns > self.line_end {
24                return false;
25            }
26        }
27        true
28    }
29}
30
31pub fn parse_diff_hunks(patch: &str) -> Vec<HunkAnchor> {
32    let mut anchors = Vec::new();
33    let mut current_file = String::new();
34    let current_commit = "pending".to_string(); // In a real setup, this would be parsed or passed in
35
36    for line in patch.lines() {
37        if let Some(f) = line.strip_prefix("+++ b/") {
38            current_file = f.to_string();
39        } else if line.starts_with("@@ ") {
40            // Parse @@ -a,b +c,d @@
41            if let Some(end_idx) = line[3..].find(" @@") {
42                let hunk_info = &line[3..3 + end_idx];
43                let parts: Vec<&str> = hunk_info.split_whitespace().collect();
44                if parts.len() >= 2 {
45                    let added = parts[1]; // e.g. +c,d
46                    let added_nums = &added[1..];
47                    let line_parts: Vec<&str> = added_nums.split(',').collect();
48                    if !line_parts.is_empty() {
49                        if let Ok(start) = line_parts[0].parse::<u32>() {
50                            let count = if line_parts.len() > 1 {
51                                line_parts[1].parse::<u32>().unwrap_or(1)
52                            } else {
53                                1
54                            };
55                            anchors.push(HunkAnchor {
56                                file: current_file.clone(),
57                                line_start: start,
58                                line_end: start + count.saturating_sub(1),
59                                commit: current_commit.clone(),
60                            });
61                        }
62                    }
63                }
64            }
65        }
66    }
67
68    anchors
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::namespace::Namespace;
75
76    fn create_test_note(
77        commit: &str,
78        file: Option<&str>,
79        line_start: Option<u32>,
80        line_end: Option<u32>,
81    ) -> Note {
82        Note::new(
83            commit.to_string(),
84            file.map(|s| s.to_string()),
85            line_start,
86            line_end,
87            "test body".to_string(),
88            "Author <author@test.com>".to_string(),
89            Namespace::Comments,
90        )
91    }
92
93    #[test]
94    fn test_matches_note_commit_mismatch() {
95        let hunk = HunkAnchor {
96            file: "src/main.rs".to_string(),
97            line_start: 10,
98            line_end: 20,
99            commit: "commit_a".to_string(),
100        };
101
102        let note = create_test_note("commit_b", Some("src/main.rs"), Some(12), Some(15));
103        assert!(!hunk.matches_note(&note));
104    }
105
106    #[test]
107    fn test_matches_note_file_matching() {
108        let hunk = HunkAnchor {
109            file: "src/main.rs".to_string(),
110            line_start: 10,
111            line_end: 20,
112            commit: "commit_a".to_string(),
113        };
114
115        // Different file -> no match
116        let note_diff_file =
117            create_test_note("commit_a", Some("src/lib.rs"), Some(12), Some(15));
118        assert!(!hunk.matches_note(&note_diff_file));
119
120        // Same file -> match
121        let note_same_file =
122            create_test_note("commit_a", Some("src/main.rs"), Some(12), Some(15));
123        assert!(hunk.matches_note(&note_same_file));
124
125        // Note file is None -> match (file-agnostic or repository level note)
126        let note_no_file = create_test_note("commit_a", None, Some(12), Some(15));
127        assert!(hunk.matches_note(&note_no_file));
128    }
129
130    #[test]
131    fn test_matches_note_line_overlap_boundary_cases() {
132        let hunk = HunkAnchor {
133            file: "src/main.rs".to_string(),
134            line_start: 10,
135            line_end: 20,
136            commit: "commit_a".to_string(),
137        };
138
139        // Case 1: Note entirely before hunk (5..9) -> false
140        let note = create_test_note("commit_a", Some("src/main.rs"), Some(5), Some(9));
141        assert!(!hunk.matches_note(&note));
142
143        // Case 2: Note touches hunk start boundary (5..10) -> true
144        let note = create_test_note("commit_a", Some("src/main.rs"), Some(5), Some(10));
145        assert!(hunk.matches_note(&note));
146
147        // Case 3: Note overlaps start boundary (5..15) -> true
148        let note = create_test_note("commit_a", Some("src/main.rs"), Some(5), Some(15));
149        assert!(hunk.matches_note(&note));
150
151        // Case 4: Note strictly inside hunk (12..18) -> true
152        let note = create_test_note("commit_a", Some("src/main.rs"), Some(12), Some(18));
153        assert!(hunk.matches_note(&note));
154
155        // Case 5: Note spans hunk entirely (5..25) -> true
156        let note = create_test_note("commit_a", Some("src/main.rs"), Some(5), Some(25));
157        assert!(hunk.matches_note(&note));
158
159        // Case 6: Note overlaps end boundary (15..25) -> true
160        let note = create_test_note("commit_a", Some("src/main.rs"), Some(15), Some(25));
161        assert!(hunk.matches_note(&note));
162
163        // Case 7: Note touches hunk end boundary (20..25) -> true
164        let note = create_test_note("commit_a", Some("src/main.rs"), Some(20), Some(25));
165        assert!(hunk.matches_note(&note));
166
167        // Case 8: Note entirely after hunk (21..25) -> false
168        let note = create_test_note("commit_a", Some("src/main.rs"), Some(21), Some(25));
169        assert!(!hunk.matches_note(&note));
170    }
171
172    #[test]
173    fn test_matches_note_partial_line_info() {
174        let hunk = HunkAnchor {
175            file: "src/main.rs".to_string(),
176            line_start: 10,
177            line_end: 20,
178            commit: "commit_a".to_string(),
179        };
180
181        // Line start specified, line end missing -> match
182        let note1 = create_test_note("commit_a", Some("src/main.rs"), Some(15), None);
183        assert!(hunk.matches_note(&note1));
184
185        // Line start missing, line end specified -> match
186        let note2 = create_test_note("commit_a", Some("src/main.rs"), None, Some(15));
187        assert!(hunk.matches_note(&note2));
188
189        // Both missing -> match
190        let note3 = create_test_note("commit_a", Some("src/main.rs"), None, None);
191        assert!(hunk.matches_note(&note3));
192    }
193
194    #[test]
195    fn test_parse_diff_hunks() {
196        let diff = r#"diff --git a/src/main.rs b/src/main.rs
197index 1234567..89abcdef 100644
198--- a/src/main.rs
199+++ b/src/main.rs
200@@ -1,3 +1,5 @@
201 context
202 context
203+line1
204+line2
205 context
206@@ -10 +20,3 @@
207-old line
208+new line1
209+new line2
210+new line3
211"#;
212
213        let anchors = parse_diff_hunks(diff);
214        assert_eq!(anchors.len(), 2);
215        assert_eq!(
216            anchors[0],
217            HunkAnchor {
218                file: "src/main.rs".to_string(),
219                line_start: 1,
220                line_end: 5,
221                commit: "pending".to_string(),
222            }
223        );
224        assert_eq!(
225            anchors[1],
226            HunkAnchor {
227                file: "src/main.rs".to_string(),
228                line_start: 20,
229                line_end: 22,
230                commit: "pending".to_string(),
231            }
232        );
233    }
234}