1use std::collections::HashSet;
6
7use crate::event::GitEvent;
8
9#[derive(Debug, Clone)]
11pub struct RelevanceScore {
12 pub hash: String,
14 pub score: f32,
16 pub matched_files: usize,
18}
19
20pub fn calculate_relevance<F>(
26 working_files: &[String],
27 events: &[&GitEvent],
28 get_commit_files: F,
29) -> Vec<RelevanceScore>
30where
31 F: Fn(&str) -> Option<Vec<String>>,
32{
33 if working_files.is_empty() {
34 return Vec::new();
35 }
36
37 let working_set: HashSet<&str> = working_files.iter().map(|s| s.as_str()).collect();
38
39 events
40 .iter()
41 .filter_map(|event| {
42 let files = get_commit_files(&event.short_hash)?;
43 let matched = files
44 .iter()
45 .filter(|f| working_set.contains(f.as_str()))
46 .count();
47
48 if matched == 0 {
49 return None;
50 }
51
52 let match_ratio = matched as f32 / working_files.len() as f32;
54 let file_weight = (matched as f32).sqrt() / (files.len() as f32).sqrt().max(1.0);
55 let score = (match_ratio * 0.7 + file_weight * 0.3).min(1.0);
56
57 Some(RelevanceScore {
58 hash: event.short_hash.clone(),
59 score,
60 matched_files: matched,
61 })
62 })
63 .collect()
64}
65
66pub fn sort_by_relevance(scores: &[RelevanceScore]) -> Vec<(usize, f32)> {
68 let mut indexed: Vec<(usize, f32)> = scores
69 .iter()
70 .enumerate()
71 .map(|(i, s)| (i, s.score))
72 .collect();
73 indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
74 indexed
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::event::GitEvent;
81 use chrono::Local;
82
83 fn create_test_event(hash: &str) -> GitEvent {
84 GitEvent::commit(
85 hash.to_string(),
86 "test".to_string(),
87 "author".to_string(),
88 Local::now(),
89 1,
90 0,
91 )
92 }
93
94 #[test]
95 fn test_calculate_relevance_empty_working_files() {
96 let event = create_test_event("abc1234");
97 let events = vec![&event];
98 let scores = calculate_relevance(&[], &events, |_| Some(vec!["file.rs".to_string()]));
99 assert!(scores.is_empty());
100 }
101
102 #[test]
103 fn test_calculate_relevance_with_matches() {
104 let event = create_test_event("abc1234");
105 let events = vec![&event];
106 let working_files = vec!["src/main.rs".to_string(), "src/lib.rs".to_string()];
107
108 let scores = calculate_relevance(&working_files, &events, |_| {
109 Some(vec!["src/main.rs".to_string(), "other.rs".to_string()])
110 });
111
112 assert_eq!(scores.len(), 1);
113 assert_eq!(scores[0].matched_files, 1);
114 assert!(scores[0].score > 0.0);
115 }
116
117 #[test]
118 fn test_calculate_relevance_no_matches() {
119 let event = create_test_event("abc1234");
120 let events = vec![&event];
121 let working_files = vec!["src/main.rs".to_string()];
122
123 let scores = calculate_relevance(&working_files, &events, |_| {
124 Some(vec!["other.rs".to_string()])
125 });
126
127 assert!(scores.is_empty());
128 }
129}