use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use super::bus_factor::{self, BusFactor, FileAuthorship};
use super::cache::CommitEvent;
use super::classify::Classification;
use super::entropy::{self, CochangeGraph};
use super::identity::AuthorId;
use super::options::{Options, RiskFormula};
use super::score;
use super::stats::{Accumulator, ChangeRecord, Stats};
pub(crate) struct FoldContext<'a> {
pub commit_time: i64,
pub in_recent: bool,
pub class: Classification,
pub authors: &'a [AuthorId],
}
const MAX_ALIAS_DEPTH: usize = 10_000;
pub(crate) fn resolve_alias<'a>(
alias: &'a HashMap<PathBuf, PathBuf>,
path: &'a Path,
) -> Cow<'a, Path> {
let mut current: &'a Path = path;
let mut visited: HashSet<&'a Path> = HashSet::new();
for _ in 0..MAX_ALIAS_DEPTH {
if !visited.insert(current) {
break;
}
match alias.get(current) {
Some(next) => current = next.as_path(),
None => break,
}
}
Cow::Borrowed(current)
}
#[allow(clippy::cast_precision_loss)]
pub(crate) fn fold_commit(
touched: &[(PathBuf, u64)],
options: &Options,
accumulators: &mut HashMap<PathBuf, Accumulator>,
graph: &mut CochangeGraph,
ctx: &FoldContext<'_>,
) {
let churn_bits = || touched.iter().map(|&(_, churn)| churn as f64);
let total: f64 = churn_bits().sum();
let commit_entropy = entropy::shannon_entropy(churn_bits());
for (path, churn) in touched {
let change_entropy = if total > 0.0 {
(*churn as f64 / total) * commit_entropy
} else {
0.0
};
let accumulator = match accumulators.get_mut(path) {
Some(acc) => acc,
None if options.include_deleted && options.file_types.includes(path) => accumulators
.entry(path.clone())
.or_insert_with(|| Accumulator::new(0)),
None => continue,
};
accumulator.record(&ChangeRecord {
churn: *churn,
commit_time: ctx.commit_time,
in_recent: ctx.in_recent,
class: ctx.class,
authors: ctx.authors,
change_entropy,
});
}
let paths: Vec<&Path> = touched.iter().map(|(path, _)| path.as_path()).collect();
graph.record_commit(&paths, ctx.in_recent);
}
pub(crate) struct ReplayOutput {
pub files: HashMap<PathBuf, Stats>,
pub bus_factor: Option<BusFactor>,
}
pub(crate) fn replay(
seed_files: HashMap<PathBuf, u64>,
events: &[CommitEvent],
options: &Options,
now: i64,
) -> ReplayOutput {
let mut accumulators: HashMap<PathBuf, Accumulator> = seed_files
.into_iter()
.map(|(path, sloc)| (path, Accumulator::new(sloc)))
.collect();
let long_boundary = now.saturating_sub(options.long_window_secs);
let recent_boundary = now.saturating_sub(options.recent_window_secs);
let mut graph = CochangeGraph::new();
let mut alias: HashMap<PathBuf, PathBuf> = HashMap::new();
for event in events {
let commit_time = event.time.min(now);
if commit_time < long_boundary {
continue;
}
for (source, dest) in &event.renames {
alias.insert(source.clone(), dest.clone());
}
let authors = event.author_ids();
let touched: Vec<(PathBuf, u64)> = event
.touched
.iter()
.map(|(location, churn)| (resolve_alias(&alias, location).into_owned(), *churn))
.collect();
let ctx = FoldContext {
commit_time,
in_recent: commit_time >= recent_boundary,
class: event.classification(),
authors: &authors,
};
fold_commit(&touched, options, &mut accumulators, &mut graph, &ctx);
}
let bus_factor = options
.compute_bus_factor
.then(|| bus_factor_aggregate(&accumulators, options))
.flatten();
let files = finalize(accumulators, &graph, options, now);
ReplayOutput { files, bus_factor }
}
pub(crate) fn bus_factor_aggregate(
accumulators: &HashMap<PathBuf, Accumulator>,
options: &Options,
) -> Option<BusFactor> {
let authorship: Vec<FileAuthorship> = accumulators
.iter()
.filter_map(|(path, acc)| {
acc.authorship().map(|contributions| FileAuthorship {
path: path.clone(),
contributions,
})
})
.collect();
if authorship.is_empty() {
return None;
}
Some(bus_factor::compute(
&authorship,
options.bus_factor_threshold,
options.emit_author_details,
options.author_hash_key.as_ref(),
))
}
pub(crate) fn finalize(
accumulators: HashMap<PathBuf, Accumulator>,
graph: &CochangeGraph,
options: &Options,
now: i64,
) -> HashMap<PathBuf, Stats> {
let (paths, mut stats): (Vec<PathBuf>, Vec<Stats>) = accumulators
.into_iter()
.map(|(path, acc)| {
let (cochange_long, cochange_recent) = graph.entropy(&path);
let stats = acc.finalize(now, options, cochange_long, cochange_recent);
(path, stats)
})
.unzip();
if options.risk_formula == RiskFormula::Percentile {
score::apply_percentile(&mut stats);
}
paths.into_iter().zip(stats).collect()
}
#[cfg(test)]
#[path = "replay_tests.rs"]
mod tests;