1const DEFAULT_EXCLUDES: &[&str] = &[
2 ".git",
3 "node_modules/.cache",
4 ".DS_Store",
5 "target",
6];
7
8pub struct PathFilter {
10 include_prefixes: Vec<String>,
11 exclude_patterns: Vec<String>,
12}
13
14impl PathFilter {
15 pub fn new(include: Vec<String>, exclude: Vec<String>) -> Self {
16 PathFilter {
17 include_prefixes: include,
18 exclude_patterns: exclude,
19 }
20 }
21
22 pub fn is_included(&self, path: &str) -> bool {
24 for excl in DEFAULT_EXCLUDES {
26 if path.starts_with(excl) || path.contains(&format!("/{}", excl)) {
27 return false;
28 }
29 }
30
31 for pattern in &self.exclude_patterns {
33 if glob_matches(pattern, path) {
34 return false;
35 }
36 }
37
38 if self.include_prefixes.is_empty() {
41 return true;
42 }
43
44 self.include_prefixes.iter().any(|prefix| {
45 path == prefix || path.starts_with(&format!("{}/", prefix))
46 })
47 }
48}
49
50fn glob_matches(pattern: &str, path: &str) -> bool {
52 if let Some(ext) = pattern.strip_prefix("*.") {
53 let filename = path.rsplit('/').next().unwrap_or(path);
54 filename.ends_with(&format!(".{}", ext))
55 } else {
56 path == pattern || path.starts_with(&format!("{}/", pattern))
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn include_matches() {
66 let filter = PathFilter::new(
67 vec!["src".into(), "package.json".into()],
68 vec![],
69 );
70 assert!(filter.is_included("src/main.rs"));
71 assert!(filter.is_included("src/lib/utils.rs"));
72 assert!(filter.is_included("package.json"));
73 assert!(!filter.is_included("README.md"));
74 }
75
76 #[test]
77 fn exclude_overrides_include() {
78 let filter = PathFilter::new(
79 vec!["src".into()],
80 vec!["*.log".into()],
81 );
82 assert!(filter.is_included("src/main.rs"));
83 assert!(!filter.is_included("src/debug.log"));
84 }
85
86 #[test]
87 fn default_excludes() {
88 let filter = PathFilter::new(vec![], vec![]);
89 assert!(!filter.is_included(".git/config"));
90 assert!(!filter.is_included("node_modules/.cache/foo"));
91 }
92
93 #[test]
94 fn empty_include_means_all() {
95 let filter = PathFilter::new(vec![], vec![]);
96 assert!(filter.is_included("any/file.rs"));
97 assert!(filter.is_included("other.txt"));
98 }
99
100 #[test]
101 fn glob_patterns_in_exclude() {
102 let filter = PathFilter::new(
103 vec!["src".into()],
104 vec!["*.tmp".into(), "*.swp".into()],
105 );
106 assert!(!filter.is_included("src/file.tmp"));
107 assert!(!filter.is_included("src/.main.rs.swp"));
108 assert!(filter.is_included("src/main.rs"));
109 }
110}