Skip to main content

aivcs_core/
git.rs

1//! Git integration utilities for capturing repository state.
2
3use std::path::Path;
4use std::process::Command;
5
6use crate::domain::error::{AivcsError, Result};
7
8/// Capture the HEAD commit SHA from a git repository.
9///
10/// Runs `git rev-parse HEAD` in the given directory. Returns an error if the
11/// directory is not inside a git repository or if git is not available.
12pub fn capture_head_sha(repo_dir: &Path) -> Result<String> {
13    let output = Command::new("git")
14        .args(["rev-parse", "HEAD"])
15        .current_dir(repo_dir)
16        .output()
17        .map_err(|e| AivcsError::GitError(format!("failed to run git: {e}")))?;
18
19    if !output.status.success() {
20        let stderr = String::from_utf8_lossy(&output.stderr);
21        return Err(AivcsError::GitError(format!(
22            "git rev-parse HEAD failed: {stderr}"
23        )));
24    }
25
26    let sha = String::from_utf8_lossy(&output.stdout).trim().to_string();
27    if sha.is_empty() {
28        return Err(AivcsError::GitError(
29            "git rev-parse HEAD returned empty output".to_string(),
30        ));
31    }
32
33    Ok(sha)
34}
35
36/// Check whether a directory is inside a git work tree.
37pub fn is_git_repo(dir: &Path) -> bool {
38    Command::new("git")
39        .args(["rev-parse", "--is-inside-work-tree"])
40        .current_dir(dir)
41        .output()
42        .map(|o| o.status.success())
43        .unwrap_or(false)
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use std::path::Path;
50    use std::process::Command as StdCommand;
51
52    fn run_git(repo_dir: &Path, args: &[&str]) {
53        let output = StdCommand::new("git")
54            .args(args)
55            .current_dir(repo_dir)
56            .output()
57            .unwrap();
58        assert!(
59            output.status.success(),
60            "git {:?} failed: {}",
61            args,
62            String::from_utf8_lossy(&output.stderr)
63        );
64    }
65
66    fn make_git_repo() -> tempfile::TempDir {
67        let dir = tempfile::tempdir().unwrap();
68        run_git(dir.path(), &["init"]);
69        run_git(dir.path(), &["config", "user.name", "test-user"]);
70        run_git(dir.path(), &["config", "user.email", "test@example.com"]);
71        run_git(dir.path(), &["commit", "--allow-empty", "-m", "initial"]);
72        dir
73    }
74
75    #[test]
76    fn capture_head_sha_returns_40_hex_chars() {
77        let repo = make_git_repo();
78        let sha = capture_head_sha(repo.path()).unwrap();
79        assert_eq!(sha.len(), 40, "SHA should be 40 hex chars, got: {sha}");
80        assert!(sha.chars().all(|c| c.is_ascii_hexdigit()));
81    }
82
83    #[test]
84    fn capture_head_sha_fails_outside_repo() {
85        let dir = tempfile::tempdir().unwrap();
86        let result = capture_head_sha(dir.path());
87        assert!(result.is_err());
88    }
89
90    #[test]
91    fn is_git_repo_true_for_repo() {
92        let repo = make_git_repo();
93        assert!(is_git_repo(repo.path()));
94    }
95
96    #[test]
97    fn is_git_repo_false_for_non_repo() {
98        let dir = tempfile::tempdir().unwrap();
99        assert!(!is_git_repo(dir.path()));
100    }
101}