use anyhow::{bail, Context, Result};
pub fn staged_files() -> Result<Vec<String>> {
run_name_only(&["diff", "--name-only", "--diff-filter=ACMR", "--cached"])
}
pub fn changed_in_range(base: &str) -> Result<Vec<String>> {
let range = format!("{base}...HEAD");
run_name_only(&["diff", "--name-only", "--diff-filter=ACMR", &range])
}
pub fn staged_content(path: &str) -> Result<Option<String>> {
let output = std::process::Command::new("git")
.args(["show", &format!(":{path}")])
.output()
.context("cannot run git show")?;
if !output.status.success() {
return Ok(None);
}
Ok(String::from_utf8(output.stdout).ok())
}
fn run_name_only(args: &[&str]) -> Result<Vec<String>> {
let output = std::process::Command::new("git")
.args(args)
.output()
.context("cannot run git diff")?;
if !output.status.success() {
bail!(
"git diff failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.map(str::to_owned)
.collect())
}
pub fn git_dir() -> Option<std::path::PathBuf> {
let output = std::process::Command::new("git")
.args(["rev-parse", "--absolute-git-dir"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8(output.stdout).ok()?;
let trimmed = text.trim();
(!trimmed.is_empty()).then(|| std::path::PathBuf::from(trimmed))
}
pub fn hooks_path_override() -> Option<String> {
let output = std::process::Command::new("git")
.args(["config", "core.hooksPath"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8(output.stdout).ok()?;
let trimmed = text.trim();
(!trimmed.is_empty()).then(|| trimmed.to_owned())
}
pub fn repo_root() -> Result<Option<std::path::PathBuf>> {
let output = std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.map_err(|error| {
anyhow::anyhow!("`git` could not be run ({error}), so the repository root is unknown.")
})?;
if !output.status.success() {
return Ok(None);
}
let root = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if root.is_empty() {
return Ok(None);
}
Ok(Some(std::path::PathBuf::from(root)))
}
#[must_use]
pub fn last_commit_touching(path: &str) -> Option<String> {
let out = std::process::Command::new("git")
.args(["log", "-1", "--format=%cd", "--date=iso-strict-local", "--"])
.arg(path)
.env("TZ", "UTC")
.output()
.ok()?;
if !out.status.success() {
return None;
}
let ts = String::from_utf8_lossy(&out.stdout).trim().to_owned();
(!ts.is_empty()).then_some(ts)
}
#[must_use]
pub fn committed_in_head(path: &str) -> bool {
std::process::Command::new("git")
.args(["cat-file", "-e", &format!("HEAD:{path}")])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
#[must_use]
pub fn head_sha() -> Option<String> {
let out = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let sha = String::from_utf8_lossy(&out.stdout).trim().to_owned();
(!sha.is_empty()).then_some(sha)
}
#[must_use]
pub fn config_user_name() -> Option<String> {
let output = std::process::Command::new("git")
.args(["config", "user.name"])
.output()
.ok()?
.stdout;
let name = String::from_utf8_lossy(&output).trim().to_owned();
(!name.is_empty()).then_some(name)
}