_diffctx/edges/history/
cochange.rs1use 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}