kaish_kernel/
ignore_config.rs1use std::path::Path;
8
9use crate::walker::{IgnoreFilter, WalkerFs};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum IgnoreScope {
14 Advisory,
17 Enforced,
20}
21
22#[derive(Debug, Clone)]
27pub struct IgnoreConfig {
28 scope: IgnoreScope,
29 ignore_files: Vec<String>,
30 use_defaults: bool,
31 auto_gitignore: bool,
32}
33
34impl IgnoreConfig {
35 pub fn none() -> Self {
37 Self {
38 scope: IgnoreScope::Advisory,
39 ignore_files: Vec::new(),
40 use_defaults: false,
41 auto_gitignore: false,
42 }
43 }
44
45 pub fn mcp() -> Self {
47 Self {
48 scope: IgnoreScope::Enforced,
49 ignore_files: vec![".gitignore".to_string()],
50 use_defaults: true,
51 auto_gitignore: true,
52 }
53 }
54
55 pub fn is_active(&self) -> bool {
57 self.use_defaults || self.auto_gitignore || !self.ignore_files.is_empty()
58 }
59
60 pub fn scope(&self) -> IgnoreScope {
61 self.scope
62 }
63
64 pub fn auto_gitignore(&self) -> bool {
66 self.auto_gitignore
67 }
68
69 pub fn use_defaults(&self) -> bool {
70 self.use_defaults
71 }
72
73 pub fn files(&self) -> &[String] {
74 &self.ignore_files
75 }
76
77 pub fn set_scope(&mut self, scope: IgnoreScope) {
78 self.scope = scope;
79 }
80
81 pub fn set_defaults(&mut self, on: bool) {
82 self.use_defaults = on;
83 }
84
85 pub fn set_auto_gitignore(&mut self, on: bool) {
86 self.auto_gitignore = on;
87 }
88
89 pub fn add_file(&mut self, name: &str) {
90 if !self.ignore_files.iter().any(|f| f == name) {
91 self.ignore_files.push(name.to_string());
92 }
93 }
94
95 pub fn remove_file(&mut self, name: &str) {
96 self.ignore_files.retain(|f| f != name);
97 }
98
99 pub fn clear(&mut self) {
100 self.ignore_files.clear();
101 self.use_defaults = false;
102 self.auto_gitignore = false;
103 }
104
105 pub async fn build_filter<F: WalkerFs>(
110 &self,
111 root: &Path,
112 fs: &F,
113 ) -> Option<IgnoreFilter> {
114 if !self.is_active() {
115 return None;
116 }
117
118 let mut filter = if self.use_defaults {
119 IgnoreFilter::with_defaults()
120 } else {
121 IgnoreFilter::new()
122 };
123
124 for filename in &self.ignore_files {
125 let path = root.join(filename);
126 if let Ok(file_filter) = IgnoreFilter::from_gitignore(&path, fs).await {
127 filter.merge(&file_filter);
128 }
129 }
131
132 Some(filter)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_none_is_inactive() {
142 let config = IgnoreConfig::none();
143 assert!(!config.is_active());
144 assert_eq!(config.scope(), IgnoreScope::Advisory);
145 assert!(!config.auto_gitignore());
146 }
147
148 #[test]
149 fn test_mcp_is_active() {
150 let config = IgnoreConfig::mcp();
151 assert!(config.is_active());
152 assert_eq!(config.scope(), IgnoreScope::Enforced);
153 assert!(config.auto_gitignore());
154 assert!(config.use_defaults());
155 assert_eq!(config.files(), &[".gitignore"]);
156 }
157
158 #[test]
159 fn test_add_remove_files() {
160 let mut config = IgnoreConfig::none();
161 assert!(!config.is_active());
162
163 config.add_file(".dockerignore");
164 assert!(config.is_active());
165 assert_eq!(config.files(), &[".dockerignore"]);
166
167 config.add_file(".dockerignore");
169 assert_eq!(config.files().len(), 1);
170
171 config.remove_file(".dockerignore");
172 assert!(config.files().is_empty());
173 }
174
175 #[test]
176 fn test_clear() {
177 let mut config = IgnoreConfig::mcp();
178 config.clear();
179 assert!(!config.is_active());
180 assert!(config.files().is_empty());
181 assert!(!config.use_defaults());
182 assert!(!config.auto_gitignore());
183 }
184
185 #[test]
186 fn test_set_scope() {
187 let mut config = IgnoreConfig::none();
188 config.set_scope(IgnoreScope::Enforced);
189 assert_eq!(config.scope(), IgnoreScope::Enforced);
190 }
191
192 #[test]
193 fn test_defaults_toggle() {
194 let mut config = IgnoreConfig::none();
195 config.set_defaults(true);
196 assert!(config.is_active());
197 config.set_defaults(false);
198 assert!(!config.is_active());
199 }
200
201 #[test]
202 fn test_auto_gitignore_alone_is_active() {
203 let mut config = IgnoreConfig::none();
204 assert!(!config.is_active());
205 config.set_auto_gitignore(true);
206 assert!(config.is_active());
207 }
208
209 mod async_tests {
210 use super::*;
211 use crate::walker::{WalkerDirEntry, WalkerError, WalkerFs};
212 use std::collections::HashMap;
213 use std::path::PathBuf;
214
215 struct MemEntry;
216 impl WalkerDirEntry for MemEntry {
217 fn name(&self) -> &str { "" }
218 fn is_dir(&self) -> bool { false }
219 fn is_file(&self) -> bool { true }
220 fn is_symlink(&self) -> bool { false }
221 }
222
223 struct FakeFs(HashMap<PathBuf, Vec<u8>>);
224
225 #[async_trait::async_trait]
226 impl WalkerFs for FakeFs {
227 type DirEntry = MemEntry;
228 async fn list_dir(&self, _: &Path) -> Result<Vec<MemEntry>, WalkerError> {
229 Ok(vec![])
230 }
231 async fn read_file(&self, path: &Path) -> Result<Vec<u8>, WalkerError> {
232 self.0.get(path)
233 .cloned()
234 .ok_or_else(|| WalkerError::NotFound(path.display().to_string()))
235 }
236 async fn is_dir(&self, _: &Path) -> bool { false }
237 async fn exists(&self, path: &Path) -> bool { self.0.contains_key(path) }
238 }
239
240 #[tokio::test]
241 async fn test_build_filter_none_returns_none() {
242 let config = IgnoreConfig::none();
243 let fs = FakeFs(HashMap::new());
244 assert!(config.build_filter(Path::new("/"), &fs).await.is_none());
245 }
246
247 #[tokio::test]
248 async fn test_build_filter_defaults_returns_some() {
249 let mut config = IgnoreConfig::none();
250 config.set_defaults(true);
251 let fs = FakeFs(HashMap::new());
252
253 let filter = config.build_filter(Path::new("/"), &fs).await;
254 assert!(filter.is_some());
255 let filter = filter.unwrap();
256 assert!(filter.is_name_ignored("target", true));
258 assert!(filter.is_name_ignored("node_modules", true));
259 assert!(!filter.is_name_ignored("src", true));
260 }
261
262 #[tokio::test]
263 async fn test_build_filter_loads_gitignore() {
264 let mut config = IgnoreConfig::none();
265 config.add_file(".gitignore");
266
267 let mut files = HashMap::new();
268 files.insert(PathBuf::from("/project/.gitignore"), b"*.log\nbuild/\n".to_vec());
269 let fs = FakeFs(files);
270
271 let filter = config.build_filter(Path::new("/project"), &fs).await;
272 assert!(filter.is_some());
273 let filter = filter.unwrap();
274 assert!(filter.is_name_ignored("debug.log", false));
275 assert!(filter.is_name_ignored("build", true));
276 assert!(!filter.is_name_ignored("src", true));
277 }
278
279 #[tokio::test]
280 async fn test_build_filter_missing_file_skipped() {
281 let mut config = IgnoreConfig::none();
282 config.add_file(".gitignore");
283 config.add_file(".nonexistent");
284
285 let mut files = HashMap::new();
286 files.insert(PathBuf::from("/root/.gitignore"), b"*.tmp\n".to_vec());
287 let fs = FakeFs(files);
288
289 let filter = config.build_filter(Path::new("/root"), &fs).await;
291 assert!(filter.is_some());
292 let filter = filter.unwrap();
293 assert!(filter.is_name_ignored("test.tmp", false));
294 }
295
296 #[tokio::test]
297 async fn test_build_filter_defaults_plus_gitignore_merged() {
298 let config = IgnoreConfig::mcp();
299
300 let mut files = HashMap::new();
301 files.insert(PathBuf::from("/project/.gitignore"), b"*.secret\n".to_vec());
302 let fs = FakeFs(files);
303
304 let filter = config.build_filter(Path::new("/project"), &fs).await;
305 assert!(filter.is_some());
306 let filter = filter.unwrap();
307 assert!(filter.is_name_ignored("target", true));
309 assert!(filter.is_name_ignored("node_modules", true));
310 assert!(filter.is_name_ignored("passwords.secret", false));
312 assert!(!filter.is_name_ignored("main.rs", false));
314 }
315 }
316}