use std::collections::HashSet;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use ignore::WalkBuilder;
pub struct FileEntry<'a> {
pub root: &'a Path,
pub rel: &'a Path,
pub abs: &'a Path,
}
pub fn find_files<T>(
roots: &[PathBuf],
ignore_dirs: &[String],
accept: impl Fn(&FileEntry) -> Option<T>,
) -> Result<Vec<T>> {
let ignored: HashSet<String> = ignore_dirs.iter().cloned().collect();
let mut found: Vec<(PathBuf, T)> = Vec::new();
let mut seen = HashSet::new();
for root in roots {
let ignored = ignored.clone();
let mut walker = WalkBuilder::new(root);
walker.filter_entry(move |entry| {
if entry.file_type().is_some_and(|t| t.is_dir())
&& let Some(name) = entry.file_name().to_str()
{
return !ignored.contains(name);
}
true
});
for result in walker.build() {
let entry = result.with_context(|| format!("error walking {}", root.display()))?;
if !entry.file_type().is_some_and(|t| t.is_file()) {
continue;
}
let abs = entry.path();
let rel = abs.strip_prefix(root).unwrap_or(abs);
let file = FileEntry { root, rel, abs };
if let Some(value) = accept(&file)
&& seen.insert(abs.to_path_buf())
{
found.push((abs.to_path_buf(), value));
}
}
}
found.sort_by(|a, b| a.0.cmp(&b.0));
Ok(found.into_iter().map(|(_, value)| value).collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("dead-poets-walk-{tag}"));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write(dir: &Path, name: &str, body: &str) {
let path = dir.join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&path, body).unwrap();
}
#[test]
fn accept_selects_and_ignore_dirs_prune() {
let dir = temp_dir("accept");
write(&dir, "keep.php", "");
write(&dir, "skip.txt", "");
write(&dir, "vendor/buried.php", "");
let hits = find_files(std::slice::from_ref(&dir), &["vendor".to_string()], |e| {
(e.abs.extension().and_then(|x| x.to_str()) == Some("php")).then(|| e.abs.to_path_buf())
})
.unwrap();
assert_eq!(hits.len(), 1);
assert!(hits[0].ends_with("keep.php"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn rel_is_relative_to_root() {
let dir = temp_dir("rel");
write(&dir, "a/b/c.po", "");
let rels = find_files(std::slice::from_ref(&dir), &[], |e| {
Some(e.rel.to_path_buf())
})
.unwrap();
assert_eq!(rels, vec![PathBuf::from("a/b/c.po")]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn dedups_overlapping_roots_and_sorts() {
let dir = temp_dir("dedup");
write(&dir, "z.php", "");
write(&dir, "a.php", "");
let sub = dir.join("nested");
std::fs::create_dir_all(&sub).unwrap();
write(&sub, "m.php", "");
let roots = vec![dir.clone(), sub.clone()];
let names: Vec<String> = find_files(&roots, &[], |e| {
Some(e.abs.file_name().unwrap().to_string_lossy().into_owned())
})
.unwrap();
assert_eq!(names, vec!["a.php", "m.php", "z.php"]);
std::fs::remove_dir_all(&dir).ok();
}
}