use std::process::Command;
fn run(args: &[&str]) -> Result<Vec<u8>, String> {
let out = Command::new("git")
.args(["-c", "core.quotePath=false"])
.args(args)
.output()
.map_err(|e| format!("cannot run git: {e}"))?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
return Err(format!("git {}: {err}", args.join(" ")));
}
Ok(out.stdout)
}
fn nul_separated(bytes: Vec<u8>) -> Vec<String> {
bytes
.split(|b| *b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).into_owned())
.collect()
}
fn with_pathspec<'a>(base: &[&'a str], paths: &'a [String]) -> Vec<&'a str> {
let mut args: Vec<&str> = base.to_vec();
args.push("--");
args.extend(paths.iter().map(String::as_str));
args
}
pub fn staged(paths: &[String]) -> Result<Vec<String>, String> {
let args = with_pathspec(&["diff", "--cached", "--name-only", "-z"], paths);
Ok(nul_separated(run(&args)?))
}
pub fn staged_existing(paths: &[String]) -> Result<Vec<String>, String> {
let base = ["diff", "--cached", "--name-only", "-z", "--diff-filter=d"];
let args = with_pathspec(&base, paths);
Ok(nul_separated(run(&args)?))
}
pub fn unmatched_literals(paths: &[String]) -> Result<Vec<String>, String> {
let mut bad = Vec::new();
for spec in paths {
if spec.contains(['*', '?', '[', ':']) {
continue;
}
let args = with_pathspec(&["ls-files", "-z"], std::slice::from_ref(spec));
if nul_separated(run(&args)?).is_empty() {
bad.push(spec.clone());
}
}
Ok(bad)
}
pub fn relative_to_root(doc: &str) -> Option<String> {
let root = run(&["rev-parse", "--show-toplevel"]).ok()?;
let root = std::path::Path::new(String::from_utf8_lossy(&root).trim())
.canonicalize()
.ok()?;
let rest = std::path::Path::new(doc).strip_prefix(root).ok()?;
Some(rest.to_string_lossy().into_owned())
}
pub fn in_progress(marker: &str) -> bool {
let Ok(out) = run(&["rev-parse", "--git-path", marker]) else {
return false;
};
std::path::Path::new(String::from_utf8_lossy(&out).trim()).exists()
}
pub fn trailers(msg_file: &str) -> Result<String, String> {
let out = run(&["interpret-trailers", "--parse", msg_file])?;
Ok(String::from_utf8_lossy(&out).into_owned())
}