1use chrono::{DateTime, Local};
6use std::collections::HashMap;
7
8use crate::event::GitEvent;
9
10#[derive(Debug, Clone)]
12pub struct AuthorStats {
13 pub name: String,
15 pub commit_count: usize,
17 pub insertions: usize,
19 pub deletions: usize,
21 pub last_commit: DateTime<Local>,
23}
24
25impl AuthorStats {
26 pub fn commit_percentage(&self, total: usize) -> f64 {
28 if total == 0 {
29 0.0
30 } else {
31 (self.commit_count as f64 / total as f64) * 100.0
32 }
33 }
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct RepoStats {
39 pub authors: Vec<AuthorStats>,
41 pub total_commits: usize,
43 pub total_insertions: usize,
45 pub total_deletions: usize,
47}
48
49impl RepoStats {
50 pub fn author_count(&self) -> usize {
52 self.authors.len()
53 }
54}
55
56pub fn calculate_stats(events: &[GitEvent]) -> RepoStats {
58 let mut author_map: HashMap<String, AuthorStats> = HashMap::new();
59 let mut total_insertions = 0usize;
60 let mut total_deletions = 0usize;
61
62 for event in events {
63 total_insertions += event.files_added;
64 total_deletions += event.files_deleted;
65
66 let entry = author_map
67 .entry(event.author.clone())
68 .or_insert(AuthorStats {
69 name: event.author.clone(),
70 commit_count: 0,
71 insertions: 0,
72 deletions: 0,
73 last_commit: event.timestamp,
74 });
75
76 entry.commit_count += 1;
77 entry.insertions += event.files_added;
78 entry.deletions += event.files_deleted;
79
80 if event.timestamp > entry.last_commit {
82 entry.last_commit = event.timestamp;
83 }
84 }
85
86 let mut authors: Vec<AuthorStats> = author_map.into_values().collect();
88 authors.sort_by(|a, b| b.commit_count.cmp(&a.commit_count));
89
90 RepoStats {
91 authors,
92 total_commits: events.len(),
93 total_insertions,
94 total_deletions,
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use chrono::Local;
102
103 fn create_test_event(author: &str, insertions: usize, deletions: usize) -> GitEvent {
104 GitEvent::commit(
105 "abc1234".to_string(),
106 "test commit".to_string(),
107 author.to_string(),
108 Local::now(),
109 insertions,
110 deletions,
111 )
112 }
113
114 #[test]
115 fn test_calculate_stats_empty() {
116 let stats = calculate_stats(&[]);
117 assert_eq!(stats.total_commits, 0);
118 assert_eq!(stats.authors.len(), 0);
119 }
120
121 #[test]
122 fn test_calculate_stats_single_author() {
123 let events = vec![
124 create_test_event("Alice", 10, 5),
125 create_test_event("Alice", 20, 10),
126 ];
127 let stats = calculate_stats(&events);
128
129 assert_eq!(stats.total_commits, 2);
130 assert_eq!(stats.authors.len(), 1);
131 assert_eq!(stats.authors[0].name, "Alice");
132 assert_eq!(stats.authors[0].commit_count, 2);
133 assert_eq!(stats.authors[0].insertions, 30);
134 assert_eq!(stats.authors[0].deletions, 15);
135 }
136
137 #[test]
138 fn test_calculate_stats_multiple_authors() {
139 let events = vec![
140 create_test_event("Alice", 10, 5),
141 create_test_event("Bob", 5, 2),
142 create_test_event("Alice", 20, 10),
143 create_test_event("Bob", 15, 8),
144 create_test_event("Bob", 10, 5),
145 ];
146 let stats = calculate_stats(&events);
147
148 assert_eq!(stats.total_commits, 5);
149 assert_eq!(stats.authors.len(), 2);
150
151 assert_eq!(stats.authors[0].name, "Bob");
153 assert_eq!(stats.authors[0].commit_count, 3);
154
155 assert_eq!(stats.authors[1].name, "Alice");
157 assert_eq!(stats.authors[1].commit_count, 2);
158 }
159
160 #[test]
161 fn test_calculate_stats_totals() {
162 let events = vec![
163 create_test_event("Alice", 10, 5),
164 create_test_event("Bob", 20, 10),
165 ];
166 let stats = calculate_stats(&events);
167
168 assert_eq!(stats.total_insertions, 30);
169 assert_eq!(stats.total_deletions, 15);
170 }
171
172 #[test]
173 fn test_author_stats_commit_percentage() {
174 let author = AuthorStats {
175 name: "Alice".to_string(),
176 commit_count: 25,
177 insertions: 0,
178 deletions: 0,
179 last_commit: Local::now(),
180 };
181
182 assert!((author.commit_percentage(100) - 25.0).abs() < 0.01);
183 assert!((author.commit_percentage(50) - 50.0).abs() < 0.01);
184 }
185
186 #[test]
187 fn test_author_stats_commit_percentage_zero() {
188 let author = AuthorStats {
189 name: "Alice".to_string(),
190 commit_count: 10,
191 insertions: 0,
192 deletions: 0,
193 last_commit: Local::now(),
194 };
195
196 assert_eq!(author.commit_percentage(0), 0.0);
197 }
198
199 #[test]
200 fn test_repo_stats_author_count() {
201 let events = vec![
202 create_test_event("Alice", 10, 5),
203 create_test_event("Bob", 5, 2),
204 create_test_event("Charlie", 15, 8),
205 ];
206 let stats = calculate_stats(&events);
207
208 assert_eq!(stats.author_count(), 3);
209 }
210}