use std::path::{Path, PathBuf};
use std::process::Command;
pub struct Git {
root: PathBuf,
}
impl Git {
pub fn discover(start: &Path) -> Result<Self, Error> {
let start_dir = start
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.current_dir(start_dir)
.output()
.map_err(|e| Error::Exec(format!("git rev-parse: {e}")))?;
if !output.status.success() {
return Err(Error::NotARepo(start_dir.display().to_string()));
}
let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(Self {
root: PathBuf::from(root),
})
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn ref_exists(&self, refname: &str) -> bool {
Command::new("git")
.args(["rev-parse", "--verify", refname])
.current_dir(&self.root)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn merge_base(&self, ref1: &str, ref2: &str) -> Result<String, Error> {
let output = self.run_output(&["merge-base", ref1, ref2])?;
Ok(output.trim().to_string())
}
pub fn checkout(&self, branch: &str) -> Result<(), Error> {
self.run(&["checkout", branch])
}
pub fn checkout_new_branch(&self, branch: &str, start: &str) -> Result<(), Error> {
self.run(&["checkout", "-b", branch, start])
}
pub fn diff(&self, from: &str, to: &str) -> Result<String, Error> {
let range = format!("{from}..{to}");
self.run_output(&["diff", &range])
}
pub fn diff_stat(&self, from: &str, to: &str) -> Result<String, Error> {
let range = format!("{from}..{to}");
self.run_output(&["diff", "--stat", &range])
}
pub fn checkout_files(&self, refname: &str, pathspec: &str) -> Result<(), Error> {
self.run(&["checkout", refname, "--", pathspec])
}
pub fn add_all(&self) -> Result<(), Error> {
self.run(&["add", "-A"])
}
pub fn commit(&self, message: &str) -> Result<String, Error> {
self.add_all()?;
self.run(&["commit", "-m", message])?;
self.head_short()
}
pub fn head_short(&self) -> Result<String, Error> {
let hash = self.run_output(&["rev-parse", "HEAD"])?;
let hash = hash.trim();
Ok(hash[..8.min(hash.len())].to_string())
}
fn run(&self, args: &[&str]) -> Result<(), Error> {
let status = Command::new("git")
.args(args)
.current_dir(&self.root)
.status()
.map_err(|e| Error::Exec(format!("git {}: {e}", args.first().unwrap_or(&""))))?;
if status.success() {
Ok(())
} else {
Err(Error::Failed(format!("git {}", args.join(" "))))
}
}
fn run_output(&self, args: &[&str]) -> Result<String, Error> {
let output = Command::new("git")
.args(args)
.current_dir(&self.root)
.output()
.map_err(|e| Error::Exec(format!("git {}: {e}", args.first().unwrap_or(&""))))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(Error::Failed(format!("git {}", args.join(" "))))
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to execute: {0}")]
Exec(String),
#[error("not a git repository (searched from '{0}')")]
NotARepo(String),
#[error("{0}")]
Failed(String),
}