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 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 let output = self
93 .command(&["pull", "--rebase"])
94 .output()
95 .context("Failed to run git pull --rebase")?;
96
97 if !output.status.success() {
98 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") .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}