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 for_path(root: &Path, path: &Path) -> Self {
23 let mut layers = Vec::new();
24 push_layer(root, &mut layers);
25 let parent = path.parent().unwrap_or(path);
26 if let Ok(relative) = parent.strip_prefix(root) {
27 let mut directory = root.to_path_buf();
28 for component in relative.components() {
29 directory.push(component.as_os_str());
30 push_layer(&directory, &mut layers);
31 }
32 }
33 layers.sort_by_key(|layer| std::cmp::Reverse(layer.path().components().count()));
34 Self { layers }
35 }
36
37 pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool {
38 for layer in &self.layers {
39 if !path.starts_with(layer.path()) {
40 continue;
41 }
42 match layer.matched_path_or_any_parents(path, is_dir) {
43 Match::Ignore(_) => return true,
44 Match::Whitelist(_) => return false,
45 Match::None => {}
46 }
47 }
48 false
49 }
50}
51
52fn collect_layers(dir: &Path, out: &mut Vec<Gitignore>) {
53 push_layer(dir, out);
54 if let Ok(entries) = std::fs::read_dir(dir) {
55 for entry in entries.flatten() {
56 let path = entry.path();
57 let name = path.file_name().and_then(|n| n.to_str());
58 if path.is_dir() && !name.is_some_and(is_ignored_dir_name) {
59 collect_layers(&path, out);
60 }
61 }
62 }
63}
64
65fn push_layer(dir: &Path, out: &mut Vec<Gitignore>) {
66 let mut builder = GitignoreBuilder::new(dir);
67 let mut has_rules = false;
68 for name in [".gitignore", ".ignore"] {
69 let path = dir.join(name);
70 if path.is_file() {
71 let _ = builder.add(&path);
72 has_rules = true;
73 }
74 }
75 if has_rules {
76 if let Ok(gitignore) = builder.build() {
77 out.push(gitignore);
78 }
79 }
80}
81
82pub fn is_ignored_dir_name(name: &str) -> bool {
85 matches!(
86 name,
87 ".code-moniker-cache" | ".git" | ".gradle" | "target" | "node_modules" | "build" | "dist"
88 )
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn nested_anchored_pattern_stays_in_its_directory() {
97 let temp = tempfile::tempdir_in(env!("CARGO_MANIFEST_DIR")).expect("temp workspace");
98 let root = temp.path();
99 std::fs::write(root.join(".gitignore"), "*.log\n").expect("root gitignore");
100 std::fs::create_dir_all(root.join("nested")).expect("nested dir");
101 std::fs::write(root.join("nested/.gitignore"), "/keep.rs\n").expect("nested gitignore");
102
103 let rules = GitignoreStack::for_root(root);
104
105 assert!(rules.is_ignored(&root.join("nested/keep.rs"), false));
106 assert!(!rules.is_ignored(&root.join("keep.rs"), false));
107 assert!(rules.is_ignored(&root.join("server.log"), false));
108 }
109
110 #[test]
111 fn nested_whitelist_overrides_shallower_ignore() {
112 let temp = tempfile::tempdir_in(env!("CARGO_MANIFEST_DIR")).expect("temp workspace");
113 let root = temp.path();
114 std::fs::write(root.join(".gitignore"), "*.rs\n").expect("root gitignore");
115 std::fs::create_dir_all(root.join("keep")).expect("keep dir");
116 std::fs::write(root.join("keep/.gitignore"), "!*.rs\n").expect("nested gitignore");
117
118 let rules = GitignoreStack::for_root(root);
119
120 assert!(rules.is_ignored(&root.join("top.rs"), false));
121 assert!(!rules.is_ignored(&root.join("keep/lib.rs"), false));
122 }
123
124 #[test]
125 fn path_stack_loads_only_the_candidate_ancestor_chain() {
126 let temp = tempfile::tempdir_in(env!("CARGO_MANIFEST_DIR")).expect("temp workspace");
127 let root = temp.path();
128 std::fs::write(root.join(".gitignore"), "*.rs\n").expect("root gitignore");
129 std::fs::create_dir_all(root.join("keep/deep")).expect("candidate dirs");
130 std::fs::write(root.join("keep/.gitignore"), "!*.rs\n").expect("nested gitignore");
131 std::fs::create_dir_all(root.join("unrelated")).expect("unrelated dir");
132 std::fs::write(root.join("unrelated/.ignore"), "*.txt\n").expect("unrelated ignore");
133
134 let candidate = root.join("keep/deep/lib.rs");
135 let rules = GitignoreStack::for_path(root, &candidate);
136
137 assert!(!rules.is_ignored(&candidate, false));
138 assert_eq!(rules.layers.len(), 2);
139 }
140}