use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Default)]
pub struct GitMeta {
pub available: bool,
pub head_id: Option<String>,
pub branch: Option<String>,
pub commit_count_sampled: usize,
pub days_since_last_commit: Option<u64>,
pub conventional_commit_ratio: Option<f64>,
pub issue_link_ratio: Option<f64>,
pub recent_subjects: Vec<String>,
pub note: Option<String>,
}
pub fn inspect(git_dir: &Path, repo_root: &Path, sample_limit: usize) -> Result<GitMeta> {
let mut meta = GitMeta {
available: true,
..GitMeta::default()
};
let repo = match gix::open(repo_root) {
Ok(r) => r,
Err(e) => match gix::open(git_dir.parent().unwrap_or(repo_root)) {
Ok(r) => r,
Err(_) => {
meta.note = Some(format!("unable to open git repository: {e}"));
return Ok(meta);
}
},
};
match repo.head() {
Ok(head) => {
if let Some(name) = head.referent_name() {
let s = name.as_bstr().to_string();
meta.branch = s
.strip_prefix("refs/heads/")
.map(str::to_string)
.or(Some(s));
}
match head.id() {
Some(id) => {
let oid = id.detach();
meta.head_id = Some(oid.to_string());
enrich_history(&repo, oid, sample_limit, &mut meta);
}
None => {
meta.note = Some("repository has no commits yet".into());
}
}
}
Err(e) => {
meta.note = Some(format!("unable to read HEAD: {e}"));
}
}
Ok(meta)
}
fn enrich_history(
repo: &gix::Repository,
head: gix::ObjectId,
sample_limit: usize,
meta: &mut GitMeta,
) {
let Ok(commit) = repo.find_commit(head) else {
meta.note = Some("unable to load HEAD commit".into());
return;
};
let mut ids = Vec::new();
match repo.rev_walk([head]).all() {
Ok(walk) => {
for item in walk.take(sample_limit) {
match item {
Ok(info) => ids.push(info.id),
Err(_) => break,
}
}
}
Err(_) => {
ids.push(head);
}
}
if ids.is_empty() {
ids.push(head);
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs() as i64;
if let Ok(time) = commit.time() {
let ts = time.seconds;
if ts > 0 && now >= ts {
meta.days_since_last_commit = Some(((now - ts) as u64) / 86_400);
}
}
let conventional_re = regex::Regex::new(
r"(?i)^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?:",
)
.ok();
let issue_re = regex::Regex::new(r"(?i)(#[0-9]+|[A-Z][A-Z0-9]+-\d+)").ok();
let mut conventional = 0usize;
let mut linked = 0usize;
let mut subjects = Vec::new();
for id in &ids {
let Ok(c) = repo.find_commit(*id) else {
continue;
};
let Ok(msg) = c.message_raw() else {
continue;
};
let msg = msg.to_string();
let subject = msg.lines().next().unwrap_or("").trim().to_string();
if subjects.len() < 10 && !subject.is_empty() {
subjects.push(subject.clone());
}
if let Some(re) = &conventional_re
&& re.is_match(&subject)
{
conventional += 1;
}
if let Some(re) = &issue_re
&& re.is_match(&msg)
{
linked += 1;
}
}
let n = ids.len();
meta.commit_count_sampled = n;
meta.recent_subjects = subjects;
if n > 0 {
meta.conventional_commit_ratio = Some(conventional as f64 / n as f64);
meta.issue_link_ratio = Some(linked as f64 / n as f64);
}
}
#[allow(dead_code)]
pub fn map_err(err: impl std::fmt::Display) -> Error {
Error::Git(err.to_string())
}