Skip to main content

code_moniker_workspace/
gitignore.rs

1use std::path::Path;
2
3use ignore::Match;
4use ignore::gitignore::{Gitignore, GitignoreBuilder};
5
6/// The `.gitignore`/`.ignore` rules in effect under a root directory, kept as
7/// one matcher per directory and evaluated deepest-first so a nested rule wins
8/// over a shallower one (standard gitignore precedence).
9#[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
61/// Directory names never worth descending into when collecting ignore rules or
62/// classifying live events: VCS, build, and cache dirs that are always ignored.
63pub 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}