Skip to main content

git_stk/
git.rs

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
58/// Rebase only the commits after `base`, replaying `base..branch` onto
59/// `parent`. Used when the recorded fork point is known so commits that
60/// landed upstream by squash or rebase are not replayed.
61pub fn rebase_onto(parent: &str, base: &str, branch: &str, update_refs: bool) -> Result<()> {
62    let mut args = vec!["rebase"];
63    if update_refs {
64        args.push("--update-refs");
65    }
66    args.extend(["--onto", parent, base, branch]);
67
68    status(&args).with_context(|| format!("failed to rebase {branch} onto {parent} from {base}"))
69}
70
71pub fn merge_base(a: &str, b: &str) -> Result<String> {
72    output(&["merge-base", a, b])
73        .with_context(|| format!("failed to find merge base of {a} and {b}"))
74}
75
76pub fn is_ancestor(ancestor: &str, descendant: &str) -> Result<bool> {
77    let output = Command::new("git")
78        .args(["merge-base", "--is-ancestor", ancestor, descendant])
79        .stdout(Stdio::piped())
80        .stderr(Stdio::piped())
81        .output()
82        .context("failed to run git merge-base --is-ancestor")?;
83
84    match output.status.code() {
85        Some(0) => Ok(true),
86        Some(1) => Ok(false),
87        _ => Err(command_error(
88            "git merge-base --is-ancestor",
89            &output.stderr,
90        )),
91    }
92}
93
94pub fn supports_rebase_update_refs() -> Result<bool> {
95    let output = Command::new("git")
96        .args(["rebase", "-h"])
97        .stdout(Stdio::piped())
98        .stderr(Stdio::piped())
99        .output()
100        .context("failed to inspect git rebase help")?;
101
102    let help = format!(
103        "{}{}",
104        String::from_utf8_lossy(&output.stdout),
105        String::from_utf8_lossy(&output.stderr)
106    );
107    Ok(help.contains("--update-refs"))
108}
109
110pub fn rebase_continue() -> Result<()> {
111    status(&["rebase", "--continue"]).context("failed to continue rebase")
112}
113
114pub fn rebase_abort() -> Result<()> {
115    status(&["rebase", "--abort"]).context("failed to abort rebase")
116}
117
118pub fn config_get(key: &str) -> Result<Option<String>> {
119    let output = Command::new("git")
120        .args(["config", "--get", key])
121        .stdout(Stdio::piped())
122        .stderr(Stdio::piped())
123        .output()
124        .with_context(|| format!("failed to read git config {key}"))?;
125
126    match output.status.code() {
127        Some(0) => Ok(Some(
128            String::from_utf8_lossy(&output.stdout).trim().to_owned(),
129        )),
130        Some(1) => Ok(None),
131        _ => Err(command_error("git config --get", &output.stderr)),
132    }
133}
134
135pub fn config_get_bool(key: &str) -> Result<Option<bool>> {
136    let output = Command::new("git")
137        .args(["config", "--type=bool", "--get", key])
138        .stdout(Stdio::piped())
139        .stderr(Stdio::piped())
140        .output()
141        .with_context(|| format!("failed to read git config {key}"))?;
142
143    match output.status.code() {
144        Some(0) => {
145            let value = String::from_utf8_lossy(&output.stdout).trim().to_owned();
146            match value.as_str() {
147                "true" => Ok(Some(true)),
148                "false" => Ok(Some(false)),
149                _ => bail!("git config {key} is not a boolean: {value}"),
150            }
151        }
152        Some(1) => Ok(None),
153        _ => Err(command_error(
154            "git config --type=bool --get",
155            &output.stderr,
156        )),
157    }
158}
159
160pub fn config_set(key: &str, value: &str) -> Result<()> {
161    status(&["config", key, value]).with_context(|| format!("failed to set git config {key}"))
162}
163
164pub fn config_unset(key: &str) -> Result<()> {
165    let output = Command::new("git")
166        .args(["config", "--unset", key])
167        .stdout(Stdio::piped())
168        .stderr(Stdio::piped())
169        .output()
170        .with_context(|| format!("failed to unset git config {key}"))?;
171
172    match output.status.code() {
173        Some(0) | Some(5) => Ok(()),
174        _ => Err(command_error("git config --unset", &output.stderr)),
175    }
176}
177
178fn output(args: &[&str]) -> Result<String> {
179    let output = Command::new("git")
180        .args(args)
181        .stdout(Stdio::piped())
182        .stderr(Stdio::piped())
183        .output()
184        .context("failed to run git")?;
185
186    if output.status.success() {
187        Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
188    } else {
189        Err(command_error("git", &output.stderr))
190    }
191}
192
193fn status(args: &[&str]) -> Result<()> {
194    let status = Command::new("git")
195        .args(args)
196        .status()
197        .context("failed to run git")?;
198
199    if status.success() {
200        Ok(())
201    } else {
202        bail!("git exited with status {status}")
203    }
204}
205
206fn command_error(command: &str, stderr: &[u8]) -> anyhow::Error {
207    let stderr = String::from_utf8_lossy(stderr).trim().to_owned();
208    if stderr.is_empty() {
209        anyhow!("{command} failed")
210    } else {
211        anyhow!("{command} failed: {stderr}")
212    }
213}