1use crate::error::{BbError, Result};
2use std::path::Path;
3use std::process::Command;
4
5fn git_in(dir: Option<&Path>, args: &[&str]) -> Result<String> {
9 let mut cmd = Command::new("git");
10 cmd.args(args);
11 if let Some(dir) = dir {
12 cmd.current_dir(dir);
13 }
14 let output = cmd
15 .output()
16 .map_err(|e| BbError::Git(format!("cannot run git: {e}")))?;
17
18 if !output.status.success() {
19 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
20 return Err(BbError::Git(if stderr.is_empty() {
21 format!("git {} failed", args.join(" "))
22 } else {
23 stderr
24 }));
25 }
26
27 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
28}
29
30fn git(args: &[&str]) -> Result<String> {
31 git_in(None, args)
32}
33
34pub fn current_branch() -> Result<String> {
35 let branch = git(&["symbolic-ref", "--short", "HEAD"])?;
36 if branch.is_empty() {
37 return Err(BbError::Git(
38 "detached HEAD — cannot infer source branch".into(),
39 ));
40 }
41 Ok(branch)
42}
43
44pub fn remote_url(remote: &str) -> Result<String> {
45 let url = git(&["config", "--get", &format!("remote.{remote}.url")])?;
46 if url.is_empty() {
47 return Err(BbError::Git(format!("remote `{remote}` has no url")));
48 }
49 Ok(url)
50}
51
52pub fn remotes() -> Result<Vec<String>> {
54 remotes_in(None)
55}
56
57fn remotes_in(dir: Option<&Path>) -> Result<Vec<String>> {
58 Ok(git_in(dir, &["remote"])?
59 .lines()
60 .map(str::trim)
61 .filter(|line| !line.is_empty())
62 .map(str::to_string)
63 .collect())
64}
65
66pub fn in_repo() -> bool {
67 git(&["rev-parse", "--git-dir"]).is_ok()
68}
69
70#[cfg(test)]
71#[allow(clippy::unwrap_used)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn remotes_is_empty_for_a_repo_with_no_remotes() {
77 let tmp = tempfile::tempdir().unwrap();
78 let dir = tmp.path();
79
80 git_in(Some(dir), &["init"]).unwrap();
81
82 let names = remotes_in(Some(dir)).unwrap();
83 assert!(names.is_empty(), "expected no remotes, got {names:?}");
84 }
85
86 #[test]
87 fn remotes_lists_configured_remote_names() {
88 let tmp = tempfile::tempdir().unwrap();
89 let dir = tmp.path();
90
91 git_in(Some(dir), &["init"]).unwrap();
92 git_in(
93 Some(dir),
94 &["remote", "add", "origin", "https://example.com/origin.git"],
95 )
96 .unwrap();
97 git_in(
98 Some(dir),
99 &[
100 "remote",
101 "add",
102 "bitbucket",
103 "https://example.com/bitbucket.git",
104 ],
105 )
106 .unwrap();
107
108 let names = remotes_in(Some(dir)).unwrap();
109 assert_eq!(names, vec!["bitbucket".to_string(), "origin".to_string()]);
110 }
111
112 #[test]
113 fn git_failure_is_reported_not_panicked() {
114 let err = git(&[
119 "rev-parse",
120 "--verify",
121 "refs/heads/definitely-not-a-branch-xyz",
122 ])
123 .unwrap_err();
124 assert!(matches!(err, BbError::Git(_)));
125 }
126}