1use std::path::Path;
19
20#[derive(Debug, Clone, Default)]
22pub struct IgnoreRules {
23 rules: Vec<(bool, glob::Pattern)>,
25}
26
27impl IgnoreRules {
28 pub fn load(path: &Path) -> anyhow::Result<Self> {
31 if !path.exists() {
32 return Ok(Self::default());
33 }
34 let text = std::fs::read_to_string(path)
35 .map_err(|e| anyhow::anyhow!("Cannot read {}: {e}", path.display()))?;
36 Self::from_str(&text)
37 }
38
39 pub fn from_str(text: &str) -> anyhow::Result<Self> {
41 let mut rules = Vec::new();
42 for line in text.lines() {
43 let line = line.trim();
44 if line.is_empty() || line.starts_with('#') {
45 continue;
46 }
47 let (negated, pattern_str) = if let Some(rest) = line.strip_prefix('!') {
48 (true, rest.trim())
49 } else {
50 (false, line)
51 };
52 match glob::Pattern::new(pattern_str) {
53 Ok(pat) => rules.push((negated, pat)),
54 Err(e) => {
55 log::warn!(".aaaiignore: invalid pattern {:?} — {e}", pattern_str);
56 }
57 }
58 }
59 Ok(Self { rules })
60 }
61
62 pub fn is_ignored(&self, path: &str) -> bool {
67 let mut ignored = false;
68 for (negated, pat) in &self.rules {
69 if pat.matches(path) {
70 ignored = !negated;
71 }
72 }
73 ignored
74 }
75}
76
77#[cfg(test)]
80mod tests {
81 use super::*;
82
83 fn rules(text: &str) -> IgnoreRules {
84 IgnoreRules::from_str(text).unwrap()
85 }
86
87 #[test]
88 fn simple_glob_ignores() {
89 let r = rules("target/**\n*.lock");
90 assert!(r.is_ignored("target/debug/aaai"));
91 assert!(r.is_ignored("Cargo.lock"));
92 assert!(!r.is_ignored("src/main.rs"));
93 }
94
95 #[test]
96 fn negation_un_ignores() {
97 let r = rules("*.lock\n!Cargo.lock");
98 assert!(r.is_ignored("some.lock"));
99 assert!(!r.is_ignored("Cargo.lock"), "negation should un-ignore");
100 }
101
102 #[test]
103 fn comments_and_blanks_are_skipped() {
104 let r = rules("# comment\n\n*.tmp");
105 assert!(r.is_ignored("file.tmp"));
106 assert!(!r.is_ignored("file.rs"));
107 }
108
109 #[test]
110 fn empty_ruleset_ignores_nothing() {
111 let r = rules("");
112 assert!(!r.is_ignored("anything"));
113 }
114
115 #[test]
116 fn last_rule_wins() {
117 let r = rules("*.yaml\n!audit.yaml\n*.yaml");
119 assert!(r.is_ignored("audit.yaml"), "last *.yaml should win");
120 }
121}