use std::sync::mpsc;
use std::time::Duration;
use trusty_mpm::core::spawn_disclaim::disclaimed_stdout_piped_spawn;
use super::CommitLine;
pub(crate) fn probe_recent_commits(workdir: &str) -> Vec<CommitLine> {
let path = workdir.to_string();
let (tx, rx) = mpsc::channel::<Vec<CommitLine>>();
std::thread::spawn(move || {
let commits = run_git_log(&path).unwrap_or_default();
let _ = tx.send(commits);
});
rx.recv_timeout(Duration::from_millis(150))
.unwrap_or_default()
}
fn run_git_log(workdir: &str) -> Option<Vec<CommitLine>> {
let mut cmd = trusty_common::git::command();
cmd.arg("-C")
.arg(workdir)
.args(["log", "--oneline", "-5", "--format=%h|%cr|%s"]);
let spawned = disclaimed_stdout_piped_spawn(cmd).ok()?;
let pid = spawned.id;
std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(3));
#[cfg(unix)]
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGKILL);
}
#[cfg(not(unix))]
{
let _ = std::process::Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.status();
}
});
let out = spawned.wait_with_output().ok()?;
if !out.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&out.stdout);
let commits: Vec<CommitLine> = stdout.lines().filter_map(parse_commit_line).collect();
if commits.is_empty() {
None
} else {
Some(commits)
}
}
fn parse_commit_line(line: &str) -> Option<CommitLine> {
let mut parts = line.splitn(3, '|');
let sha = parts.next()?.trim().to_string();
let age_raw = parts.next()?.trim().to_string();
let subject = parts.next()?.trim().to_string();
if sha.is_empty() {
return None;
}
let age = shorten_git_age(&age_raw);
Some(CommitLine { sha, age, subject })
}
pub(crate) fn shorten_git_age(raw: &str) -> String {
let s = raw.trim();
if let Some(rest) = s.strip_prefix("a ").or_else(|| s.strip_prefix("an ")) {
let unit = rest.split_whitespace().next().unwrap_or("");
return format!("1{}", unit_suffix(unit));
}
let mut words = s.split_whitespace();
let n_str = words.next().unwrap_or("?");
let unit = words.next().unwrap_or("");
if let Ok(n) = n_str.parse::<u64>() {
format!("{}{}", n, unit_suffix(unit))
} else {
let short: String = s.chars().take(8).collect();
short
}
}
fn unit_suffix(unit: &str) -> &'static str {
match unit {
"seconds" | "second" => "s",
"minutes" | "minute" => "m",
"hours" | "hour" => "h",
"days" | "day" => "d",
"weeks" | "week" => "w",
"months" | "month" => "mo",
"years" | "year" => "y",
_ => "?",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_commits_returns_empty_for_non_git_dir() {
let commits = probe_recent_commits("/tmp");
assert!(
commits.is_empty(),
"expected empty commits for non-git dir, got: {commits:?}"
);
}
#[test]
fn shorten_age_seconds() {
assert_eq!(shorten_git_age("45 seconds ago"), "45s");
}
#[test]
fn shorten_age_minutes() {
assert_eq!(shorten_git_age("3 minutes ago"), "3m");
}
#[test]
fn shorten_age_hours() {
assert_eq!(shorten_git_age("2 hours ago"), "2h");
}
#[test]
fn shorten_age_days() {
assert_eq!(shorten_git_age("5 days ago"), "5d");
}
#[test]
fn shorten_age_weeks() {
assert_eq!(shorten_git_age("1 week ago"), "1w");
}
#[test]
fn shorten_age_months() {
assert_eq!(shorten_git_age("2 months ago"), "2mo");
}
#[test]
fn shorten_age_years() {
assert_eq!(shorten_git_age("1 year ago"), "1y");
}
#[test]
fn shorten_age_a_minute() {
assert_eq!(shorten_git_age("a minute ago"), "1m");
}
#[test]
fn shorten_age_an_hour() {
assert_eq!(shorten_git_age("an hour ago"), "1h");
}
#[test]
fn shorten_age_unknown_truncates() {
let raw = "right now tomorrow";
let result = shorten_git_age(raw);
assert!(
result.chars().count() <= 8,
"unknown age must be truncated to ≤8 chars: {result:?}"
);
}
#[test]
fn probe_commits_real_repo_returns_commits() {
let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let commits = probe_recent_commits(&dir);
for c in &commits {
assert!(!c.sha.is_empty(), "sha must not be empty");
assert!(!c.age.is_empty(), "age must not be empty");
}
}
}