use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::git::{CoChangeHistory, FileDiffStat};
#[derive(Debug, Clone, PartialEq)]
pub struct Diffusion {
pub files: usize,
pub directories: usize,
pub subsystems: usize,
pub lines_added: usize,
pub lines_deleted: usize,
pub entropy: f64,
}
fn subsystem(path: &Path) -> &Path {
let mut components = path.components();
match (components.next(), components.next()) {
(Some(first), Some(_)) => Path::new(first.as_os_str()),
_ => Path::new("."),
}
}
fn normalized_entropy(weights: &[usize]) -> f64 {
let weights: Vec<f64> = weights
.iter()
.filter(|&&w| w > 0)
.map(|&w| w as f64)
.collect();
if weights.len() < 2 {
return 0.0;
}
let total: f64 = weights.iter().sum();
let entropy: f64 = weights
.iter()
.map(|w| {
let p = w / total;
-p * p.log2()
})
.sum();
entropy / (weights.len() as f64).log2()
}
pub fn compute_diffusion(changes: &[FileDiffStat]) -> Diffusion {
let directories: HashSet<&Path> = changes
.iter()
.map(|c| c.path.parent().unwrap_or(Path::new("")))
.collect();
let subsystems: HashSet<&Path> = changes.iter().map(|c| subsystem(&c.path)).collect();
let modified: Vec<usize> = changes.iter().map(|c| c.added + c.deleted).collect();
Diffusion {
files: changes.len(),
directories: directories.len(),
subsystems: subsystems.len(),
lines_added: changes.iter().map(|c| c.added).sum(),
lines_deleted: changes.iter().map(|c| c.deleted).sum(),
entropy: normalized_entropy(&modified),
}
}
pub struct Target {
pub path: PathBuf,
pub history_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Trigger {
pub path: PathBuf,
pub shared_commits: usize,
pub commits: usize,
pub confidence: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MissingCoChange {
pub path: PathBuf,
pub triggers: Vec<Trigger>,
}
impl MissingCoChange {
pub fn best(&self) -> &Trigger {
&self.triggers[0]
}
}
#[derive(Debug, Clone, Copy)]
pub struct Thresholds {
pub min_confidence: f64,
pub min_shared: usize,
}
fn by_strength(a: &Trigger, b: &Trigger) -> std::cmp::Ordering {
b.confidence
.total_cmp(&a.confidence)
.then_with(|| b.shared_commits.cmp(&a.shared_commits))
.then_with(|| a.path.cmp(&b.path))
}
pub fn missing_co_changes(
history: &CoChangeHistory,
targets: &[Target],
is_candidate: impl Fn(&Path) -> bool,
thresholds: Thresholds,
) -> Vec<MissingCoChange> {
let mut by_path: HashMap<&Path, Vec<Trigger>> = HashMap::new();
for target in targets {
let (Some(&commits), Some(shared)) = (
history.commits.get(&target.history_path),
history.shared.get(&target.history_path),
) else {
continue;
};
for (other, &shared_commits) in shared {
let confidence = shared_commits as f64 / commits as f64;
if shared_commits < thresholds.min_shared
|| confidence < thresholds.min_confidence
|| !is_candidate(other)
{
continue;
}
by_path.entry(other).or_default().push(Trigger {
path: target.path.clone(),
shared_commits,
commits,
confidence,
});
}
}
let mut missing: Vec<MissingCoChange> = by_path
.into_iter()
.map(|(path, mut triggers)| {
triggers.sort_by(by_strength);
MissingCoChange {
path: path.to_path_buf(),
triggers,
}
})
.collect();
missing.sort_by(|a, b| by_strength(a.best(), b.best()).then_with(|| a.path.cmp(&b.path)));
missing
}
#[cfg(test)]
#[path = "analyzer_test.rs"]
mod tests;