Skip to main content

grits_core/
git.rs

1use anyhow::{bail, Context, Result};
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5pub trait GitOps {
6    fn init(&self) -> Result<()>;
7    fn add(&self, path: &Path) -> Result<()>;
8    fn commit(&self, message: &str) -> Result<()>;
9    fn pull_rebase(&self) -> Result<()>;
10    fn push(&self) -> Result<()>;
11    fn status(&self) -> Result<String>;
12    fn show(&self, revision: &str) -> Result<String>;
13    fn rebase_continue(&self) -> Result<()>;
14    fn has_remote(&self) -> Result<bool>;
15    fn config(&self, key: &str, value: &str) -> Result<()>;
16}
17
18pub struct StdGit {
19    root: PathBuf,
20}
21
22impl StdGit {
23    pub fn new<P: AsRef<Path>>(root: P) -> Self {
24        Self {
25            root: root.as_ref().to_path_buf(),
26        }
27    }
28
29    fn command(&self, args: &[&str]) -> Command {
30        let mut cmd = Command::new("git");
31        cmd.current_dir(&self.root);
32        cmd.args(args);
33        cmd
34    }
35}
36
37impl GitOps for StdGit {
38    fn init(&self) -> Result<()> {
39        let output = self
40            .command(&["init"])
41            .output()
42            .context("Failed to run git init")?;
43
44        if !output.status.success() {
45            bail!(
46                "git init failed: {}",
47                String::from_utf8_lossy(&output.stderr)
48            );
49        }
50        Ok(())
51    }
52
53    fn add(&self, path: &Path) -> Result<()> {
54        let path_s = path.to_string_lossy();
55        let output = self
56            .command(&["add", &path_s])
57            .output()
58            .context("Failed to run git add")?;
59
60        if !output.status.success() {
61            bail!(
62                "git add failed: {}",
63                String::from_utf8_lossy(&output.stderr)
64            );
65        }
66        Ok(())
67    }
68
69    fn commit(&self, message: &str) -> Result<()> {
70        let output = self
71            .command(&["commit", "-m", message])
72            .output()
73            .context("Failed to run git commit")?;
74
75        if !output.status.success() {
76            // Check if "nothing to commit" or "clean"
77            let stdout = String::from_utf8_lossy(&output.stdout);
78            if stdout.contains("nothing to commit") || stdout.contains("working tree clean") {
79                return Ok(());
80            }
81            bail!(
82                "git commit failed: {}\n{}",
83                stdout,
84                String::from_utf8_lossy(&output.stderr)
85            );
86        }
87        Ok(())
88    }
89
90    fn pull_rebase(&self) -> Result<()> {
91        // pull --rebase
92        let output = self
93            .command(&["pull", "--rebase"])
94            .output()
95            .context("Failed to run git pull --rebase")?;
96
97        if !output.status.success() {
98            // Return error, caller checks for conflict
99            bail!(
100                "git pull --rebase failed: {}",
101                String::from_utf8_lossy(&output.stderr)
102            );
103        }
104        Ok(())
105    }
106
107    fn push(&self) -> Result<()> {
108        let output = self
109            .command(&["push"])
110            .output()
111            .context("Failed to run git push")?;
112
113        if !output.status.success() {
114            bail!(
115                "git push failed: {}",
116                String::from_utf8_lossy(&output.stderr)
117            );
118        }
119        Ok(())
120    }
121
122    fn status(&self) -> Result<String> {
123        let output = self
124            .command(&["status", "--porcelain"])
125            .output()
126            .context("Failed to run git status")?;
127
128        if !output.status.success() {
129            bail!(
130                "git status failed: {}",
131                String::from_utf8_lossy(&output.stderr)
132            );
133        }
134        Ok(String::from_utf8(output.stdout)?)
135    }
136
137    fn show(&self, revision: &str) -> Result<String> {
138        let output = self
139            .command(&["show", revision])
140            .output()
141            .context("Failed to run git show")?;
142
143        if !output.status.success() {
144            bail!(
145                "git show failed: {}",
146                String::from_utf8_lossy(&output.stderr)
147            );
148        }
149        Ok(String::from_utf8(output.stdout)?)
150    }
151
152    fn rebase_continue(&self) -> Result<()> {
153        let output = self
154            .command(&["rebase", "--continue"])
155            .env("GIT_EDITOR", "true") // avoid editor opening
156            .output()
157            .context("Failed to run git rebase --continue")?;
158
159        if !output.status.success() {
160            bail!(
161                "git rebase --continue failed: {}",
162                String::from_utf8_lossy(&output.stderr)
163            );
164        }
165        Ok(())
166    }
167
168    fn has_remote(&self) -> Result<bool> {
169        let output = self
170            .command(&["remote"])
171            .output()
172            .context("Failed to run git remote")?;
173
174        if !output.status.success() {
175            return Ok(false);
176        }
177        let stdout = String::from_utf8_lossy(&output.stdout);
178        Ok(!stdout.trim().is_empty())
179    }
180
181    fn config(&self, key: &str, value: &str) -> Result<()> {
182        let output = self
183            .command(&["config", "--local", key, value])
184            .output()
185            .context("Failed to run git config")?;
186
187        if !output.status.success() {
188            bail!(
189                "git config failed: {}",
190                String::from_utf8_lossy(&output.stderr)
191            );
192        }
193        Ok(())
194    }
195}