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 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(); 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 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]; 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(¬e));
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 let note_diff_file =
117 create_test_note("commit_a", Some("src/lib.rs"), Some(12), Some(15));
118 assert!(!hunk.matches_note(¬e_diff_file));
119
120 let note_same_file =
122 create_test_note("commit_a", Some("src/main.rs"), Some(12), Some(15));
123 assert!(hunk.matches_note(¬e_same_file));
124
125 let note_no_file = create_test_note("commit_a", None, Some(12), Some(15));
127 assert!(hunk.matches_note(¬e_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 let note = create_test_note("commit_a", Some("src/main.rs"), Some(5), Some(9));
141 assert!(!hunk.matches_note(¬e));
142
143 let note = create_test_note("commit_a", Some("src/main.rs"), Some(5), Some(10));
145 assert!(hunk.matches_note(¬e));
146
147 let note = create_test_note("commit_a", Some("src/main.rs"), Some(5), Some(15));
149 assert!(hunk.matches_note(¬e));
150
151 let note = create_test_note("commit_a", Some("src/main.rs"), Some(12), Some(18));
153 assert!(hunk.matches_note(¬e));
154
155 let note = create_test_note("commit_a", Some("src/main.rs"), Some(5), Some(25));
157 assert!(hunk.matches_note(¬e));
158
159 let note = create_test_note("commit_a", Some("src/main.rs"), Some(15), Some(25));
161 assert!(hunk.matches_note(¬e));
162
163 let note = create_test_note("commit_a", Some("src/main.rs"), Some(20), Some(25));
165 assert!(hunk.matches_note(¬e));
166
167 let note = create_test_note("commit_a", Some("src/main.rs"), Some(21), Some(25));
169 assert!(!hunk.matches_note(¬e));
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 let note1 = create_test_note("commit_a", Some("src/main.rs"), Some(15), None);
183 assert!(hunk.matches_note(¬e1));
184
185 let note2 = create_test_note("commit_a", Some("src/main.rs"), None, Some(15));
187 assert!(hunk.matches_note(¬e2));
188
189 let note3 = create_test_note("commit_a", Some("src/main.rs"), None, None);
191 assert!(hunk.matches_note(¬e3));
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}