git_workty/
git.rs

1use anyhow::{bail, Context, Result};
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5pub struct GitRepo {
6    pub root: PathBuf,
7    pub common_dir: PathBuf,
8}
9
10impl GitRepo {
11    pub fn discover(start_path: Option<&Path>) -> Result<Self> {
12        let working_directory = start_path
13            .map(PathBuf::from)
14            .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
15
16        let root = git_rev_parse(&working_directory, &["--show-toplevel"])?;
17        let common_dir = git_rev_parse(&working_directory, &["--git-common-dir"])?;
18
19        let root = PathBuf::from(root.trim());
20        let common_dir_str = common_dir.trim();
21
22        let common_dir = if Path::new(common_dir_str).is_absolute() {
23            PathBuf::from(common_dir_str)
24        } else {
25            root.join(common_dir_str)
26        };
27
28        Ok(Self {
29            root: root.canonicalize().unwrap_or(root),
30            common_dir: common_dir.canonicalize().unwrap_or(common_dir),
31        })
32    }
33
34    pub fn run_git(&self, args: &[&str]) -> Result<String> {
35        run_git_command(Some(&self.root), args)
36    }
37
38    #[allow(dead_code)]
39    pub fn run_git_in(&self, worktree_path: &Path, args: &[&str]) -> Result<String> {
40        run_git_command(Some(worktree_path), args)
41    }
42
43    pub fn origin_url(&self) -> Option<String> {
44        self.run_git(&["remote", "get-url", "origin"])
45            .ok()
46            .map(|s| s.trim().to_string())
47    }
48}
49
50fn git_rev_parse(working_directory: &Path, args: &[&str]) -> Result<String> {
51    let mut cmd_args = vec!["rev-parse"];
52    cmd_args.extend(args);
53    run_git_command(Some(working_directory), &cmd_args)
54}
55
56pub fn run_git_command(working_directory: Option<&Path>, args: &[&str]) -> Result<String> {
57    let mut cmd = Command::new("git");
58    if let Some(directory) = working_directory {
59        cmd.current_dir(directory);
60    }
61    cmd.args(args);
62
63    let output = cmd.output().context("Failed to execute git command")?;
64
65    if !output.status.success() {
66        let stderr = String::from_utf8_lossy(&output.stderr);
67        bail!(
68            "git {} failed: {}",
69            args.first().unwrap_or(&""),
70            stderr.trim()
71        );
72    }
73
74    Ok(String::from_utf8_lossy(&output.stdout).to_string())
75}
76
77pub fn is_git_installed() -> bool {
78    Command::new("git")
79        .arg("--version")
80        .output()
81        .map(|o| o.status.success())
82        .unwrap_or(false)
83}
84
85pub fn is_in_git_repo(path: &Path) -> bool {
86    Command::new("git")
87        .current_dir(path)
88        .args(["rev-parse", "--git-dir"])
89        .output()
90        .map(|o| o.status.success())
91        .unwrap_or(false)
92}
93
94pub fn branch_exists(repo: &GitRepo, branch: &str) -> bool {
95    repo.run_git(&["rev-parse", "--verify", &format!("refs/heads/{}", branch)])
96        .is_ok()
97}
98
99pub fn is_ancestor(repo: &GitRepo, ancestor: &str, descendant: &str) -> Result<bool> {
100    let result = Command::new("git")
101        .current_dir(&repo.root)
102        .args(["merge-base", "--is-ancestor", ancestor, descendant])
103        .output()
104        .context("Failed to check ancestry")?;
105    Ok(result.status.success())
106}