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 push_force_with_lease(remote: &str, branches: &[String]) -> Result<()> {
52 let mut args = vec!["push", "--force-with-lease", remote];
53 args.extend(branches.iter().map(String::as_str));
54
55 status(&args).with_context(|| format!("failed to push branches to {remote}"))
56}
57
58pub fn push_set_upstream_force_with_lease(remote: &str, branches: &[String]) -> Result<()> {
61 let mut args = vec!["push", "--set-upstream", "--force-with-lease", remote];
62 args.extend(branches.iter().map(String::as_str));
63
64 status(&args).with_context(|| format!("failed to push branches to {remote}"))
65}
66
67pub fn rebase(parent: &str, branch: &str, update_refs: bool) -> Result<()> {
68 let mut args = vec!["rebase"];
69 if update_refs {
70 args.push("--update-refs");
71 }
72 args.extend([parent, branch]);
73
74 status(&args).with_context(|| format!("failed to rebase {branch} onto {parent}"))
75}
76
77pub fn rebase_onto(parent: &str, base: &str, branch: &str, update_refs: bool) -> Result<()> {
81 let mut args = vec!["rebase"];
82 if update_refs {
83 args.push("--update-refs");
84 }
85 args.extend(["--onto", parent, base, branch]);
86
87 status(&args).with_context(|| format!("failed to rebase {branch} onto {parent} from {base}"))
88}
89
90pub fn rev_parse(rev: &str) -> Result<String> {
91 let spec = format!("{rev}^{{commit}}");
92 output(&["rev-parse", "--verify", &spec]).with_context(|| format!("failed to resolve {rev}"))
93}
94
95pub fn remote_default_branch(remote: &str) -> Option<String> {
97 let reference = format!("refs/remotes/{remote}/HEAD");
98 let full = output(&["symbolic-ref", "--short", &reference]).ok()?;
99 full.strip_prefix(&format!("{remote}/")).map(str::to_owned)
100}
101
102pub fn merge_base(a: &str, b: &str) -> Result<String> {
103 output(&["merge-base", a, b])
104 .with_context(|| format!("failed to find merge base of {a} and {b}"))
105}
106
107pub fn is_ancestor(ancestor: &str, descendant: &str) -> Result<bool> {
108 let output = Command::new("git")
109 .args(["merge-base", "--is-ancestor", ancestor, descendant])
110 .stdout(Stdio::piped())
111 .stderr(Stdio::piped())
112 .output()
113 .context("failed to run git merge-base --is-ancestor")?;
114
115 match output.status.code() {
116 Some(0) => Ok(true),
117 Some(1) => Ok(false),
118 _ => Err(command_error(
119 "git merge-base --is-ancestor",
120 &output.stderr,
121 )),
122 }
123}
124
125pub fn supports_rebase_update_refs() -> Result<bool> {
126 let output = Command::new("git")
127 .args(["rebase", "-h"])
128 .stdout(Stdio::piped())
129 .stderr(Stdio::piped())
130 .output()
131 .context("failed to inspect git rebase help")?;
132
133 let help = format!(
134 "{}{}",
135 String::from_utf8_lossy(&output.stdout),
136 String::from_utf8_lossy(&output.stderr)
137 );
138 Ok(help.contains("--update-refs"))
139}
140
141pub fn rebase_continue() -> Result<()> {
142 status(&["rebase", "--continue"]).context("failed to continue rebase")
143}
144
145pub fn rebase_abort() -> Result<()> {
146 status(&["rebase", "--abort"]).context("failed to abort rebase")
147}
148
149pub fn config_get(key: &str) -> Result<Option<String>> {
150 let output = Command::new("git")
151 .args(["config", "--get", key])
152 .stdout(Stdio::piped())
153 .stderr(Stdio::piped())
154 .output()
155 .with_context(|| format!("failed to read git config {key}"))?;
156
157 match output.status.code() {
158 Some(0) => Ok(Some(
159 String::from_utf8_lossy(&output.stdout).trim().to_owned(),
160 )),
161 Some(1) => Ok(None),
162 _ => Err(command_error("git config --get", &output.stderr)),
163 }
164}
165
166pub fn config_get_bool(key: &str) -> Result<Option<bool>> {
167 let output = Command::new("git")
168 .args(["config", "--type=bool", "--get", key])
169 .stdout(Stdio::piped())
170 .stderr(Stdio::piped())
171 .output()
172 .with_context(|| format!("failed to read git config {key}"))?;
173
174 match output.status.code() {
175 Some(0) => {
176 let value = String::from_utf8_lossy(&output.stdout).trim().to_owned();
177 match value.as_str() {
178 "true" => Ok(Some(true)),
179 "false" => Ok(Some(false)),
180 _ => bail!("git config {key} is not a boolean: {value}"),
181 }
182 }
183 Some(1) => Ok(None),
184 _ => Err(command_error(
185 "git config --type=bool --get",
186 &output.stderr,
187 )),
188 }
189}
190
191pub fn config_get_regexp(pattern: &str) -> Result<Vec<(String, String)>> {
192 let output = Command::new("git")
193 .args(["config", "--get-regexp", pattern])
194 .stdout(Stdio::piped())
195 .stderr(Stdio::piped())
196 .output()
197 .with_context(|| format!("failed to read git config matching {pattern}"))?;
198
199 match output.status.code() {
200 Some(0) => Ok(String::from_utf8_lossy(&output.stdout)
201 .lines()
202 .filter_map(|line| {
203 line.split_once(' ')
204 .map(|(key, value)| (key.to_owned(), value.to_owned()))
205 })
206 .collect()),
207 Some(1) => Ok(Vec::new()),
208 _ => Err(command_error("git config --get-regexp", &output.stderr)),
209 }
210}
211
212pub fn config_set(key: &str, value: &str) -> Result<()> {
213 status(&["config", key, value]).with_context(|| format!("failed to set git config {key}"))
214}
215
216pub fn config_unset(key: &str) -> Result<()> {
217 let output = Command::new("git")
218 .args(["config", "--unset", key])
219 .stdout(Stdio::piped())
220 .stderr(Stdio::piped())
221 .output()
222 .with_context(|| format!("failed to unset git config {key}"))?;
223
224 match output.status.code() {
225 Some(0) | Some(5) => Ok(()),
226 _ => Err(command_error("git config --unset", &output.stderr)),
227 }
228}
229
230fn output(args: &[&str]) -> Result<String> {
231 let output = Command::new("git")
232 .args(args)
233 .stdout(Stdio::piped())
234 .stderr(Stdio::piped())
235 .output()
236 .context("failed to run git")?;
237
238 if output.status.success() {
239 Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
240 } else {
241 Err(command_error("git", &output.stderr))
242 }
243}
244
245fn status(args: &[&str]) -> Result<()> {
246 let status = Command::new("git")
247 .args(args)
248 .status()
249 .context("failed to run git")?;
250
251 if status.success() {
252 Ok(())
253 } else {
254 bail!("git exited with status {status}")
255 }
256}
257
258fn command_error(command: &str, stderr: &[u8]) -> anyhow::Error {
259 let stderr = String::from_utf8_lossy(stderr).trim().to_owned();
260 if stderr.is_empty() {
261 anyhow!("{command} failed")
262 } else {
263 anyhow!("{command} failed: {stderr}")
264 }
265}