use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn collect_tracked_paths(root: &Path) -> Option<HashSet<PathBuf>> {
let output = Command::new("git")
.arg("-C")
.arg(root)
.args(["ls-files", "-z"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let mut out = HashSet::new();
for chunk in output.stdout.split(|&b| b == 0) {
if chunk.is_empty() {
continue;
}
let s = std::str::from_utf8(chunk).ok()?;
out.insert(PathBuf::from(s));
}
Some(out)
}
pub fn collect_changed_paths(root: &Path, base: Option<&str>) -> Option<HashSet<PathBuf>> {
let output = match base {
Some(base) => Command::new("git")
.arg("-C")
.arg(root)
.args(["diff", "--name-only", "--relative", "-z"])
.arg(format!("{base}...HEAD"))
.output()
.ok()?,
None => Command::new("git")
.arg("-C")
.arg(root)
.args([
"ls-files",
"--modified",
"--others",
"--exclude-standard",
"-z",
])
.output()
.ok()?,
};
if !output.status.success() {
return None;
}
let mut out = HashSet::new();
for chunk in output.stdout.split(|&b| b == 0) {
if chunk.is_empty() {
continue;
}
let s = std::str::from_utf8(chunk).ok()?;
out.insert(PathBuf::from(s));
}
Some(out)
}
pub fn head_commit_message(root: &Path) -> Option<String> {
let output = Command::new("git")
.arg("-C")
.arg(root)
.args(["log", "-1", "--format=%B"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let raw = String::from_utf8(output.stdout).ok()?;
Some(raw.trim_end_matches('\n').to_string())
}
pub fn dir_has_tracked_files<S>(
dir_rel: &Path,
tracked: &std::collections::HashSet<PathBuf, S>,
) -> bool
where
S: std::hash::BuildHasher,
{
tracked.iter().any(|p| p.starts_with(dir_rel))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collect_returns_none_outside_git() {
let tmp = tempfile::tempdir().unwrap();
let result = collect_tracked_paths(tmp.path());
assert!(result.is_none());
}
#[test]
fn collect_changed_returns_none_outside_git() {
let tmp = tempfile::tempdir().unwrap();
assert!(collect_changed_paths(tmp.path(), None).is_none());
assert!(collect_changed_paths(tmp.path(), Some("main")).is_none());
}
#[test]
fn head_message_returns_none_outside_git() {
let tmp = tempfile::tempdir().unwrap();
assert!(head_commit_message(tmp.path()).is_none());
}
#[test]
fn dir_has_tracked_files_walks_prefix() {
let mut set = HashSet::new();
set.insert(PathBuf::from("src/main.rs"));
set.insert(PathBuf::from("README.md"));
assert!(dir_has_tracked_files(Path::new("src"), &set));
assert!(!dir_has_tracked_files(Path::new("target"), &set));
assert!(!dir_has_tracked_files(Path::new("tar"), &set));
}
}