use std::{path::PathBuf, process::Stdio};
use anyhow::{Context, bail};
use tokio::process::Command;
pub struct GitClient;
impl GitClient {
pub fn new() -> Self {
Self
}
pub async fn remote_url(&self, name: &str) -> anyhow::Result<String> {
self.run(&["remote", "get-url", name]).await
}
pub async fn default_branch(&self) -> anyhow::Result<String> {
let remote_head = self
.run(&["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])
.await?;
Ok(remote_head
.rsplit('/')
.next()
.unwrap_or(&remote_head)
.to_string())
}
pub async fn git_dir(&self) -> anyhow::Result<PathBuf> {
let dir = self.run(&["rev-parse", "--git-dir"]).await?;
Ok(PathBuf::from(dir))
}
pub async fn rev_parse(&self, rev: &str) -> anyhow::Result<String> {
self.run(&["rev-parse", rev]).await
}
pub async fn current_branch(&self) -> anyhow::Result<String> {
self.run(&["branch", "--show-current"]).await
}
pub async fn last_commit_message(&self) -> anyhow::Result<String> {
self.run(&["log", "-1", "--format=%s"]).await
}
pub async fn log_oneline(&self, range: &str) -> anyhow::Result<Vec<(String, String)>> {
let stdout = self.run(&["log", range, "--oneline"]).await?;
Ok(stdout
.lines()
.filter_map(|line| {
let (hash, subject) = line.split_once(' ')?;
Some((hash.to_string(), subject.to_string()))
})
.collect())
}
pub async fn pull_rebase(&self, branch: &str) -> anyhow::Result<()> {
self.run_silent(&["pull", "origin", branch, "--rebase"])
.await
.with_context(|| format!("Failed to rebase onto origin/{}", branch))
}
pub async fn push_refspec(&self, refspec: &str) -> anyhow::Result<String> {
let output = Command::new("git")
.args(["push", "origin", refspec])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.context("Failed to run git push")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("{}", stderr.trim());
bail!("Failed to push to {}", refspec);
}
Ok(String::from_utf8_lossy(&output.stderr).to_string())
}
pub async fn add_all(&self) -> anyhow::Result<()> {
self.run_status(&["add", "-A"])
.await
.context("Failed to stage changes")
}
pub async fn commit_amend_no_edit(&self) -> anyhow::Result<()> {
self.run_status(&["commit", "--amend", "--no-edit"])
.await
.context("Failed to amend commit")
}
pub async fn commit_allow_empty(&self, message: &str) -> anyhow::Result<()> {
self.run_status(&["commit", "--allow-empty-message", "-m", message])
.await
.context("Failed to commit")
}
pub async fn rebase_onto(
&self,
new_base: &str,
old_base: &str,
branch: &str,
) -> anyhow::Result<()> {
self.run_status(&["rebase", "--onto", new_base, old_base, branch])
.await
.context("Rebase failed. Resolve conflicts and run `git rebase --continue`.")
}
pub async fn checkout(&self, rev: &str) -> anyhow::Result<()> {
self.run_status(&["checkout", rev])
.await
.with_context(|| format!("Failed to checkout {}", rev))
}
async fn run(&self, args: &[&str]) -> anyhow::Result<String> {
let output = Command::new("git")
.args(args)
.output()
.await
.with_context(|| format!("Failed to run `git {}`", args.join(" ")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("`git {}` failed: {}", args.join(" "), stderr.trim());
}
Ok(String::from_utf8(output.stdout)?.trim().to_string())
}
async fn run_status(&self, args: &[&str]) -> anyhow::Result<()> {
let status = Command::new("git")
.args(args)
.status()
.await
.with_context(|| format!("Failed to run `git {}`", args.join(" ")))?;
if !status.success() {
bail!("`git {}` failed", args.join(" "));
}
Ok(())
}
async fn run_silent(&self, args: &[&str]) -> anyhow::Result<()> {
let status = Command::new("git")
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await
.with_context(|| format!("Failed to run `git {}`", args.join(" ")))?;
if !status.success() {
bail!("`git {}` failed", args.join(" "));
}
Ok(())
}
}