Skip to main content

gitstack/
related_files.rs

1//! 関連ファイル分析機能
2//!
3//! コミット履歴から「一緒に変更されることが多いファイル」を分析
4
5use std::collections::HashMap;
6
7/// 関連ファイル情報
8#[derive(Debug, Clone)]
9pub struct RelatedFiles {
10    /// 対象ファイル
11    pub target_file: String,
12    /// 関連ファイル一覧(ファイルパス, 共変更回数)
13    pub related: Vec<(String, usize)>,
14}
15
16/// 関連ファイルを分析
17///
18/// target_file: 対象ファイルパス
19/// commit_files: 各コミットの変更ファイル一覧を取得するイテレータ
20pub fn analyze_related_files<I>(target_file: &str, commit_files: I) -> RelatedFiles
21where
22    I: Iterator<Item = Vec<String>>,
23{
24    let mut co_change_counts: HashMap<String, usize> = HashMap::new();
25
26    for files in commit_files {
27        // このコミットに対象ファイルが含まれているか
28        if files.iter().any(|f| f == target_file) {
29            // 同じコミット内の他のファイルをカウント
30            for file in files {
31                if file != target_file {
32                    *co_change_counts.entry(file).or_insert(0) += 1;
33                }
34            }
35        }
36    }
37
38    // カウント順にソート
39    let mut related: Vec<(String, usize)> = co_change_counts.into_iter().collect();
40    related.sort_by(|a, b| b.1.cmp(&a.1));
41
42    // 上位20件に制限
43    related.truncate(20);
44
45    RelatedFiles {
46        target_file: target_file.to_string(),
47        related,
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn test_analyze_related_files_basic() {
57        let commits = vec![
58            vec!["src/a.rs".to_string(), "src/b.rs".to_string()],
59            vec!["src/a.rs".to_string(), "src/c.rs".to_string()],
60            vec![
61                "src/a.rs".to_string(),
62                "src/b.rs".to_string(),
63                "src/c.rs".to_string(),
64            ],
65            vec!["src/d.rs".to_string()], // a.rs無し
66        ];
67
68        let result = analyze_related_files("src/a.rs", commits.into_iter());
69
70        assert_eq!(result.target_file, "src/a.rs");
71        // b.rs: 2回, c.rs: 2回
72        assert!(result
73            .related
74            .iter()
75            .any(|(f, c)| f == "src/b.rs" && *c == 2));
76        assert!(result
77            .related
78            .iter()
79            .any(|(f, c)| f == "src/c.rs" && *c == 2));
80        // d.rsはa.rsと共変更なし
81        assert!(!result.related.iter().any(|(f, _)| f == "src/d.rs"));
82    }
83
84    #[test]
85    fn test_analyze_related_files_no_target() {
86        let commits = vec![
87            vec!["src/x.rs".to_string(), "src/y.rs".to_string()],
88            vec!["src/z.rs".to_string()],
89        ];
90
91        let result = analyze_related_files("src/a.rs", commits.into_iter());
92
93        assert!(result.related.is_empty());
94    }
95
96    #[test]
97    fn test_analyze_related_files_empty() {
98        let commits: Vec<Vec<String>> = vec![];
99        let result = analyze_related_files("src/a.rs", commits.into_iter());
100        assert!(result.related.is_empty());
101    }
102}