1use crate::error::GitError;
2use crate::git::GitOps;
3use crate::schema::annotation::Annotation;
4
5use super::{MatchedRegion, ReadQuery};
6
7pub fn retrieve_regions(
15 git: &dyn GitOps,
16 query: &ReadQuery,
17) -> Result<Vec<MatchedRegion>, GitError> {
18 let shas = git.log_for_file(&query.file)?;
19 let mut matched = Vec::new();
20
21 for sha in &shas {
22 let note = match git.note_read(sha)? {
23 Some(n) => n,
24 None => continue,
25 };
26
27 let annotation: Annotation = match serde_json::from_str(¬e) {
28 Ok(a) => a,
29 Err(_) => continue, };
31
32 for region in &annotation.regions {
33 if !file_matches(®ion.file, &query.file) {
34 continue;
35 }
36 if let Some(ref anchor_name) = query.anchor {
37 if region.ast_anchor.name != *anchor_name {
38 continue;
39 }
40 }
41 if let Some(ref line_range) = query.lines {
42 if !ranges_overlap(
43 region.lines.start,
44 region.lines.end,
45 line_range.start,
46 line_range.end,
47 ) {
48 continue;
49 }
50 }
51 matched.push(MatchedRegion {
52 commit: sha.clone(),
53 timestamp: annotation.timestamp.clone(),
54 region: region.clone(),
55 summary: annotation.summary.clone(),
56 });
57 }
58 }
59
60 Ok(matched)
61}
62
63fn file_matches(region_file: &str, query_file: &str) -> bool {
66 fn norm(s: &str) -> &str {
67 s.strip_prefix("./").unwrap_or(s)
68 }
69 norm(region_file) == norm(query_file)
70}
71
72fn ranges_overlap(a_start: u32, a_end: u32, b_start: u32, b_end: u32) -> bool {
74 a_start <= b_end && b_start <= a_end
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn test_file_matches_exact() {
83 assert!(file_matches("src/main.rs", "src/main.rs"));
84 }
85
86 #[test]
87 fn test_file_matches_dot_slash() {
88 assert!(file_matches("./src/main.rs", "src/main.rs"));
89 assert!(file_matches("src/main.rs", "./src/main.rs"));
90 }
91
92 #[test]
93 fn test_file_no_match() {
94 assert!(!file_matches("src/lib.rs", "src/main.rs"));
95 }
96
97 #[test]
98 fn test_ranges_overlap() {
99 assert!(ranges_overlap(1, 10, 5, 15));
100 assert!(ranges_overlap(5, 15, 1, 10));
101 assert!(ranges_overlap(1, 10, 10, 20));
102 assert!(ranges_overlap(1, 10, 1, 10));
103 }
104
105 #[test]
106 fn test_ranges_no_overlap() {
107 assert!(!ranges_overlap(1, 5, 6, 10));
108 assert!(!ranges_overlap(6, 10, 1, 5));
109 }
110
111 #[test]
112 fn test_retrieve_filters_by_file() {
113 use crate::schema::annotation::*;
114
115 let git = MockGitOps {
116 shas: vec!["abc123".to_string()],
117 note: Some(
118 serde_json::to_string(&Annotation {
119 schema: "chronicle/v1".to_string(),
120 commit: "abc123".to_string(),
121 timestamp: "2025-01-01T00:00:00Z".to_string(),
122 task: None,
123 summary: "test commit".to_string(),
124 context_level: ContextLevel::Enhanced,
125 regions: vec![
126 RegionAnnotation {
127 file: "src/main.rs".to_string(),
128 ast_anchor: AstAnchor {
129 unit_type: "fn".to_string(),
130 name: "main".to_string(),
131 signature: None,
132 },
133 lines: LineRange { start: 1, end: 10 },
134 intent: "entry point".to_string(),
135 reasoning: None,
136 constraints: vec![],
137 semantic_dependencies: vec![],
138 related_annotations: vec![],
139 tags: vec![],
140 risk_notes: None,
141 corrections: vec![],
142 },
143 RegionAnnotation {
144 file: "src/lib.rs".to_string(),
145 ast_anchor: AstAnchor {
146 unit_type: "mod".to_string(),
147 name: "lib".to_string(),
148 signature: None,
149 },
150 lines: LineRange { start: 1, end: 5 },
151 intent: "module decl".to_string(),
152 reasoning: None,
153 constraints: vec![],
154 semantic_dependencies: vec![],
155 related_annotations: vec![],
156 tags: vec![],
157 risk_notes: None,
158 corrections: vec![],
159 },
160 ],
161 cross_cutting: vec![],
162 provenance: Provenance {
163 operation: ProvenanceOperation::Initial,
164 derived_from: vec![],
165 original_annotations_preserved: false,
166 synthesis_notes: None,
167 },
168 })
169 .unwrap(),
170 ),
171 };
172
173 let query = ReadQuery {
174 file: "src/main.rs".to_string(),
175 anchor: None,
176 lines: None,
177 };
178
179 let results = retrieve_regions(&git, &query).unwrap();
180 assert_eq!(results.len(), 1);
181 assert_eq!(results[0].region.file, "src/main.rs");
182 assert_eq!(results[0].region.intent, "entry point");
183 }
184
185 #[test]
186 fn test_retrieve_filters_by_anchor() {
187 use crate::schema::annotation::*;
188
189 let git = MockGitOps {
190 shas: vec!["abc123".to_string()],
191 note: Some(
192 serde_json::to_string(&Annotation {
193 schema: "chronicle/v1".to_string(),
194 commit: "abc123".to_string(),
195 timestamp: "2025-01-01T00:00:00Z".to_string(),
196 task: None,
197 summary: "test commit".to_string(),
198 context_level: ContextLevel::Enhanced,
199 regions: vec![
200 RegionAnnotation {
201 file: "src/main.rs".to_string(),
202 ast_anchor: AstAnchor {
203 unit_type: "fn".to_string(),
204 name: "main".to_string(),
205 signature: None,
206 },
207 lines: LineRange { start: 1, end: 10 },
208 intent: "entry point".to_string(),
209 reasoning: None,
210 constraints: vec![],
211 semantic_dependencies: vec![],
212 related_annotations: vec![],
213 tags: vec![],
214 risk_notes: None,
215 corrections: vec![],
216 },
217 RegionAnnotation {
218 file: "src/main.rs".to_string(),
219 ast_anchor: AstAnchor {
220 unit_type: "fn".to_string(),
221 name: "helper".to_string(),
222 signature: None,
223 },
224 lines: LineRange { start: 12, end: 20 },
225 intent: "helper fn".to_string(),
226 reasoning: None,
227 constraints: vec![],
228 semantic_dependencies: vec![],
229 related_annotations: vec![],
230 tags: vec![],
231 risk_notes: None,
232 corrections: vec![],
233 },
234 ],
235 cross_cutting: vec![],
236 provenance: Provenance {
237 operation: ProvenanceOperation::Initial,
238 derived_from: vec![],
239 original_annotations_preserved: false,
240 synthesis_notes: None,
241 },
242 })
243 .unwrap(),
244 ),
245 };
246
247 let query = ReadQuery {
248 file: "src/main.rs".to_string(),
249 anchor: Some("main".to_string()),
250 lines: None,
251 };
252
253 let results = retrieve_regions(&git, &query).unwrap();
254 assert_eq!(results.len(), 1);
255 assert_eq!(results[0].region.ast_anchor.name, "main");
256 }
257
258 #[test]
259 fn test_retrieve_filters_by_lines() {
260 use crate::schema::annotation::*;
261
262 let git = MockGitOps {
263 shas: vec!["abc123".to_string()],
264 note: Some(
265 serde_json::to_string(&Annotation {
266 schema: "chronicle/v1".to_string(),
267 commit: "abc123".to_string(),
268 timestamp: "2025-01-01T00:00:00Z".to_string(),
269 task: None,
270 summary: "test commit".to_string(),
271 context_level: ContextLevel::Enhanced,
272 regions: vec![
273 RegionAnnotation {
274 file: "src/main.rs".to_string(),
275 ast_anchor: AstAnchor {
276 unit_type: "fn".to_string(),
277 name: "main".to_string(),
278 signature: None,
279 },
280 lines: LineRange { start: 1, end: 10 },
281 intent: "entry point".to_string(),
282 reasoning: None,
283 constraints: vec![],
284 semantic_dependencies: vec![],
285 related_annotations: vec![],
286 tags: vec![],
287 risk_notes: None,
288 corrections: vec![],
289 },
290 RegionAnnotation {
291 file: "src/main.rs".to_string(),
292 ast_anchor: AstAnchor {
293 unit_type: "fn".to_string(),
294 name: "helper".to_string(),
295 signature: None,
296 },
297 lines: LineRange { start: 50, end: 60 },
298 intent: "helper fn".to_string(),
299 reasoning: None,
300 constraints: vec![],
301 semantic_dependencies: vec![],
302 related_annotations: vec![],
303 tags: vec![],
304 risk_notes: None,
305 corrections: vec![],
306 },
307 ],
308 cross_cutting: vec![],
309 provenance: Provenance {
310 operation: ProvenanceOperation::Initial,
311 derived_from: vec![],
312 original_annotations_preserved: false,
313 synthesis_notes: None,
314 },
315 })
316 .unwrap(),
317 ),
318 };
319
320 let query = ReadQuery {
321 file: "src/main.rs".to_string(),
322 anchor: None,
323 lines: Some(LineRange { start: 5, end: 15 }),
324 };
325
326 let results = retrieve_regions(&git, &query).unwrap();
327 assert_eq!(results.len(), 1);
328 assert_eq!(results[0].region.ast_anchor.name, "main");
329 }
330
331 #[test]
332 fn test_retrieve_skips_commits_without_notes() {
333 let git = MockGitOps {
334 shas: vec!["abc123".to_string()],
335 note: None,
336 };
337
338 let query = ReadQuery {
339 file: "src/main.rs".to_string(),
340 anchor: None,
341 lines: None,
342 };
343
344 let results = retrieve_regions(&git, &query).unwrap();
345 assert!(results.is_empty());
346 }
347
348 struct MockGitOps {
350 shas: Vec<String>,
351 note: Option<String>,
352 }
353
354 impl crate::git::GitOps for MockGitOps {
355 fn diff(&self, _commit: &str) -> Result<Vec<crate::git::FileDiff>, crate::error::GitError> {
356 Ok(vec![])
357 }
358 fn note_read(&self, _commit: &str) -> Result<Option<String>, crate::error::GitError> {
359 Ok(self.note.clone())
360 }
361 fn note_write(&self, _commit: &str, _content: &str) -> Result<(), crate::error::GitError> {
362 Ok(())
363 }
364 fn note_exists(&self, _commit: &str) -> Result<bool, crate::error::GitError> {
365 Ok(self.note.is_some())
366 }
367 fn file_at_commit(
368 &self,
369 _path: &std::path::Path,
370 _commit: &str,
371 ) -> Result<String, crate::error::GitError> {
372 Ok(String::new())
373 }
374 fn commit_info(
375 &self,
376 _commit: &str,
377 ) -> Result<crate::git::CommitInfo, crate::error::GitError> {
378 Ok(crate::git::CommitInfo {
379 sha: "abc123".to_string(),
380 message: "test".to_string(),
381 author_name: "test".to_string(),
382 author_email: "test@test.com".to_string(),
383 timestamp: "2025-01-01T00:00:00Z".to_string(),
384 parent_shas: vec![],
385 })
386 }
387 fn resolve_ref(&self, _refspec: &str) -> Result<String, crate::error::GitError> {
388 Ok("abc123".to_string())
389 }
390 fn config_get(&self, _key: &str) -> Result<Option<String>, crate::error::GitError> {
391 Ok(None)
392 }
393 fn config_set(&self, _key: &str, _value: &str) -> Result<(), crate::error::GitError> {
394 Ok(())
395 }
396 fn log_for_file(&self, _path: &str) -> Result<Vec<String>, crate::error::GitError> {
397 Ok(self.shas.clone())
398 }
399 fn list_annotated_commits(
400 &self,
401 _limit: u32,
402 ) -> Result<Vec<String>, crate::error::GitError> {
403 Ok(self.shas.clone())
404 }
405 }
406}