use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use ignore::Match;
const IGNORE_FILENAME: &str = ".capnpfmtignore";
pub struct Ignore {
matchers: Vec<Gitignore>,
}
impl Ignore {
pub fn discover(start_dir: &Path) -> Self {
let mut matchers = Vec::new();
let mut dir: Option<&Path> = Some(start_dir);
while let Some(d) = dir {
let candidate = d.join(IGNORE_FILENAME);
if candidate.is_file() {
let mut b = GitignoreBuilder::new(d);
let _ = b.add(&candidate);
if let Ok(gi) = b.build() {
matchers.push(gi);
}
}
if d.join(".git").exists() {
break;
}
dir = d.parent();
}
matchers.reverse();
Self { matchers }
}
pub fn is_ignored(&self, path: &Path) -> bool {
let mut decision = Match::None;
for gi in &self.matchers {
let m = gi.matched_path_or_any_parents(path, false);
if !matches!(m, Match::None) {
decision = m;
}
}
matches!(decision, Match::Ignore(_))
}
}
#[derive(Default)]
pub struct IgnoreCache {
by_dir: HashMap<PathBuf, Arc<Ignore>>,
}
impl IgnoreCache {
pub fn new() -> Self {
Self::default()
}
pub fn is_ignored(&mut self, path: &Path) -> bool {
let abs = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let parent = abs.parent().unwrap_or_else(|| Path::new("/"));
let ig = if let Some(ig) = self.by_dir.get(parent) {
ig.clone()
} else {
let ig = Arc::new(Ignore::discover(parent));
self.by_dir.insert(parent.to_path_buf(), ig.clone());
ig
};
ig.is_ignored(&abs)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
fn tempdir(label: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!(
"capnpfmt-ignore-{}-{}-{nanos}",
label,
std::process::id()
));
fs::create_dir_all(&dir).unwrap();
fs::create_dir_all(dir.join(".git")).unwrap();
dir
}
#[test]
fn matches_simple_pattern_in_subdir() {
let root = tempdir("simple");
fs::write(root.join(".capnpfmtignore"), "vendor/\n").unwrap();
fs::create_dir_all(root.join("vendor")).unwrap();
fs::write(root.join("vendor/foo.capnp"), "").unwrap();
fs::write(root.join("bar.capnp"), "").unwrap();
let mut cache = IgnoreCache::new();
assert!(cache.is_ignored(&root.join("vendor/foo.capnp")));
assert!(!cache.is_ignored(&root.join("bar.capnp")));
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn whitelist_in_nested_ignore_overrides_parent() {
let root = tempdir("nested");
fs::write(root.join(".capnpfmtignore"), "*.capnp\n").unwrap();
fs::create_dir_all(root.join("keep")).unwrap();
fs::write(root.join("keep/.capnpfmtignore"), "!*.capnp\n").unwrap();
fs::write(root.join("skip.capnp"), "").unwrap();
fs::write(root.join("keep/me.capnp"), "").unwrap();
let mut cache = IgnoreCache::new();
assert!(cache.is_ignored(&root.join("skip.capnp")));
assert!(!cache.is_ignored(&root.join("keep/me.capnp")));
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn discovery_stops_at_git_root() {
let root = tempdir("boundary");
let outside = root.parent().unwrap().join(".capnpfmtignore");
let _ = fs::write(&outside, "*.capnp\n");
fs::write(root.join("inside.capnp"), "").unwrap();
let mut cache = IgnoreCache::new();
assert!(!cache.is_ignored(&root.join("inside.capnp")));
let _ = fs::remove_file(&outside);
fs::remove_dir_all(&root).unwrap();
}
}