1use globset::{Glob, GlobSet, GlobSetBuilder};
13use std::path::{Path, PathBuf};
14
15pub fn matches_pattern(files: &[PathBuf], project_root: &Path, pattern: &str) -> bool {
32 let is_simple_path = !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[');
34
35 for file in files {
36 let relative_path = normalize_path(file, project_root);
38
39 if is_simple_path {
40 let pattern_path = Path::new(pattern);
42 if relative_path.starts_with(pattern_path) {
43 return true;
44 }
45 } else {
46 let Ok(glob) = Glob::new(pattern) else {
48 tracing::trace!(pattern, "Skipping invalid glob pattern");
49 continue;
50 };
51 let Ok(set) = GlobSetBuilder::new().add(glob).build() else {
52 continue;
53 };
54 if set.is_match(&relative_path) {
55 return true;
56 }
57 }
58 }
59
60 false
61}
62
63pub fn build_glob_set<'a>(patterns: impl Iterator<Item = &'a str>) -> Option<GlobSet> {
73 let mut builder = GlobSetBuilder::new();
74 let mut has_patterns = false;
75
76 for pattern in patterns {
77 if let Ok(glob) = Glob::new(pattern) {
78 builder.add(glob);
79 has_patterns = true;
80 } else {
81 tracing::trace!(pattern, "Skipping invalid glob pattern");
82 }
83 }
84
85 if !has_patterns {
86 return None;
87 }
88
89 builder.build().ok()
90}
91
92fn normalize_path(file: &Path, project_root: &Path) -> PathBuf {
97 if project_root == Path::new(".") || project_root.as_os_str().is_empty() {
99 return file.to_path_buf();
100 }
101
102 if file.is_relative() {
104 if let Ok(stripped) = file.strip_prefix(project_root) {
106 return stripped.to_path_buf();
107 }
108 return file.to_path_buf();
110 }
111
112 file.strip_prefix(project_root)
114 .map(|p| p.to_path_buf())
115 .unwrap_or_else(|_| file.to_path_buf())
116}
117
118pub trait AffectedBy {
123 fn is_affected_by(&self, changed_files: &[PathBuf], project_root: &Path) -> bool;
134
135 fn input_patterns(&self) -> Vec<&str>;
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_matches_pattern_simple_prefix() {
147 let files = vec![PathBuf::from("crates/foo/bar.rs")];
148 let root = Path::new(".");
149
150 assert!(matches_pattern(&files, root, "crates"));
151 assert!(matches_pattern(&files, root, "crates/foo"));
152 assert!(matches_pattern(&files, root, "crates/foo/bar.rs"));
153 }
154
155 #[test]
156 fn test_matches_pattern_no_match() {
157 let files = vec![PathBuf::from("src/lib.rs")];
158 let root = Path::new(".");
159
160 assert!(!matches_pattern(&files, root, "crates"));
161 assert!(!matches_pattern(&files, root, "tests"));
162 }
163
164 #[test]
165 fn test_matches_pattern_glob() {
166 let files = vec![PathBuf::from("src/lib.rs"), PathBuf::from("src/main.rs")];
167 let root = Path::new(".");
168
169 assert!(matches_pattern(&files, root, "src/*.rs"));
170 assert!(!matches_pattern(&files, root, "*.txt"));
171 }
172
173 #[test]
174 fn test_matches_pattern_with_project_root() {
175 let files = vec![PathBuf::from("projects/website/src/app.rs")];
177 let root = Path::new("projects/website");
178
179 assert!(matches_pattern(&files, root, "src"));
181 assert!(matches_pattern(&files, root, "src/app.rs"));
182 }
183
184 #[test]
185 fn test_matches_pattern_different_project() {
186 let files = vec![PathBuf::from("projects/api/src/main.rs")];
188 let root = Path::new("projects/website");
189
190 assert!(!matches_pattern(&files, root, "src"));
192 }
193
194 #[test]
195 fn test_normalize_path_relative_file_with_project_root() {
196 let file = Path::new("projects/website/src/lib.rs");
197 let root = Path::new("projects/website");
198
199 let normalized = normalize_path(file, root);
200 assert_eq!(normalized, PathBuf::from("src/lib.rs"));
201 }
202
203 #[test]
204 fn test_normalize_path_dot_root() {
205 let file = Path::new("src/lib.rs");
206 let root = Path::new(".");
207
208 let normalized = normalize_path(file, root);
209 assert_eq!(normalized, PathBuf::from("src/lib.rs"));
210 }
211
212 #[test]
213 fn test_build_glob_set() {
214 let patterns = ["src/**/*.rs", "tests/*.rs"];
215 let set = build_glob_set(patterns.iter().copied()).unwrap();
216
217 assert!(set.is_match("src/lib.rs"));
218 assert!(set.is_match("src/foo/bar.rs"));
219 assert!(set.is_match("tests/test.rs"));
220 assert!(!set.is_match("docs/readme.md"));
221 }
222
223 #[test]
224 fn test_build_glob_set_invalid_patterns() {
225 let patterns = ["[invalid", "src/**"];
226 let set = build_glob_set(patterns.iter().copied()).unwrap();
227
228 assert!(set.is_match("src/lib.rs"));
230 }
231
232 #[test]
233 fn test_build_glob_set_empty() {
234 let patterns: [&str; 0] = [];
235 let set = build_glob_set(patterns.iter().copied());
236 assert!(set.is_none());
237 }
238}