history.rs•4.59 kB
use crate::{errors::*, types::*};
use git2::{DiffOptions, Sort};
use std::collections::{BTreeMap, HashMap};
impl super::GitRepository {
pub fn analyze_history(&self, opts: HistoryOptions) -> Result<HistoryInsights> {
let repo = self.repository();
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(Sort::TIME | Sort::REVERSE)?;
// Determine start point
if let Some(branch) = &opts.branch {
let reference = repo.resolve_reference_from_short_name(branch)?;
revwalk.push(
reference
.target()
.ok_or_else(|| git2::Error::from_str("Invalid reference target"))?,
)?;
} else {
revwalk.push_head()?;
}
let mut total_commits = 0usize;
let mut authors: HashMap<(String, String), usize> = HashMap::new();
let mut file_churn: BTreeMap<String, FileChurn> = BTreeMap::new();
for (i, oid_res) in revwalk.enumerate() {
if let Some(max) = opts.max_commits {
if i >= max {
break;
}
}
let oid = match oid_res {
Ok(oid) => oid,
Err(_) => continue,
};
let commit = match repo.find_commit(oid) {
Ok(c) => c,
Err(_) => continue,
};
if let Some(since) = opts.since_timestamp {
if commit.time().seconds() < since {
continue;
}
}
total_commits += 1;
let sig = commit.author();
let name = sig.name().unwrap_or("unknown").to_string();
let email = sig.email().unwrap_or("").to_string();
*authors.entry((name, email)).or_insert(0) += 1;
if commit.parent_count() == 0 {
continue;
}
let _parent = match commit.parent(0) {
Ok(p) => p,
Err(_) => continue,
};
let mut diffopts = DiffOptions::new();
let diff = repo.diff_tree_to_tree(
commit
.parent(0)
.ok()
.as_ref()
.map(|p| p.tree().ok())
.flatten()
.as_ref(),
Some(&commit.tree().ok().unwrap()),
Some(&mut diffopts),
)?;
let stats = diff.stats()?;
// Iterate deltas for per-file churn
diff.foreach(
&mut |delta, _| {
if let Some(path) = delta.new_file().path().or_else(|| delta.old_file().path())
{
if let Some(path_str) = path.to_str() {
let entry =
file_churn.entry(path_str.to_string()).or_insert(FileChurn {
additions: 0,
deletions: 0,
});
// Approximate by distributing stats evenly is too naive; do per-patch below if available
let _ = entry; // fallthrough
}
}
true
},
None,
None,
None,
)?;
// If patch stats per file are needed, we can compute patches and sum hunk changes; keep it lightweight for large repos.
let insertions = stats.insertions();
let deletions = stats.deletions();
// Assign to a synthetic "_aggregate" for repo-wide churn
let agg = file_churn.entry("_aggregate".into()).or_insert(FileChurn {
additions: 0,
deletions: 0,
});
agg.additions += insertions as usize;
agg.deletions += deletions as usize;
}
let mut authors_vec: Vec<AuthorStat> = authors
.into_iter()
.map(|((name, email), commits)| AuthorStat {
name,
email,
commits,
})
.collect();
authors_vec.sort_by(|a, b| b.commits.cmp(&a.commits));
// Count branches analyzed
let branches_analyzed = repo.branches(Some(git2::BranchType::Local))?.count();
Ok(HistoryInsights {
total_commits,
authors: authors_vec,
file_churn,
branches_analyzed,
})
}
}