1use std::process::{Command, Stdio};
2
3use anyhow::{Context, Result, anyhow, bail};
4
5pub fn current_branch() -> Result<String> {
6 output(&["symbolic-ref", "--quiet", "--short", "HEAD"])
7 .context("failed to determine current branch")
8}
9
10pub fn local_branches() -> Result<Vec<String>> {
11 let output = output(&["for-each-ref", "--format=%(refname:short)", "refs/heads"])?;
12 Ok(output.lines().map(str::to_owned).collect())
13}
14
15pub fn git_path(path: &str) -> Result<String> {
16 output(&["rev-parse", "--git-path", path])
17}
18
19pub fn remote_url(remote: &str) -> Result<Option<String>> {
20 let output = Command::new("git")
21 .args(["remote", "get-url", remote])
22 .stdout(Stdio::piped())
23 .stderr(Stdio::piped())
24 .output()
25 .with_context(|| format!("failed to read git remote {remote}"))?;
26
27 match output.status.code() {
28 Some(0) => Ok(Some(
29 String::from_utf8_lossy(&output.stdout).trim().to_owned(),
30 )),
31 Some(2) => Ok(None),
32 _ => Err(command_error("git remote get-url", &output.stderr)),
33 }
34}
35
36pub fn checkout(branch: &str) -> Result<()> {
37 status(&["switch", branch]).with_context(|| format!("failed to check out {branch}"))
38}
39
40pub fn create_branch(branch: &str) -> Result<()> {
41 status(&["switch", "-c", branch]).with_context(|| format!("failed to create branch {branch}"))
42}
43
44pub fn delete_branch(branch: &str) -> Result<()> {
48 status(&["branch", "-D", branch]).with_context(|| format!("failed to delete branch {branch}"))
49}
50
51pub fn fetch_branch(remote: &str, branch: &str) -> Result<()> {
53 let refspec = format!("{branch}:{branch}");
54 status(&["fetch", remote, &refspec])
55 .with_context(|| format!("failed to fetch {branch} from {remote}"))
56}
57
58pub fn pull_ff_only() -> Result<()> {
59 status(&["pull", "--ff-only"]).context("failed to fast-forward from the remote")
60}
61
62pub fn push_force_with_lease(remote: &str, branches: &[String]) -> Result<()> {
63 let mut args = vec!["push", "--force-with-lease", remote];
64 args.extend(branches.iter().map(String::as_str));
65
66 status(&args).with_context(|| format!("failed to push branches to {remote}"))
67}
68
69pub fn push_set_upstream_force_with_lease(remote: &str, branches: &[String]) -> Result<()> {
72 let mut args = vec!["push", "--set-upstream", "--force-with-lease", remote];
73 args.extend(branches.iter().map(String::as_str));
74
75 status(&args).with_context(|| format!("failed to push branches to {remote}"))
76}
77
78pub fn rebase(parent: &str, branch: &str, update_refs: bool) -> Result<()> {
79 let mut args = vec!["rebase"];
80 if update_refs {
81 args.push("--update-refs");
82 }
83 args.extend([parent, branch]);
84
85 status(&args).with_context(|| format!("failed to rebase {branch} onto {parent}"))
86}
87
88pub fn rebase_onto(parent: &str, base: &str, branch: &str, update_refs: bool) -> Result<()> {
92 let mut args = vec!["rebase"];
93 if update_refs {
94 args.push("--update-refs");
95 }
96 args.extend(["--onto", parent, base, branch]);
97
98 status(&args).with_context(|| format!("failed to rebase {branch} onto {parent} from {base}"))
99}
100
101pub fn rev_parse(rev: &str) -> Result<String> {
102 let spec = format!("{rev}^{{commit}}");
103 output(&["rev-parse", "--verify", &spec]).with_context(|| format!("failed to resolve {rev}"))
104}
105
106pub fn remote_default_branch(remote: &str) -> Option<String> {
108 let reference = format!("refs/remotes/{remote}/HEAD");
109 let full = output(&["symbolic-ref", "--short", &reference]).ok()?;
110 full.strip_prefix(&format!("{remote}/")).map(str::to_owned)
111}
112
113pub fn merge_base(a: &str, b: &str) -> Result<String> {
114 output(&["merge-base", a, b])
115 .with_context(|| format!("failed to find merge base of {a} and {b}"))
116}
117
118pub fn is_ancestor(ancestor: &str, descendant: &str) -> Result<bool> {
119 let output = Command::new("git")
120 .args(["merge-base", "--is-ancestor", ancestor, descendant])
121 .stdout(Stdio::piped())
122 .stderr(Stdio::piped())
123 .output()
124 .context("failed to run git merge-base --is-ancestor")?;
125
126 match output.status.code() {
127 Some(0) => Ok(true),
128 Some(1) => Ok(false),
129 _ => Err(command_error(
130 "git merge-base --is-ancestor",
131 &output.stderr,
132 )),
133 }
134}
135
136pub fn supports_rebase_update_refs() -> Result<bool> {
137 let output = Command::new("git")
138 .args(["rebase", "-h"])
139 .stdout(Stdio::piped())
140 .stderr(Stdio::piped())
141 .output()
142 .context("failed to inspect git rebase help")?;
143
144 let help = format!(
145 "{}{}",
146 String::from_utf8_lossy(&output.stdout),
147 String::from_utf8_lossy(&output.stderr)
148 );
149 Ok(help.contains("--update-refs"))
150}
151
152pub fn rebase_continue() -> Result<()> {
153 status(&["rebase", "--continue"]).context("failed to continue rebase")
154}
155
156pub fn rebase_abort() -> Result<()> {
157 status(&["rebase", "--abort"]).context("failed to abort rebase")
158}
159
160pub fn config_get(key: &str) -> Result<Option<String>> {
161 let output = Command::new("git")
162 .args(["config", "--get", key])
163 .stdout(Stdio::piped())
164 .stderr(Stdio::piped())
165 .output()
166 .with_context(|| format!("failed to read git config {key}"))?;
167
168 match output.status.code() {
169 Some(0) => Ok(Some(
170 String::from_utf8_lossy(&output.stdout).trim().to_owned(),
171 )),
172 Some(1) => Ok(None),
173 _ => Err(command_error("git config --get", &output.stderr)),
174 }
175}
176
177pub fn config_get_bool(key: &str) -> Result<Option<bool>> {
178 let output = Command::new("git")
179 .args(["config", "--type=bool", "--get", key])
180 .stdout(Stdio::piped())
181 .stderr(Stdio::piped())
182 .output()
183 .with_context(|| format!("failed to read git config {key}"))?;
184
185 match output.status.code() {
186 Some(0) => {
187 let value = String::from_utf8_lossy(&output.stdout).trim().to_owned();
188 match value.as_str() {
189 "true" => Ok(Some(true)),
190 "false" => Ok(Some(false)),
191 _ => bail!("git config {key} is not a boolean: {value}"),
192 }
193 }
194 Some(1) => Ok(None),
195 _ => Err(command_error(
196 "git config --type=bool --get",
197 &output.stderr,
198 )),
199 }
200}
201
202pub fn config_get_regexp(pattern: &str) -> Result<Vec<(String, String)>> {
203 let output = Command::new("git")
204 .args(["config", "--get-regexp", pattern])
205 .stdout(Stdio::piped())
206 .stderr(Stdio::piped())
207 .output()
208 .with_context(|| format!("failed to read git config matching {pattern}"))?;
209
210 match output.status.code() {
211 Some(0) => Ok(String::from_utf8_lossy(&output.stdout)
212 .lines()
213 .filter_map(|line| {
214 line.split_once(' ')
215 .map(|(key, value)| (key.to_owned(), value.to_owned()))
216 })
217 .collect()),
218 Some(1) => Ok(Vec::new()),
219 _ => Err(command_error("git config --get-regexp", &output.stderr)),
220 }
221}
222
223pub fn config_set(key: &str, value: &str) -> Result<()> {
224 status(&["config", key, value]).with_context(|| format!("failed to set git config {key}"))
225}
226
227pub fn config_unset(key: &str) -> Result<()> {
228 let output = Command::new("git")
229 .args(["config", "--unset", key])
230 .stdout(Stdio::piped())
231 .stderr(Stdio::piped())
232 .output()
233 .with_context(|| format!("failed to unset git config {key}"))?;
234
235 match output.status.code() {
236 Some(0) | Some(5) => Ok(()),
237 _ => Err(command_error("git config --unset", &output.stderr)),
238 }
239}
240
241fn output(args: &[&str]) -> Result<String> {
242 let output = Command::new("git")
243 .args(args)
244 .stdout(Stdio::piped())
245 .stderr(Stdio::piped())
246 .output()
247 .context("failed to run git")?;
248
249 if output.status.success() {
250 Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
251 } else {
252 Err(command_error("git", &output.stderr))
253 }
254}
255
256fn status(args: &[&str]) -> Result<()> {
257 let status = Command::new("git")
258 .args(args)
259 .status()
260 .context("failed to run git")?;
261
262 if status.success() {
263 Ok(())
264 } else {
265 bail!("git exited with status {status}")
266 }
267}
268
269fn command_error(command: &str, stderr: &[u8]) -> anyhow::Error {
270 let stderr = String::from_utf8_lossy(stderr).trim().to_owned();
271 if stderr.is_empty() {
272 anyhow!("{command} failed")
273 } else {
274 anyhow!("{command} failed: {stderr}")
275 }
276}