use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, anyhow};
pub fn ensure_available() -> Result<()> {
let output = Command::new("git")
.arg("--version")
.output()
.context("running `git --version` (is git installed and on PATH?)")?;
if !output.status.success() {
return Err(anyhow!(
"`git --version` exited with status {}",
output.status
));
}
Ok(())
}
pub fn clone(url: &str, dest: &Path) -> Result<()> {
let status = Command::new("git")
.arg("clone")
.arg(url)
.arg(dest)
.status()
.with_context(|| format!("invoking `git clone {url}`"))?;
if !status.success() {
return Err(anyhow!(
"`git clone {url}` failed with status {status} (check the URL and your credentials)"
));
}
Ok(())
}
pub fn fetch_and_fast_forward(repo: &Path) -> Result<()> {
run_git(repo, &["fetch", "--quiet", "--prune"])?;
run_git(repo, &["reset", "--quiet", "--hard", "@{upstream}"])?;
Ok(())
}
pub fn head_sha(repo: &Path) -> Result<String> {
let output = Command::new("git")
.current_dir(repo)
.args(["rev-parse", "HEAD"])
.output()
.with_context(|| format!("invoking `git rev-parse HEAD` in {}", repo.display()))?;
if !output.status.success() {
return Err(anyhow!(
"`git rev-parse HEAD` failed in {}: {}",
repo.display(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
let sha = String::from_utf8(output.stdout)
.context("`git rev-parse HEAD` returned non-UTF8 output")?;
Ok(sha.trim().to_string())
}
pub fn ls_tree_blobs(repo: &Path, refspec: &str, path: &Path) -> Result<HashMap<PathBuf, String>> {
let output = Command::new("git")
.current_dir(repo)
.args(["ls-tree", "-r", "-z", refspec, "--"])
.arg(path)
.output()
.with_context(|| {
format!(
"invoking `git ls-tree -r {refspec} -- {}` in {}",
path.display(),
repo.display()
)
})?;
if !output.status.success() {
return Err(anyhow!(
"`git ls-tree {refspec}` failed in {}: {}",
repo.display(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
let raw = String::from_utf8(output.stdout).context("`git ls-tree` returned non-UTF8 output")?;
let mut map = HashMap::new();
for entry in raw.split('\0') {
if entry.is_empty() {
continue;
}
let (meta, file) = entry
.split_once('\t')
.ok_or_else(|| anyhow!("malformed ls-tree entry: {entry:?}"))?;
let parts: Vec<&str> = meta.split_whitespace().collect();
if parts.len() != 3 || parts[1] != "blob" {
continue;
}
let sha = parts[2].to_string();
map.insert(PathBuf::from(file), sha);
}
Ok(map)
}
pub fn hash_object(file: &Path) -> Result<String> {
let output = Command::new("git")
.args(["hash-object"])
.arg(file)
.output()
.with_context(|| format!("invoking `git hash-object {}`", file.display()))?;
if !output.status.success() {
return Err(anyhow!(
"`git hash-object {}` failed: {}",
file.display(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
let sha =
String::from_utf8(output.stdout).context("`git hash-object` returned non-UTF8 output")?;
Ok(sha.trim().to_string())
}
pub fn add_all(repo: &Path, path: &Path) -> Result<()> {
let status = Command::new("git")
.current_dir(repo)
.args(["add", "-A", "--"])
.arg(path)
.status()
.with_context(|| {
format!(
"invoking `git add -A -- {}` in {}",
path.display(),
repo.display()
)
})?;
if !status.success() {
return Err(anyhow!(
"`git add` failed in {} with status {status}",
repo.display()
));
}
Ok(())
}
pub fn has_staged_changes(repo: &Path) -> Result<bool> {
let status = Command::new("git")
.current_dir(repo)
.args(["diff", "--cached", "--quiet"])
.status()
.with_context(|| format!("invoking `git diff --cached --quiet` in {}", repo.display()))?;
match status.code() {
Some(0) => Ok(false),
Some(1) => Ok(true),
_ => Err(anyhow!(
"`git diff --cached --quiet` exited unexpectedly with {status} in {}",
repo.display()
)),
}
}
pub fn commit(repo: &Path, message: &str) -> Result<String> {
let status = Command::new("git")
.current_dir(repo)
.args(["commit", "--quiet", "-m", message])
.status()
.with_context(|| format!("invoking `git commit` in {}", repo.display()))?;
if !status.success() {
return Err(anyhow!(
"`git commit` failed in {} with status {status} (is git user.name/user.email configured?)",
repo.display()
));
}
head_sha(repo)
}
pub fn push(repo: &Path) -> Result<()> {
let status = Command::new("git")
.current_dir(repo)
.args(["push"])
.status()
.with_context(|| format!("invoking `git push` in {}", repo.display()))?;
if !status.success() {
return Err(anyhow!(
"`git push` failed in {} with status {status} (check your credentials and write access)",
repo.display()
));
}
Ok(())
}
fn run_git(repo: &Path, args: &[&str]) -> Result<()> {
let status = Command::new("git")
.current_dir(repo)
.args(args)
.status()
.with_context(|| format!("invoking `git {}` in {}", args.join(" "), repo.display()))?;
if !status.success() {
return Err(anyhow!(
"`git {}` failed in {} with status {status}",
args.join(" "),
repo.display()
));
}
Ok(())
}