1use std::{ffi::OsStr, fmt, process::ExitStatus};
2
3use tokio::process::Command;
4
5#[derive(Debug)]
7pub struct Error(String);
8
9pub type Result<T> = std::result::Result<T, Error>;
10
11pub const HEAD: &str = "HEAD";
12
13impl fmt::Display for Error {
14 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
15 self.0.strip_suffix('\n').unwrap_or(&self.0).fmt(f)
17 }
18}
19
20fn format_git_command<S, I>(args: I) -> String
21where
22 I: IntoIterator<Item = S>,
23 S: AsRef<OsStr>,
24{
25 let args: Vec<_> = args.into_iter().collect();
26 let strs: Vec<_> = args.iter().map(|s| s.as_ref().to_string_lossy()).collect();
27 format!("git {}", strs.join(" "))
28}
29
30async fn run_git<S, I>(args: I) -> (ExitStatus, String, String)
35where
36 I: IntoIterator<Item = S>,
37 S: AsRef<OsStr>,
38{
39 let output = Command::new("git")
40 .args(args)
41 .output()
42 .await
43 .expect("git should be executable");
44 (
45 output.status,
46 String::from_utf8_lossy(&output.stdout).to_string(),
47 String::from_utf8_lossy(&output.stderr).to_string(),
48 )
49}
50
51pub async fn git_loud<S, I>(args: I) -> Result<String>
58where
59 I: IntoIterator<Item = S>,
60 S: AsRef<OsStr>,
61{
62 let args: Vec<_> = args.into_iter().collect();
63 println!("> {}", format_git_command(&args));
64 let (status, stdout, stderr) = run_git(args).await;
65 if !status.success() {
66 return Err(Error(stderr));
67 }
68 stderr.lines().for_each(|s| println!("! {s}"));
69 stdout.lines().for_each(|s| println!("< {s}"));
70 Ok(stderr + &stdout)
71}
72
73pub async fn git<S, I>(args: I) -> Result<String>
79where
80 I: IntoIterator<Item = S>,
81 S: AsRef<OsStr>,
82{
83 let (status, stdout, stderr) = run_git(args).await;
84 if !status.success() {
85 return Err(Error(stderr));
86 }
87 Ok(stderr + &stdout)
88}
89
90pub async fn merge_base(ref1: impl AsRef<OsStr>, ref2: impl AsRef<OsStr>) -> Result<String> {
96 let mut base = git(["merge-base".as_ref(), ref1.as_ref(), ref2.as_ref()]).await?;
97 base.truncate(base.trim_end().len());
98 Ok(base)
99}
100
101pub async fn merge_base_head(base: impl AsRef<OsStr>) -> Result<String> {
107 merge_base(base, HEAD).await
108}
109
110pub async fn upstream(branch: impl AsRef<OsStr>) -> Option<String> {
126 git([
127 "rev-parse",
128 "--abbrev-ref",
129 "--symbolic-full-name",
130 &format!("{}@{{u}}", branch.as_ref().display()),
131 ])
132 .await
133 .ok()
134 .map(|s| s.trim().to_owned())
135}