use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub(crate) fn gitignored_dirs(base_dir: &Path) -> HashSet<PathBuf> {
let Ok(output) = std::process::Command::new("git")
.arg("-C")
.arg(base_dir)
.args(["ls-files", "--others", "--ignored", "--exclude-standard", "--directory"])
.output()
else {
return HashSet::new();
};
if !output.status.success() {
return HashSet::new();
}
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| line.strip_suffix('/'))
.map(PathBuf::from)
.collect()
}
pub(crate) fn gitignored_paths(base_dir: &Path, paths: &[PathBuf]) -> HashSet<PathBuf> {
if paths.is_empty() {
return HashSet::new();
}
let relative: Vec<(String, &PathBuf)> = paths
.iter()
.map(|path| {
let display = path
.strip_prefix(base_dir)
.unwrap_or(path)
.to_string_lossy()
.into_owned();
(display, path)
})
.collect();
let Ok(mut child) = std::process::Command::new("git")
.arg("-C")
.arg(base_dir)
.args(["check-ignore", "--stdin", "-z"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
else {
return HashSet::new();
};
let Some(mut stdin) = child.stdin.take() else {
return HashSet::new();
};
let input: Vec<u8> = relative
.iter()
.flat_map(|(rel, _)| rel.as_bytes().iter().copied().chain(std::iter::once(0u8)))
.collect();
let writer = std::thread::spawn(move || {
use std::io::Write;
let _ = stdin.write_all(&input);
});
let Ok(output) = child.wait_with_output() else {
let _ = writer.join();
return HashSet::new();
};
let _ = writer.join();
if !output.status.success() && output.status.code() != Some(1) {
return HashSet::new();
}
let by_relative: std::collections::HashMap<&str, &PathBuf> =
relative.iter().map(|(rel, abs)| (rel.as_str(), *abs)).collect();
output
.stdout
.split(|&byte| byte == 0)
.filter(|chunk| !chunk.is_empty())
.filter_map(|chunk| std::str::from_utf8(chunk).ok())
.filter_map(|rel| by_relative.get(rel).map(|abs| (*abs).clone()))
.collect()
}
pub(crate) fn split_missing_by_gitignore(base_dir: &Path, missing: &[String]) -> (Vec<String>, Vec<String>) {
let paths: Vec<PathBuf> = missing.iter().map(PathBuf::from).collect();
let ignored = gitignored_paths(base_dir, &paths);
let mut absent = Vec::new();
let mut absent_gitignored = Vec::new();
for (path, display) in paths.into_iter().zip(missing.iter()) {
if ignored.contains(&path) {
absent_gitignored.push(display.clone());
} else {
absent.push(display.clone());
}
}
(absent, absent_gitignored)
}
#[cfg(test)]
mod tests;