Skip to main content

_diffctx/edges/history/
cochange.rs

1use std::path::Path;
2
3use rustc_hash::FxHashMap;
4
5use crate::config::limits::COCHANGE;
6use crate::config::weights::EDGE_WEIGHTS;
7use crate::types::{Fragment, FragmentId};
8
9use super::super::EdgeDict;
10use super::super::base::{EdgeBuilder, add_edge};
11
12pub struct CochangeEdgeBuilder;
13
14impl CochangeEdgeBuilder {
15    fn get_git_log_files(&self, repo_root: &Path) -> Option<Vec<Vec<String>>> {
16        let output = crate::git::git_command(repo_root)
17            .args([
18                "log",
19                "--name-only",
20                "--pretty=format:",
21                &format!("-n{}", COCHANGE.commits_limit),
22            ])
23            .output()
24            .ok()?;
25
26        if !output.status.success() {
27            return None;
28        }
29
30        let stdout = String::from_utf8_lossy(&output.stdout);
31        let commits: Vec<Vec<String>> = stdout
32            .split("\n\n")
33            .filter(|c| !c.trim().is_empty())
34            .map(|c| {
35                c.trim()
36                    .split('\n')
37                    .filter(|l| !l.is_empty())
38                    .map(|l| l.to_string())
39                    .collect()
40            })
41            .collect();
42
43        Some(commits)
44    }
45
46    fn count_cochanges(&self, commits: &[Vec<String>]) -> FxHashMap<(String, String), usize> {
47        let mut cochange: FxHashMap<(String, String), usize> = FxHashMap::default();
48        for files in commits {
49            if files.len() > COCHANGE.max_files_per_commit {
50                continue;
51            }
52            for i in 0..files.len() {
53                for j in (i + 1)..files.len() {
54                    let pair = if files[i] < files[j] {
55                        (files[i].clone(), files[j].clone())
56                    } else {
57                        (files[j].clone(), files[i].clone())
58                    };
59                    *cochange.entry(pair).or_insert(0) += 1;
60                }
61            }
62        }
63        cochange
64    }
65
66    fn build_path_to_frags_index(
67        &self,
68        fragments: &[Fragment],
69        repo_root: &Path,
70    ) -> FxHashMap<String, Vec<FragmentId>> {
71        let mut path_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
72        for f in fragments {
73            let path = Path::new(f.path());
74            let rel = if path.is_absolute() {
75                path.strip_prefix(repo_root)
76                    .ok()
77                    .map(|r| r.to_string_lossy().replace('\\', "/"))
78            } else {
79                Some(path.to_string_lossy().replace('\\', "/"))
80            };
81            if let Some(rel) = rel {
82                path_to_frags.entry(rel).or_default().push(f.id.clone());
83            }
84        }
85        path_to_frags
86    }
87}
88
89impl EdgeBuilder for CochangeEdgeBuilder {
90    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
91        let repo_root = match repo_root {
92            Some(r) => r,
93            None => return FxHashMap::default(),
94        };
95
96        let weight = EDGE_WEIGHTS["cochange"].forward;
97        let reverse_factor = EDGE_WEIGHTS["cochange"].reverse_factor;
98
99        let commits = match self.get_git_log_files(repo_root) {
100            Some(c) => c,
101            None => return FxHashMap::default(),
102        };
103
104        let cochange = self.count_cochanges(&commits);
105        let path_to_frags = self.build_path_to_frags_index(fragments, repo_root);
106
107        let mut edges: EdgeDict = FxHashMap::default();
108        for ((p1, p2), count) in &cochange {
109            if *count < COCHANGE.min_count {
110                continue;
111            }
112            let edge_weight = weight.min(COCHANGE.log_scale_factor * (*count as f64).ln_1p());
113            for fid1 in path_to_frags.get(p1).unwrap_or(&vec![]) {
114                for fid2 in path_to_frags.get(p2).unwrap_or(&vec![]) {
115                    if fid1 == fid2 {
116                        continue;
117                    }
118                    add_edge(&mut edges, fid1, fid2, edge_weight, reverse_factor);
119                }
120            }
121        }
122
123        edges
124    }
125
126    fn is_expensive(&self) -> bool {
127        true
128    }
129}