code_moniker_workspace/
gitignore.rs1use std::path::Path;
2
3use ignore::Match;
4use ignore::gitignore::{Gitignore, GitignoreBuilder};
5
6#[derive(Clone, Debug)]
10pub struct GitignoreStack {
11 layers: Vec<Gitignore>,
12}
13
14impl GitignoreStack {
15 pub fn for_root(root: &Path) -> Self {
16 let mut layers = Vec::new();
17 collect_layers(root, &mut layers);
18 layers.sort_by_key(|layer| std::cmp::Reverse(layer.path().components().count()));
19 Self { layers }
20 }
21
22 pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool {
23 for layer in &self.layers {
24 if !path.starts_with(layer.path()) {
25 continue;
26 }
27 match layer.matched_path_or_any_parents(path, is_dir) {
28 Match::Ignore(_) => return true,
29 Match::Whitelist(_) => return false,
30 Match::None => {}
31 }
32 }
33 false
34 }
35}
36
37fn collect_layers(dir: &Path, out: &mut Vec<Gitignore>) {
38 let mut builder = GitignoreBuilder::new(dir);
39 let mut has_rules = false;
40 if let Ok(entries) = std::fs::read_dir(dir) {
41 for entry in entries.flatten() {
42 let path = entry.path();
43 let name = path.file_name().and_then(|n| n.to_str());
44 if path.is_dir() {
45 if !name.is_some_and(is_ignored_dir_name) {
46 collect_layers(&path, out);
47 }
48 } else if matches!(name, Some(".gitignore") | Some(".ignore")) {
49 let _ = builder.add(&path);
50 has_rules = true;
51 }
52 }
53 }
54 if has_rules {
55 if let Ok(gitignore) = builder.build() {
56 out.push(gitignore);
57 }
58 }
59}
60
61pub fn is_ignored_dir_name(name: &str) -> bool {
64 matches!(
65 name,
66 ".code-moniker-cache" | ".git" | ".gradle" | "target" | "node_modules" | "build" | "dist"
67 )
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn nested_anchored_pattern_stays_in_its_directory() {
76 let temp = tempfile::tempdir_in(env!("CARGO_MANIFEST_DIR")).expect("temp workspace");
77 let root = temp.path();
78 std::fs::write(root.join(".gitignore"), "*.log\n").expect("root gitignore");
79 std::fs::create_dir_all(root.join("nested")).expect("nested dir");
80 std::fs::write(root.join("nested/.gitignore"), "/keep.rs\n").expect("nested gitignore");
81
82 let rules = GitignoreStack::for_root(root);
83
84 assert!(rules.is_ignored(&root.join("nested/keep.rs"), false));
85 assert!(!rules.is_ignored(&root.join("keep.rs"), false));
86 assert!(rules.is_ignored(&root.join("server.log"), false));
87 }
88
89 #[test]
90 fn nested_whitelist_overrides_shallower_ignore() {
91 let temp = tempfile::tempdir_in(env!("CARGO_MANIFEST_DIR")).expect("temp workspace");
92 let root = temp.path();
93 std::fs::write(root.join(".gitignore"), "*.rs\n").expect("root gitignore");
94 std::fs::create_dir_all(root.join("keep")).expect("keep dir");
95 std::fs::write(root.join("keep/.gitignore"), "!*.rs\n").expect("nested gitignore");
96
97 let rules = GitignoreStack::for_root(root);
98
99 assert!(rules.is_ignored(&root.join("top.rs"), false));
100 assert!(!rules.is_ignored(&root.join("keep/lib.rs"), false));
101 }
102}