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<()> {
45 status(&["branch", "-d", branch]).with_context(|| format!("failed to delete branch {branch}"))
46}
47
48pub fn rebase(parent: &str, branch: &str, update_refs: bool) -> Result<()> {
49 let mut args = vec!["rebase"];
50 if update_refs {
51 args.push("--update-refs");
52 }
53 args.extend([parent, branch]);
54
55 status(&args).with_context(|| format!("failed to rebase {branch} onto {parent}"))
56}
57
58pub fn supports_rebase_update_refs() -> Result<bool> {
59 let output = Command::new("git")
60 .args(["rebase", "-h"])
61 .stdout(Stdio::piped())
62 .stderr(Stdio::piped())
63 .output()
64 .context("failed to inspect git rebase help")?;
65
66 let help = format!(
67 "{}{}",
68 String::from_utf8_lossy(&output.stdout),
69 String::from_utf8_lossy(&output.stderr)
70 );
71 Ok(help.contains("--update-refs"))
72}
73
74pub fn rebase_continue() -> Result<()> {
75 status(&["rebase", "--continue"]).context("failed to continue rebase")
76}
77
78pub fn rebase_abort() -> Result<()> {
79 status(&["rebase", "--abort"]).context("failed to abort rebase")
80}
81
82pub fn config_get(key: &str) -> Result<Option<String>> {
83 let output = Command::new("git")
84 .args(["config", "--get", key])
85 .stdout(Stdio::piped())
86 .stderr(Stdio::piped())
87 .output()
88 .with_context(|| format!("failed to read git config {key}"))?;
89
90 match output.status.code() {
91 Some(0) => Ok(Some(
92 String::from_utf8_lossy(&output.stdout).trim().to_owned(),
93 )),
94 Some(1) => Ok(None),
95 _ => Err(command_error("git config --get", &output.stderr)),
96 }
97}
98
99pub fn config_get_bool(key: &str) -> Result<Option<bool>> {
100 let output = Command::new("git")
101 .args(["config", "--type=bool", "--get", key])
102 .stdout(Stdio::piped())
103 .stderr(Stdio::piped())
104 .output()
105 .with_context(|| format!("failed to read git config {key}"))?;
106
107 match output.status.code() {
108 Some(0) => {
109 let value = String::from_utf8_lossy(&output.stdout).trim().to_owned();
110 match value.as_str() {
111 "true" => Ok(Some(true)),
112 "false" => Ok(Some(false)),
113 _ => bail!("git config {key} is not a boolean: {value}"),
114 }
115 }
116 Some(1) => Ok(None),
117 _ => Err(command_error(
118 "git config --type=bool --get",
119 &output.stderr,
120 )),
121 }
122}
123
124pub fn config_set(key: &str, value: &str) -> Result<()> {
125 status(&["config", key, value]).with_context(|| format!("failed to set git config {key}"))
126}
127
128pub fn config_unset(key: &str) -> Result<()> {
129 let output = Command::new("git")
130 .args(["config", "--unset", key])
131 .stdout(Stdio::piped())
132 .stderr(Stdio::piped())
133 .output()
134 .with_context(|| format!("failed to unset git config {key}"))?;
135
136 match output.status.code() {
137 Some(0) | Some(5) => Ok(()),
138 _ => Err(command_error("git config --unset", &output.stderr)),
139 }
140}
141
142fn output(args: &[&str]) -> Result<String> {
143 let output = Command::new("git")
144 .args(args)
145 .stdout(Stdio::piped())
146 .stderr(Stdio::piped())
147 .output()
148 .context("failed to run git")?;
149
150 if output.status.success() {
151 Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
152 } else {
153 Err(command_error("git", &output.stderr))
154 }
155}
156
157fn status(args: &[&str]) -> Result<()> {
158 let status = Command::new("git")
159 .args(args)
160 .status()
161 .context("failed to run git")?;
162
163 if status.success() {
164 Ok(())
165 } else {
166 bail!("git exited with status {status}")
167 }
168}
169
170fn command_error(command: &str, stderr: &[u8]) -> anyhow::Error {
171 let stderr = String::from_utf8_lossy(stderr).trim().to_owned();
172 if stderr.is_empty() {
173 anyhow!("{command} failed")
174 } else {
175 anyhow!("{command} failed: {stderr}")
176 }
177}