use ignore::WalkBuilder;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
pub const HARDCODED_IGNORE_DIRS: &[&str] = &[
".git", ".hg", ".svn", ".jj",
"__pycache__", ".venv", "venv", ".tox",
".mypy_cache", ".pytest_cache", ".ruff_cache",
"node_modules", ".next", ".nuxt", ".turbo", ".parcel-cache",
"dist", "build", "out", ".eggs", "target",
".cache", ".gradle", ".idea", ".vscode",
".ast-bro", ".ast-outline",
];
pub fn add_filters(builder: &mut WalkBuilder, repo_root: &Path) {
let new_name = ".ast-bro-ignore";
let old_name = ".ast-outline-ignore";
let rename_failed = migrate_legacy_ignore_file(repo_root, new_name, old_name);
if rename_failed {
builder.add_custom_ignore_filename(old_name);
}
builder.add_custom_ignore_filename(new_name);
}
fn migrate_legacy_ignore_file(repo_root: &Path, new_name: &str, old_name: &str) -> bool {
if !repo_root.is_dir() {
return false;
}
static STATE: OnceLock<Mutex<HashMap<PathBuf, bool>>> = OnceLock::new();
let map = STATE.get_or_init(|| Mutex::new(HashMap::new()));
let mut guard = map.lock().unwrap();
if let Some(&needs_fallback) = guard.get(repo_root) {
return needs_fallback;
}
let new_path = repo_root.join(new_name);
let old_path = repo_root.join(old_name);
let needs_fallback = if old_path.exists() && !new_path.exists() {
match fs::rename(&old_path, &new_path) {
Err(e) => {
eprintln!("warning: could not rename {old_name} -> {new_name}: {e}");
true
}
Ok(()) => {
eprintln!("info: auto-renamed {old_name} -> {new_name}");
false
}
}
} else {
false
};
guard.insert(repo_root.to_path_buf(), needs_fallback);
needs_fallback
}
pub fn should_skip_path(path: &Path, repo_root: &Path) -> bool {
let Ok(rel) = path.strip_prefix(repo_root) else {
return false;
};
rel.components().any(|c| {
let s = c.as_os_str().to_string_lossy();
HARDCODED_IGNORE_DIRS.iter().any(|d| *d == s)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn skip_node_modules_anywhere() {
let root = PathBuf::from("/r");
assert!(should_skip_path(&root.join("node_modules/lodash/index.js"), &root));
assert!(should_skip_path(
&root.join("packages/foo/node_modules/lib.js"),
&root,
));
}
#[test]
fn skip_target_dir() {
let root = PathBuf::from("/r");
assert!(should_skip_path(&root.join("target/debug/build/x.rs"), &root));
}
#[test]
fn skip_self_managed_index() {
let root = PathBuf::from("/r");
assert!(should_skip_path(&root.join(".ast-bro/index/meta.json"), &root));
}
#[test]
fn allow_normal_paths() {
let root = PathBuf::from("/r");
assert!(!should_skip_path(&root.join("src/main.rs"), &root));
assert!(!should_skip_path(&root.join("docs/README.md"), &root));
}
#[test]
fn allow_paths_outside_root() {
let root = PathBuf::from("/r");
assert!(!should_skip_path(&PathBuf::from("/elsewhere/node_modules/x"), &root));
}
}