use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Debug, Default)]
pub struct GitHeatScan {
pub snapshot_id: Uuid,
pub ts: DateTime<Utc>,
pub repo: String,
pub files: Vec<GitHeatRow>,
}
#[derive(Debug, Clone)]
pub struct GitHeatRow {
pub file: String,
pub commits_total: i64,
pub commits_30d: i64,
pub commits_90d: i64,
pub authors_total: i64,
pub last_commit_ts: DateTime<Utc>,
}
pub fn scan_repo(
repo_root: &Path,
repo_name: &str,
snapshot_id: Uuid,
ts: DateTime<Utc>,
) -> Result<GitHeatScan> {
let mut out = GitHeatScan {
snapshot_id,
ts,
repo: repo_name.to_string(),
files: Vec::new(),
};
let repo = match gix::discover(repo_root) {
Ok(r) => r,
Err(_) => return Ok(out), };
let head_id = match repo.head_id() {
Ok(id) => id,
Err(_) => return Ok(out),
};
let mut per_file: BTreeMap<String, FileAgg> = BTreeMap::new();
let now = ts;
let cutoff_30 = now - chrono::Duration::days(30);
let cutoff_90 = now - chrono::Duration::days(90);
let walk = repo.rev_walk([head_id.detach()])
.all()
.context("rev_walk all")?;
for info in walk {
let info = match info { Ok(i) => i, Err(_) => continue };
let commit = match info.object() { Ok(c) => c, Err(_) => continue };
let author = match commit.author() {
Ok(a) => format!("{} <{}>", a.name, a.email),
Err(_) => "unknown".into(),
};
let commit_time = match commit.time() {
Ok(t) => DateTime::<Utc>::from_timestamp(t.seconds, 0).unwrap_or(now),
Err(_) => now,
};
let tree = match commit.tree() { Ok(t) => t, Err(_) => continue };
let parent_tree = commit
.parent_ids()
.next()
.and_then(|pid| repo.find_object(pid).ok())
.and_then(|o| o.try_into_commit().ok())
.and_then(|c| c.tree().ok());
let mut touched: BTreeSet<String> = BTreeSet::new();
if let Some(p_tree) = parent_tree {
let _ = p_tree.changes().context("changes()")?.for_each_to_obtain_tree(
&tree,
|change| -> std::result::Result<std::ops::ControlFlow<()>, std::convert::Infallible> {
let loc = change.location();
touched.insert(loc.to_string());
Ok(std::ops::ControlFlow::Continue(()))
},
);
} else {
for entry in tree.iter().flatten() {
touched.insert(entry.filename().to_string());
}
}
for path in touched {
let agg = per_file.entry(path).or_insert_with(|| FileAgg {
commits_total: 0,
commits_30d: 0,
commits_90d: 0,
authors: BTreeSet::new(),
last_commit_ts: commit_time,
});
agg.commits_total += 1;
if commit_time >= cutoff_30 { agg.commits_30d += 1; }
if commit_time >= cutoff_90 { agg.commits_90d += 1; }
agg.authors.insert(author.clone());
if commit_time > agg.last_commit_ts {
agg.last_commit_ts = commit_time;
}
}
}
for (file, agg) in per_file {
out.files.push(GitHeatRow {
file,
commits_total: agg.commits_total,
commits_30d: agg.commits_30d,
commits_90d: agg.commits_90d,
authors_total: agg.authors.len() as i64,
last_commit_ts: agg.last_commit_ts,
});
}
Ok(out)
}
struct FileAgg {
commits_total: i64,
commits_30d: i64,
commits_90d: i64,
authors: BTreeSet<String>,
last_commit_ts: DateTime<Utc>,
}