use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::path::{Path, PathBuf};
pub const AGENTIC_IGNORE_FILES: &[&str] = &[
".gitignore",
".cursorignore",
".aiignore",
".claudeignore",
".aiderignore",
".codeiumignore",
".copilotignore",
".tabbyignore",
];
#[derive(Default)]
pub struct AgenticIgnore {
matchers: Vec<Gitignore>,
}
impl AgenticIgnore {
#[must_use]
pub fn discover(root: &Path) -> Self {
let canonical = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
let mut matchers = Vec::new();
let mut dir: Option<&Path> = Some(&canonical);
while let Some(d) = dir {
for &filename in AGENTIC_IGNORE_FILES {
let ignore_path = d.join(filename);
if ignore_path.is_file()
&& let Some(gi) = load_ignore_file(d, &ignore_path)
{
matchers.push(gi);
}
}
dir = d.parent();
}
Self { matchers }
}
#[must_use]
pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool {
for gi in &self.matchers {
match gi.matched(path, is_dir) {
ignore::Match::Ignore(_) => return true,
ignore::Match::Whitelist(_) => return false,
ignore::Match::None => {}
}
}
false
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.matchers.is_empty()
}
}
fn load_ignore_file(root: &Path, path: &PathBuf) -> Option<Gitignore> {
let mut builder = GitignoreBuilder::new(root);
if builder.add(path).is_some() {
return None;
}
builder.build().ok()
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use std::fs;
#[test]
fn empty_when_no_ignore_files() {
let dir = tempfile::tempdir().expect("tempdir");
let ai = AgenticIgnore::discover(dir.path());
assert!(ai.is_empty());
assert!(!ai.is_ignored(dir.path().join("foo.rs").as_path(), false));
}
#[test]
fn respects_cursorignore() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(dir.path().join(".cursorignore"), "secret/\n*.env\n").expect("write");
let ai = AgenticIgnore::discover(dir.path());
assert!(!ai.is_empty());
assert!(ai.is_ignored(&dir.path().join("secret"), true));
assert!(ai.is_ignored(&dir.path().join(".env"), false));
assert!(!ai.is_ignored(&dir.path().join("src/main.rs"), false));
}
#[test]
fn respects_negation() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(dir.path().join(".aiignore"), "*.log\n!important.log\n").expect("write");
let ai = AgenticIgnore::discover(dir.path());
assert!(ai.is_ignored(&dir.path().join("debug.log"), false));
assert!(!ai.is_ignored(&dir.path().join("important.log"), false));
}
#[test]
fn traverses_upward() {
let parent = tempfile::tempdir().expect("tempdir");
let child = parent.path().join("project");
fs::create_dir_all(&child).expect("mkdir");
fs::write(parent.path().join(".claudeignore"), "*.secret\n").expect("write");
let ai = AgenticIgnore::discover(&child);
assert!(ai.is_ignored(&child.join("api.secret"), false));
}
}