use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub fn tracked_paths_under(root: &Path) -> Option<HashSet<PathBuf>> {
let output = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(["ls-files", "-z", "--", "."])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let mut tracked = HashSet::new();
for entry in output.stdout.split(|&byte| byte == 0) {
if entry.is_empty() {
continue;
}
let relative = std::str::from_utf8(entry).ok()?;
tracked.insert(root.join(relative));
}
Some(tracked)
}
pub struct IgnoreFilter {
root: PathBuf,
ignored: Option<Vec<PathBuf>>,
}
impl IgnoreFilter {
pub fn for_root(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
ignored: ignored_entries_under(root),
}
}
pub fn for_current_dir() -> Self {
let root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Self::for_root(&root)
}
pub fn is_degraded(&self) -> bool {
self.ignored.is_none()
}
pub fn allows(&self, path: &Path) -> bool {
let Some(ignored) = self.ignored.as_ref() else {
return true;
};
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
};
!ignored.iter().any(|entry| absolute.starts_with(entry))
}
pub fn glob(&self, pattern: &str) -> Vec<PathBuf> {
glob::glob(pattern)
.into_iter()
.flatten()
.flatten()
.filter(|path| {
if self.allows(path) {
return true;
}
tracing::debug!(
path = %path.display(),
pattern,
"skipping a git-ignored match: it is build staging or another disposable copy, \
and rewriting it would produce an edit nobody reviews that vanishes on clean"
);
false
})
.collect()
}
}
fn ignored_entries_under(root: &Path) -> Option<Vec<PathBuf>> {
let output = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args([
"ls-files",
"-z",
"--others",
"--ignored",
"--exclude-standard",
"--directory",
"--no-empty-directory",
"--",
".",
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let mut ignored = Vec::new();
for entry in output.stdout.split(|&byte| byte == 0) {
if entry.is_empty() {
continue;
}
let relative = std::str::from_utf8(entry).ok()?;
ignored.push(root.join(relative.trim_end_matches('/')));
}
Some(ignored)
}