use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitInfo {
pub commit_id: String,
}
pub fn detect(project_root: &Path) -> Option<GitInfo> {
let commit_id = rev_parse_head(project_root)?;
let dirty = is_dirty(project_root);
let full_id = if dirty {
format!("{}-dirty", commit_id)
} else {
commit_id
};
Some(GitInfo { commit_id: full_id })
}
fn rev_parse_head(dir: &Path) -> Option<String> {
let out = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(dir)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let id = String::from_utf8(out.stdout).ok()?;
let id = id.trim().to_owned();
if id.is_empty() { None } else { Some(id) }
}
fn is_dirty(dir: &Path) -> bool {
let out = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(dir)
.output();
match out {
Ok(o) if o.status.success() => !o.stdout.is_empty(),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_returns_none_outside_git_repo() {
let dir = tempfile::Builder::new()
.prefix("curie-test-no-git-")
.tempdir_in(std::env::temp_dir())
.expect("tempdir");
let result = detect(dir.path());
let _ = result; }
#[test]
fn commit_id_dirty_suffix() {
let info = GitInfo { commit_id: "abc123-dirty".to_owned() };
assert!(info.commit_id.ends_with("-dirty"));
}
#[test]
fn commit_id_clean_no_suffix() {
let info = GitInfo { commit_id: "abc123def456".to_owned() };
assert!(!info.commit_id.ends_with("-dirty"));
}
#[test]
fn rev_parse_head_returns_none_for_non_repo() {
let dir = tempfile::Builder::new()
.prefix("curie-test-no-git-")
.tempdir_in(std::env::temp_dir())
.expect("tempdir");
let result = rev_parse_head(dir.path());
assert!(result.is_none());
}
}