Skip to main content

gitstack/
stats.rs

1//! 統計情報モジュール
2//!
3//! リポジトリの著者別統計やファイル変更頻度を計算する
4
5use chrono::{DateTime, Local};
6use std::collections::HashMap;
7
8use crate::event::GitEvent;
9
10/// 著者別統計情報
11#[derive(Debug, Clone)]
12pub struct AuthorStats {
13    /// 著者名
14    pub name: String,
15    /// コミット数
16    pub commit_count: usize,
17    /// 追加行数
18    pub insertions: usize,
19    /// 削除行数
20    pub deletions: usize,
21    /// 最終コミット日時
22    pub last_commit: DateTime<Local>,
23}
24
25impl AuthorStats {
26    /// コミット割合を計算(パーセント)
27    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/// リポジトリ全体の統計情報
37#[derive(Debug, Clone, Default)]
38pub struct RepoStats {
39    /// 著者別統計(コミット数降順)
40    pub authors: Vec<AuthorStats>,
41    /// 総コミット数
42    pub total_commits: usize,
43    /// 総追加行数
44    pub total_insertions: usize,
45    /// 総削除行数
46    pub total_deletions: usize,
47}
48
49impl RepoStats {
50    /// 著者数を取得
51    pub fn author_count(&self) -> usize {
52        self.authors.len()
53    }
54}
55
56/// イベント一覧から統計情報を計算
57pub 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        // 最新のコミット日時を更新
81        if event.timestamp > entry.last_commit {
82            entry.last_commit = event.timestamp;
83        }
84    }
85
86    // コミット数降順でソート
87    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        // Bobが3コミットで最多なので先頭
152        assert_eq!(stats.authors[0].name, "Bob");
153        assert_eq!(stats.authors[0].commit_count, 3);
154
155        // Aliceが2コミット
156        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}