Skip to main content

git_side/
git.rs

1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use crate::error::{Error, Result};
5
6/// Run a git command and return stdout on success.
7///
8/// # Errors
9///
10/// Returns an error if the git command fails to execute or exits with non-zero status.
11pub fn run(args: &[&str]) -> Result<String> {
12    let output = Command::new("git")
13        .args(args)
14        .output()
15        .map_err(|e| Error::GitCommandFailed(format!("failed to execute git: {e}")))?;
16
17    if output.status.success() {
18        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
19    } else {
20        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
21        Err(Error::GitCommandFailed(stderr))
22    }
23}
24
25/// Run a git command with a specific work-tree and git-dir.
26///
27/// # Errors
28///
29/// Returns an error if the git command fails to execute or exits with non-zero status.
30pub fn run_with_paths(git_dir: &Path, work_tree: &Path, args: &[&str]) -> Result<String> {
31    let output = Command::new("git")
32        .current_dir(work_tree)
33        .env("GIT_DIR", git_dir)
34        .env("GIT_WORK_TREE", work_tree)
35        // Clear any inherited git environment from hooks
36        .env_remove("GIT_INDEX_FILE")
37        .env_remove("GIT_OBJECT_DIRECTORY")
38        .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
39        .args(args)
40        .output()
41        .map_err(|e| Error::GitCommandFailed(format!("failed to execute git: {e}")))?;
42
43    if output.status.success() {
44        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
45    } else {
46        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
47        Err(Error::GitCommandFailed(stderr))
48    }
49}
50
51/// Check if we're inside a git repository.
52#[must_use]
53pub fn is_in_repo() -> bool {
54    run(&["rev-parse", "--is-inside-work-tree"])
55        .is_ok_and(|s| s == "true")
56}
57
58/// Get the root directory of the current git repository.
59///
60/// # Errors
61///
62/// Returns an error if not inside a git repository.
63pub fn repo_root() -> Result<PathBuf> {
64    if !is_in_repo() {
65        return Err(Error::NotInGitRepo);
66    }
67    let root = run(&["rev-parse", "--show-toplevel"])?;
68    Ok(PathBuf::from(root))
69}
70
71/// Get the .git directory of the current repository.
72///
73/// # Errors
74///
75/// Returns an error if not inside a git repository.
76pub fn git_dir() -> Result<PathBuf> {
77    if !is_in_repo() {
78        return Err(Error::NotInGitRepo);
79    }
80    let dir = run(&["rev-parse", "--git-dir"])?;
81    Ok(PathBuf::from(dir))
82}
83
84/// Get the initial commit SHA of the repository (project identifier).
85///
86/// # Errors
87///
88/// Returns an error if not inside a git repository or if the repository has no commits.
89pub fn initial_commit_sha() -> Result<String> {
90    if !is_in_repo() {
91        return Err(Error::NotInGitRepo);
92    }
93    let sha = run(&["rev-list", "--max-parents=0", "HEAD"])
94        .map_err(|_| Error::NoCommits)?;
95
96    // Take first line if multiple roots exist (rare but possible)
97    Ok(sha.lines().next().unwrap_or(&sha).to_string())
98}
99
100/// Get the last commit message from the main repository.
101///
102/// # Errors
103///
104/// Returns an error if the git log command fails.
105pub fn last_commit_message() -> Result<String> {
106    run(&["log", "-1", "--format=%B"])
107}