use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct GitState {
pub branch: String,
pub commit: String,
pub dirty: bool,
}
pub fn is_git_repo(root: impl AsRef<Path>) -> bool {
root.as_ref().join(".git").exists()
}
pub fn get_current_branch(root: impl AsRef<Path>) -> Result<String> {
let output = Command::new("git")
.arg("-C")
.arg(root.as_ref())
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.output()
.context("Failed to execute git rev-parse")?;
if !output.status.success() {
anyhow::bail!(
"git rev-parse failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let branch = String::from_utf8(output.stdout)
.context("Invalid UTF-8 in branch name")?
.trim()
.to_string();
Ok(branch)
}
pub fn get_current_commit(root: impl AsRef<Path>) -> Result<String> {
let output = Command::new("git")
.arg("-C")
.arg(root.as_ref())
.args(["rev-parse", "HEAD"])
.output()
.context("Failed to execute git rev-parse HEAD")?;
if !output.status.success() {
anyhow::bail!(
"git rev-parse HEAD failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let commit = String::from_utf8(output.stdout)
.context("Invalid UTF-8 in commit SHA")?
.trim()
.to_string();
Ok(commit)
}
pub fn has_uncommitted_changes(root: impl AsRef<Path>) -> Result<bool> {
let output = Command::new("git")
.arg("-C")
.arg(root.as_ref())
.args(["status", "--porcelain"])
.output()
.context("Failed to execute git status")?;
if !output.status.success() {
anyhow::bail!(
"git status failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let has_changes = !output.stdout.is_empty();
Ok(has_changes)
}
pub fn get_git_state(root: impl AsRef<Path>) -> Result<GitState> {
let root = root.as_ref();
if !is_git_repo(root) {
anyhow::bail!("Not a git repository");
}
let branch = get_current_branch(root)?;
let commit = get_current_commit(root)?;
let dirty = has_uncommitted_changes(root)?;
Ok(GitState {
branch,
commit,
dirty,
})
}
pub fn get_git_state_optional(root: impl AsRef<Path>) -> Result<Option<GitState>> {
if !is_git_repo(&root) {
return Ok(None);
}
match get_git_state(root) {
Ok(state) => Ok(Some(state)),
Err(e) => {
log::warn!("Failed to get git state: {}", e);
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_git_repo() {
assert!(is_git_repo("."));
assert!(!is_git_repo("/tmp"));
}
#[test]
fn test_get_current_branch() {
let branch = get_current_branch(".").unwrap();
assert!(!branch.is_empty());
log::info!("Current branch: {}", branch);
}
#[test]
fn test_get_current_commit() {
let commit = get_current_commit(".").unwrap();
assert_eq!(commit.len(), 40);
assert!(commit.chars().all(|c| c.is_ascii_hexdigit()));
log::info!("Current commit: {}", commit);
}
#[test]
fn test_has_uncommitted_changes() {
let has_changes = has_uncommitted_changes(".").unwrap();
log::info!("Has uncommitted changes: {}", has_changes);
}
#[test]
fn test_get_git_state() {
let state = get_git_state(".").unwrap();
assert!(!state.branch.is_empty());
assert_eq!(state.commit.len(), 40);
log::info!("Git state: {:?}", state);
}
}