1#![expect(missing_docs)]
16
17use std::collections::HashMap;
18use std::collections::HashSet;
19use std::fmt;
20use std::fmt::Debug;
21use std::iter;
22
23use globset::Glob;
24use itertools::Itertools as _;
25use tracing::instrument;
26
27use crate::repo_path::RepoPath;
28use crate::repo_path::RepoPathComponentBuf;
29
30#[derive(PartialEq, Eq, Debug)]
31pub enum Visit {
32 AllRecursively,
35 Specific {
36 dirs: VisitDirs,
37 files: VisitFiles,
38 },
39 Nothing,
45}
46
47impl Visit {
48 const SOME: Self = Self::Specific {
51 dirs: VisitDirs::All,
52 files: VisitFiles::All,
53 };
54
55 fn sets(dirs: HashSet<RepoPathComponentBuf>, files: HashSet<RepoPathComponentBuf>) -> Self {
56 if dirs.is_empty() && files.is_empty() {
57 Self::Nothing
58 } else {
59 Self::Specific {
60 dirs: VisitDirs::Set(dirs),
61 files: VisitFiles::Set(files),
62 }
63 }
64 }
65
66 pub fn is_nothing(&self) -> bool {
67 *self == Self::Nothing
68 }
69}
70
71#[derive(PartialEq, Eq, Debug)]
72pub enum VisitDirs {
73 All,
74 Set(HashSet<RepoPathComponentBuf>),
75}
76
77#[derive(PartialEq, Eq, Debug)]
78pub enum VisitFiles {
79 All,
80 Set(HashSet<RepoPathComponentBuf>),
81}
82
83pub trait Matcher: Debug + Send + Sync {
84 fn matches(&self, file: &RepoPath) -> bool;
85 fn visit(&self, dir: &RepoPath) -> Visit;
86}
87
88impl<T: Matcher + ?Sized> Matcher for &T {
89 fn matches(&self, file: &RepoPath) -> bool {
90 <T as Matcher>::matches(self, file)
91 }
92
93 fn visit(&self, dir: &RepoPath) -> Visit {
94 <T as Matcher>::visit(self, dir)
95 }
96}
97
98impl<T: Matcher + ?Sized> Matcher for Box<T> {
99 fn matches(&self, file: &RepoPath) -> bool {
100 <T as Matcher>::matches(self, file)
101 }
102
103 fn visit(&self, dir: &RepoPath) -> Visit {
104 <T as Matcher>::visit(self, dir)
105 }
106}
107
108#[derive(PartialEq, Eq, Debug)]
109pub struct NothingMatcher;
110
111impl Matcher for NothingMatcher {
112 fn matches(&self, _file: &RepoPath) -> bool {
113 false
114 }
115
116 fn visit(&self, _dir: &RepoPath) -> Visit {
117 Visit::Nothing
118 }
119}
120
121#[derive(PartialEq, Eq, Debug)]
122pub struct EverythingMatcher;
123
124impl Matcher for EverythingMatcher {
125 fn matches(&self, _file: &RepoPath) -> bool {
126 true
127 }
128
129 fn visit(&self, _dir: &RepoPath) -> Visit {
130 Visit::AllRecursively
131 }
132}
133
134#[derive(PartialEq, Eq, Debug)]
135pub struct FilesMatcher {
136 tree: RepoPathTree<FilesNodeKind>,
137}
138
139impl FilesMatcher {
140 pub fn new(files: impl IntoIterator<Item = impl AsRef<RepoPath>>) -> Self {
141 let mut tree = RepoPathTree::default();
142 for f in files {
143 tree.add(f.as_ref()).value = FilesNodeKind::File;
144 }
145 Self { tree }
146 }
147}
148
149impl Matcher for FilesMatcher {
150 fn matches(&self, file: &RepoPath) -> bool {
151 self.tree
152 .get(file)
153 .is_some_and(|sub| sub.value == FilesNodeKind::File)
154 }
155
156 fn visit(&self, dir: &RepoPath) -> Visit {
157 self.tree
158 .get(dir)
159 .map_or(Visit::Nothing, files_tree_to_visit_sets)
160 }
161}
162
163#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
164enum FilesNodeKind {
165 #[default]
167 Dir,
168 File,
170}
171
172fn files_tree_to_visit_sets(tree: &RepoPathTree<FilesNodeKind>) -> Visit {
173 let mut dirs = HashSet::new();
174 let mut files = HashSet::new();
175 for (name, sub) in &tree.entries {
176 if !sub.entries.is_empty() {
178 dirs.insert(name.clone());
179 }
180 if sub.value == FilesNodeKind::File {
181 files.insert(name.clone());
182 }
183 }
184 Visit::sets(dirs, files)
185}
186
187#[derive(Debug)]
188pub struct PrefixMatcher {
189 tree: RepoPathTree<PrefixNodeKind>,
190}
191
192impl PrefixMatcher {
193 #[instrument(skip(prefixes))]
194 pub fn new(prefixes: impl IntoIterator<Item = impl AsRef<RepoPath>>) -> Self {
195 let mut tree = RepoPathTree::default();
196 for prefix in prefixes {
197 tree.add(prefix.as_ref()).value = PrefixNodeKind::Prefix;
198 }
199 Self { tree }
200 }
201}
202
203impl Matcher for PrefixMatcher {
204 fn matches(&self, file: &RepoPath) -> bool {
205 self.tree
206 .walk_to(file)
207 .any(|(sub, _)| sub.value == PrefixNodeKind::Prefix)
208 }
209
210 fn visit(&self, dir: &RepoPath) -> Visit {
211 for (sub, tail_path) in self.tree.walk_to(dir) {
212 if sub.value == PrefixNodeKind::Prefix {
214 return Visit::AllRecursively;
215 }
216 if tail_path.is_root() {
218 return prefix_tree_to_visit_sets(sub);
219 }
220 }
221 Visit::Nothing
222 }
223}
224
225#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
226enum PrefixNodeKind {
227 #[default]
229 Dir,
230 Prefix,
232}
233
234fn prefix_tree_to_visit_sets(tree: &RepoPathTree<PrefixNodeKind>) -> Visit {
235 let mut dirs = HashSet::new();
236 let mut files = HashSet::new();
237 for (name, sub) in &tree.entries {
238 dirs.insert(name.clone());
240 if sub.value == PrefixNodeKind::Prefix {
241 files.insert(name.clone());
242 }
243 }
244 Visit::sets(dirs, files)
245}
246
247#[derive(Clone, Debug)]
249pub struct GlobsMatcher {
250 tree: RepoPathTree<Option<regex::bytes::RegexSet>>,
251 matches_prefix_paths: bool,
252}
253
254impl GlobsMatcher {
255 pub fn builder<'a>() -> GlobsMatcherBuilder<'a> {
257 GlobsMatcherBuilder {
258 dir_patterns: vec![],
259 matches_prefix_paths: false,
260 }
261 }
262}
263
264impl Matcher for GlobsMatcher {
265 fn matches(&self, file: &RepoPath) -> bool {
266 self.tree
268 .walk_to(file)
269 .take_while(|(_, tail_path)| !tail_path.is_root()) .any(|(sub, tail_path)| {
271 let tail = tail_path.as_internal_file_string().as_bytes();
272 sub.value.as_ref().is_some_and(|pat| pat.is_match(tail))
273 })
274 }
275
276 fn visit(&self, dir: &RepoPath) -> Visit {
277 let mut max_visit = Visit::Nothing;
278 for (sub, tail_path) in self.tree.walk_to(dir) {
279 if let Some(pat) = &sub.value {
281 let tail = tail_path.as_internal_file_string().as_bytes();
282 if self.matches_prefix_paths && pat.is_match(tail) {
283 return Visit::AllRecursively;
285 } else {
286 max_visit = Visit::SOME;
287 }
288 if !self.matches_prefix_paths {
289 break; }
291 }
292 if tail_path.is_root() && max_visit == Visit::Nothing {
294 let sub_dirs = sub.entries.keys().cloned().collect();
295 return Visit::sets(sub_dirs, HashSet::new());
296 }
297 }
298 max_visit
299 }
300}
301
302#[derive(Clone, Debug)]
304pub struct GlobsMatcherBuilder<'a> {
305 dir_patterns: Vec<(&'a RepoPath, &'a Glob)>,
306 matches_prefix_paths: bool,
307}
308
309impl<'a> GlobsMatcherBuilder<'a> {
310 pub fn prefix_paths(mut self, yes: bool) -> Self {
312 self.matches_prefix_paths = yes;
313 self
314 }
315
316 pub fn is_empty(&self) -> bool {
318 self.dir_patterns.is_empty()
319 }
320
321 pub fn add(&mut self, dir: &'a RepoPath, pattern: &'a Glob) {
326 self.dir_patterns.push((dir, pattern));
327 }
328
329 pub fn build(self) -> GlobsMatcher {
331 let Self {
332 mut dir_patterns,
333 matches_prefix_paths,
334 } = self;
335 dir_patterns.sort_unstable_by_key(|&(dir, _)| dir);
336
337 let mut tree: RepoPathTree<Option<regex::bytes::RegexSet>> = Default::default();
338 for (dir, chunk) in &dir_patterns.into_iter().chunk_by(|&(dir, _)| dir) {
339 let mut regex_builder = if matches_prefix_paths {
342 let regex_patterns = chunk.map(|(_, pattern)| glob_to_prefix_regex(pattern));
343 regex::bytes::RegexSetBuilder::new(regex_patterns)
344 } else {
345 regex::bytes::RegexSetBuilder::new(chunk.map(|(_, pattern)| pattern.regex()))
346 };
347 let regex = regex_builder
348 .dot_matches_new_line(true)
349 .build()
350 .expect("glob regex should be valid");
351 let sub = tree.add(dir);
352 assert!(sub.value.is_none());
353 sub.value = Some(regex);
354 }
355
356 GlobsMatcher {
357 tree,
358 matches_prefix_paths,
359 }
360 }
361}
362
363fn glob_to_prefix_regex(glob: &Glob) -> String {
364 let prefix = glob
368 .regex()
369 .strip_suffix('$')
370 .expect("glob regex should be anchored");
371 format!("{prefix}(?:/|$)")
372}
373
374#[derive(Clone, Debug)]
376pub struct UnionMatcher<M1, M2> {
377 input1: M1,
378 input2: M2,
379}
380
381impl<M1: Matcher, M2: Matcher> UnionMatcher<M1, M2> {
382 pub fn new(input1: M1, input2: M2) -> Self {
383 Self { input1, input2 }
384 }
385}
386
387impl<M1: Matcher, M2: Matcher> Matcher for UnionMatcher<M1, M2> {
388 fn matches(&self, file: &RepoPath) -> bool {
389 self.input1.matches(file) || self.input2.matches(file)
390 }
391
392 fn visit(&self, dir: &RepoPath) -> Visit {
393 match self.input1.visit(dir) {
394 Visit::AllRecursively => Visit::AllRecursively,
395 Visit::Nothing => self.input2.visit(dir),
396 Visit::Specific {
397 dirs: dirs1,
398 files: files1,
399 } => match self.input2.visit(dir) {
400 Visit::AllRecursively => Visit::AllRecursively,
401 Visit::Nothing => Visit::Specific {
402 dirs: dirs1,
403 files: files1,
404 },
405 Visit::Specific {
406 dirs: dirs2,
407 files: files2,
408 } => {
409 let dirs = match (dirs1, dirs2) {
410 (VisitDirs::All, _) | (_, VisitDirs::All) => VisitDirs::All,
411 (VisitDirs::Set(dirs1), VisitDirs::Set(dirs2)) => {
412 VisitDirs::Set(dirs1.iter().chain(&dirs2).cloned().collect())
413 }
414 };
415 let files = match (files1, files2) {
416 (VisitFiles::All, _) | (_, VisitFiles::All) => VisitFiles::All,
417 (VisitFiles::Set(files1), VisitFiles::Set(files2)) => {
418 VisitFiles::Set(files1.iter().chain(&files2).cloned().collect())
419 }
420 };
421 Visit::Specific { dirs, files }
422 }
423 },
424 }
425 }
426}
427
428#[derive(Clone, Debug)]
431pub struct DifferenceMatcher<M1, M2> {
432 wanted: M1,
434 unwanted: M2,
436}
437
438impl<M1: Matcher, M2: Matcher> DifferenceMatcher<M1, M2> {
439 pub fn new(wanted: M1, unwanted: M2) -> Self {
440 Self { wanted, unwanted }
441 }
442}
443
444impl<M1: Matcher, M2: Matcher> Matcher for DifferenceMatcher<M1, M2> {
445 fn matches(&self, file: &RepoPath) -> bool {
446 self.wanted.matches(file) && !self.unwanted.matches(file)
447 }
448
449 fn visit(&self, dir: &RepoPath) -> Visit {
450 match self.unwanted.visit(dir) {
451 Visit::AllRecursively => Visit::Nothing,
452 Visit::Nothing => self.wanted.visit(dir),
453 Visit::Specific { .. } => match self.wanted.visit(dir) {
454 Visit::AllRecursively => Visit::SOME,
455 wanted_visit => wanted_visit,
456 },
457 }
458 }
459}
460
461#[derive(Clone, Debug)]
463pub struct IntersectionMatcher<M1, M2> {
464 input1: M1,
465 input2: M2,
466}
467
468impl<M1: Matcher, M2: Matcher> IntersectionMatcher<M1, M2> {
469 pub fn new(input1: M1, input2: M2) -> Self {
470 Self { input1, input2 }
471 }
472}
473
474impl<M1: Matcher, M2: Matcher> Matcher for IntersectionMatcher<M1, M2> {
475 fn matches(&self, file: &RepoPath) -> bool {
476 self.input1.matches(file) && self.input2.matches(file)
477 }
478
479 fn visit(&self, dir: &RepoPath) -> Visit {
480 match self.input1.visit(dir) {
481 Visit::AllRecursively => self.input2.visit(dir),
482 Visit::Nothing => Visit::Nothing,
483 Visit::Specific {
484 dirs: dirs1,
485 files: files1,
486 } => match self.input2.visit(dir) {
487 Visit::AllRecursively => Visit::Specific {
488 dirs: dirs1,
489 files: files1,
490 },
491 Visit::Nothing => Visit::Nothing,
492 Visit::Specific {
493 dirs: dirs2,
494 files: files2,
495 } => {
496 let dirs = match (dirs1, dirs2) {
497 (VisitDirs::All, VisitDirs::All) => VisitDirs::All,
498 (dirs1, VisitDirs::All) => dirs1,
499 (VisitDirs::All, dirs2) => dirs2,
500 (VisitDirs::Set(dirs1), VisitDirs::Set(dirs2)) => {
501 VisitDirs::Set(dirs1.intersection(&dirs2).cloned().collect())
502 }
503 };
504 let files = match (files1, files2) {
505 (VisitFiles::All, VisitFiles::All) => VisitFiles::All,
506 (files1, VisitFiles::All) => files1,
507 (VisitFiles::All, files2) => files2,
508 (VisitFiles::Set(files1), VisitFiles::Set(files2)) => {
509 VisitFiles::Set(files1.intersection(&files2).cloned().collect())
510 }
511 };
512 match (&dirs, &files) {
513 (VisitDirs::Set(dirs), VisitFiles::Set(files))
514 if dirs.is_empty() && files.is_empty() =>
515 {
516 Visit::Nothing
517 }
518 _ => Visit::Specific { dirs, files },
519 }
520 }
521 },
522 }
523 }
524}
525
526#[derive(Clone, Default, Eq, PartialEq)]
528struct RepoPathTree<V> {
529 entries: HashMap<RepoPathComponentBuf, Self>,
530 value: V,
531}
532
533impl<V> RepoPathTree<V> {
534 fn add(&mut self, dir: &RepoPath) -> &mut Self
535 where
536 V: Default,
537 {
538 dir.components().fold(self, |sub, name| {
539 if !sub.entries.contains_key(name) {
541 sub.entries.insert(name.to_owned(), Self::default());
542 }
543 sub.entries.get_mut(name).unwrap()
544 })
545 }
546
547 fn get(&self, dir: &RepoPath) -> Option<&Self> {
548 dir.components()
549 .try_fold(self, |sub, name| sub.entries.get(name))
550 }
551
552 fn walk_to<'a, 'b>(
555 &'a self,
556 dir: &'b RepoPath,
557 ) -> impl Iterator<Item = (&'a Self, &'b RepoPath)> {
558 iter::successors(Some((self, dir)), |(sub, dir)| {
559 let mut components = dir.components();
560 let name = components.next()?;
561 Some((sub.entries.get(name)?, components.as_path()))
562 })
563 }
564}
565
566impl<V: Debug> Debug for RepoPathTree<V> {
567 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
568 self.value.fmt(f)?;
569 f.write_str(" ")?;
570 f.debug_map()
571 .entries(
572 self.entries
573 .iter()
574 .sorted_unstable_by_key(|&(name, _)| name),
575 )
576 .finish()
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use maplit::hashset;
583
584 use super::*;
585 use crate::fileset::parse_file_glob;
586
587 fn repo_path(value: &str) -> &RepoPath {
588 RepoPath::from_internal_string(value).unwrap()
589 }
590
591 fn repo_path_component_buf(value: &str) -> RepoPathComponentBuf {
592 RepoPathComponentBuf::new(value).unwrap()
593 }
594
595 fn glob(s: &str) -> Glob {
596 let icase = false;
597 parse_file_glob(s, icase).unwrap()
598 }
599
600 fn new_file_globs_matcher(dir_patterns: &[(&RepoPath, Glob)]) -> GlobsMatcher {
601 let mut builder = GlobsMatcher::builder();
602 for (dir, pattern) in dir_patterns {
603 builder.add(dir, pattern);
604 }
605 builder.build()
606 }
607
608 fn new_prefix_globs_matcher(dir_patterns: &[(&RepoPath, Glob)]) -> GlobsMatcher {
609 let mut builder = GlobsMatcher::builder().prefix_paths(true);
610 for (dir, pattern) in dir_patterns {
611 builder.add(dir, pattern);
612 }
613 builder.build()
614 }
615
616 #[test]
617 fn test_nothing_matcher() {
618 let m = NothingMatcher;
619 assert!(!m.matches(repo_path("file")));
620 assert!(!m.matches(repo_path("dir/file")));
621 assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
622 }
623
624 #[test]
625 fn test_files_matcher_empty() {
626 let m = FilesMatcher::new([] as [&RepoPath; 0]);
627 assert!(!m.matches(repo_path("file")));
628 assert!(!m.matches(repo_path("dir/file")));
629 assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
630 }
631
632 #[test]
633 fn test_files_matcher_nonempty() {
634 let m = FilesMatcher::new([
635 repo_path("dir1/subdir1/file1"),
636 repo_path("dir1/subdir1/file2"),
637 repo_path("dir1/subdir2/file3"),
638 repo_path("file4"),
639 ]);
640
641 assert!(!m.matches(repo_path("dir1")));
642 assert!(!m.matches(repo_path("dir1/subdir1")));
643 assert!(m.matches(repo_path("dir1/subdir1/file1")));
644 assert!(m.matches(repo_path("dir1/subdir1/file2")));
645 assert!(!m.matches(repo_path("dir1/subdir1/file3")));
646
647 assert_eq!(
648 m.visit(RepoPath::root()),
649 Visit::sets(
650 hashset! {repo_path_component_buf("dir1")},
651 hashset! {repo_path_component_buf("file4")}
652 )
653 );
654 assert_eq!(
655 m.visit(repo_path("dir1")),
656 Visit::sets(
657 hashset! {
658 repo_path_component_buf("subdir1"),
659 repo_path_component_buf("subdir2"),
660 },
661 hashset! {}
662 )
663 );
664 assert_eq!(
665 m.visit(repo_path("dir1/subdir1")),
666 Visit::sets(
667 hashset! {},
668 hashset! {
669 repo_path_component_buf("file1"),
670 repo_path_component_buf("file2"),
671 },
672 )
673 );
674 assert_eq!(
675 m.visit(repo_path("dir1/subdir2")),
676 Visit::sets(hashset! {}, hashset! {repo_path_component_buf("file3")})
677 );
678 }
679
680 #[test]
681 fn test_prefix_matcher_empty() {
682 let m = PrefixMatcher::new([] as [&RepoPath; 0]);
683 assert!(!m.matches(repo_path("file")));
684 assert!(!m.matches(repo_path("dir/file")));
685 assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
686 }
687
688 #[test]
689 fn test_prefix_matcher_root() {
690 let m = PrefixMatcher::new([RepoPath::root()]);
691 assert!(m.matches(repo_path("file")));
693 assert!(m.matches(repo_path("dir/file")));
694 assert_eq!(m.visit(RepoPath::root()), Visit::AllRecursively);
696 assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
697 }
698
699 #[test]
700 fn test_prefix_matcher_single_prefix() {
701 let m = PrefixMatcher::new([repo_path("foo/bar")]);
702
703 assert!(!m.matches(repo_path("foo")));
705 assert!(!m.matches(repo_path("bar")));
706 assert!(m.matches(repo_path("foo/bar")));
708 assert!(m.matches(repo_path("foo/bar/baz")));
710 assert!(m.matches(repo_path("foo/bar/baz/qux")));
711 assert!(!m.matches(repo_path("foo/foo")));
713 assert!(!m.matches(repo_path("bar/foo/bar")));
715
716 assert_eq!(
719 m.visit(RepoPath::root()),
720 Visit::sets(hashset! {repo_path_component_buf("foo")}, hashset! {})
721 );
722 assert_eq!(
725 m.visit(repo_path("foo")),
726 Visit::sets(
727 hashset! {repo_path_component_buf("bar")},
728 hashset! {repo_path_component_buf("bar")}
729 )
730 );
731 assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
733 assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
735 assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
738 }
739
740 #[test]
741 fn test_prefix_matcher_nested_prefixes() {
742 let m = PrefixMatcher::new([repo_path("foo"), repo_path("foo/bar/baz")]);
743
744 assert!(m.matches(repo_path("foo")));
745 assert!(!m.matches(repo_path("bar")));
746 assert!(m.matches(repo_path("foo/bar")));
747 assert!(m.matches(repo_path("foo/baz/foo")));
749
750 assert_eq!(
751 m.visit(RepoPath::root()),
752 Visit::sets(
753 hashset! {repo_path_component_buf("foo")},
754 hashset! {repo_path_component_buf("foo")}
755 )
756 );
757 assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
759 assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
761 }
762
763 #[test]
764 fn test_file_globs_matcher_rooted() {
765 let m = new_file_globs_matcher(&[(RepoPath::root(), glob("*.rs"))]);
766 assert!(!m.matches(repo_path("foo")));
767 assert!(m.matches(repo_path("foo.rs")));
768 assert!(m.matches(repo_path("foo\n.rs"))); assert!(!m.matches(repo_path("foo.rss")));
770 assert!(!m.matches(repo_path("foo.rs/bar.rs")));
771 assert!(!m.matches(repo_path("foo/bar.rs")));
772 assert_eq!(m.visit(RepoPath::root()), Visit::SOME);
773
774 let m = new_file_globs_matcher(&[
776 (RepoPath::root(), glob("foo?")),
777 (repo_path("other"), glob("")),
778 (RepoPath::root(), glob("**/*.rs")),
779 ]);
780 assert!(!m.matches(repo_path("foo")));
781 assert!(m.matches(repo_path("foo1")));
782 assert!(!m.matches(repo_path("Foo1")));
783 assert!(!m.matches(repo_path("foo1/foo2")));
784 assert!(m.matches(repo_path("foo.rs")));
785 assert!(m.matches(repo_path("foo.rs/bar.rs")));
786 assert!(m.matches(repo_path("foo/bar.rs")));
787 assert_eq!(m.visit(RepoPath::root()), Visit::SOME);
788 assert_eq!(m.visit(repo_path("foo")), Visit::SOME);
789 assert_eq!(m.visit(repo_path("bar/baz")), Visit::SOME);
790 }
791
792 #[test]
793 fn test_file_globs_matcher_nested() {
794 let m = new_file_globs_matcher(&[
795 (repo_path("foo"), glob("**/*.a")),
796 (repo_path("foo/bar"), glob("*.b")),
797 (repo_path("baz"), glob("?*")),
798 ]);
799 assert!(!m.matches(repo_path("foo")));
800 assert!(m.matches(repo_path("foo/x.a")));
801 assert!(!m.matches(repo_path("foo/x.b")));
802 assert!(m.matches(repo_path("foo/bar/x.a")));
803 assert!(m.matches(repo_path("foo/bar/x.b")));
804 assert!(m.matches(repo_path("foo/bar/baz/x.a")));
805 assert!(!m.matches(repo_path("foo/bar/baz/x.b")));
806 assert!(!m.matches(repo_path("baz")));
807 assert!(m.matches(repo_path("baz/x")));
808 assert_eq!(
809 m.visit(RepoPath::root()),
810 Visit::Specific {
811 dirs: VisitDirs::Set(hashset! {
812 repo_path_component_buf("foo"),
813 repo_path_component_buf("baz"),
814 }),
815 files: VisitFiles::Set(hashset! {}),
816 }
817 );
818 assert_eq!(m.visit(repo_path("foo")), Visit::SOME);
819 assert_eq!(m.visit(repo_path("foo/bar")), Visit::SOME);
820 assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::SOME);
821 assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
822 assert_eq!(m.visit(repo_path("baz")), Visit::SOME);
823 }
824
825 #[test]
826 fn test_file_globs_matcher_wildcard_any() {
827 let m = new_file_globs_matcher(&[(RepoPath::root(), glob("*"))]);
833 assert!(!m.matches(RepoPath::root()));
834 assert!(m.matches(repo_path("x")));
835 assert!(m.matches(repo_path("x.rs")));
836 assert!(!m.matches(repo_path("foo/bar.rs")));
837 assert_eq!(m.visit(RepoPath::root()), Visit::SOME);
838
839 let m = new_file_globs_matcher(&[(repo_path("foo"), glob("*"))]);
841 assert!(!m.matches(RepoPath::root()));
842 assert!(!m.matches(repo_path("foo")));
843 assert!(m.matches(repo_path("foo/x")));
844 assert!(!m.matches(repo_path("foo/bar/baz")));
845 assert_eq!(
846 m.visit(RepoPath::root()),
847 Visit::Specific {
848 dirs: VisitDirs::Set(hashset! {repo_path_component_buf("foo")}),
849 files: VisitFiles::Set(hashset! {}),
850 }
851 );
852 assert_eq!(m.visit(repo_path("foo")), Visit::SOME);
853 assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
854 }
855
856 #[test]
857 fn test_prefix_globs_matcher_rooted() {
858 let m = new_prefix_globs_matcher(&[(RepoPath::root(), glob("*.rs"))]);
859 assert!(!m.matches(repo_path("foo")));
860 assert!(m.matches(repo_path("foo.rs")));
861 assert!(m.matches(repo_path("foo\n.rs"))); assert!(!m.matches(repo_path("foo.rss")));
863 assert!(m.matches(repo_path("foo.rs/bar")));
864 assert!(!m.matches(repo_path("foo/bar.rs")));
865 assert_eq!(m.visit(RepoPath::root()), Visit::SOME);
866 assert_eq!(m.visit(repo_path("foo.rs")), Visit::AllRecursively);
867 assert_eq!(m.visit(repo_path("foo.rs/bar")), Visit::AllRecursively);
868 assert_eq!(m.visit(repo_path("foo.rss")), Visit::SOME);
869 assert_eq!(m.visit(repo_path("foo.rss/bar")), Visit::SOME);
870
871 let m = new_prefix_globs_matcher(&[
873 (RepoPath::root(), glob("foo?")),
874 (repo_path("other"), glob("")),
875 (RepoPath::root(), glob("**/*.rs")),
876 ]);
877 assert!(!m.matches(repo_path("foo")));
878 assert!(m.matches(repo_path("foo1")));
879 assert!(!m.matches(repo_path("Foo1")));
880 assert!(m.matches(repo_path("foo1/foo2")));
881 assert!(m.matches(repo_path("foo.rs")));
882 assert!(m.matches(repo_path("foo.rs/bar.rs")));
883 assert!(m.matches(repo_path("foo/bar.rs")));
884 assert_eq!(m.visit(RepoPath::root()), Visit::SOME);
885 assert_eq!(m.visit(repo_path("foo")), Visit::SOME);
886 assert_eq!(m.visit(repo_path("bar/baz")), Visit::SOME);
887 }
888
889 #[test]
890 fn test_prefix_globs_matcher_nested() {
891 let m = new_prefix_globs_matcher(&[
892 (repo_path("foo"), glob("**/*.a")),
893 (repo_path("foo/bar"), glob("*.b")),
894 (repo_path("baz"), glob("?*")),
895 ]);
896 assert!(!m.matches(repo_path("foo")));
897 assert!(m.matches(repo_path("foo/x.a")));
898 assert!(!m.matches(repo_path("foo/x.b")));
899 assert!(m.matches(repo_path("foo/bar/x.a")));
900 assert!(m.matches(repo_path("foo/bar/x.b")));
901 assert!(m.matches(repo_path("foo/bar/x.b/y")));
902 assert!(m.matches(repo_path("foo/bar/baz/x.a")));
903 assert!(!m.matches(repo_path("foo/bar/baz/x.b")));
904 assert!(!m.matches(repo_path("baz")));
905 assert!(m.matches(repo_path("baz/x")));
906 assert!(m.matches(repo_path("baz/x/y")));
907 assert_eq!(
908 m.visit(RepoPath::root()),
909 Visit::Specific {
910 dirs: VisitDirs::Set(hashset! {
911 repo_path_component_buf("foo"),
912 repo_path_component_buf("baz"),
913 }),
914 files: VisitFiles::Set(hashset! {}),
915 }
916 );
917 assert_eq!(m.visit(repo_path("foo")), Visit::SOME);
918 assert_eq!(m.visit(repo_path("foo/x.a")), Visit::AllRecursively);
919 assert_eq!(m.visit(repo_path("foo/bar")), Visit::SOME);
920 assert_eq!(m.visit(repo_path("foo/bar/x.a")), Visit::AllRecursively);
921 assert_eq!(m.visit(repo_path("foo/bar/x.b")), Visit::AllRecursively);
922 assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::SOME);
923 assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
924 assert_eq!(m.visit(repo_path("baz")), Visit::SOME);
925 assert_eq!(m.visit(repo_path("baz/x")), Visit::AllRecursively);
926 assert_eq!(m.visit(repo_path("baz/x/y")), Visit::AllRecursively);
927 }
928
929 #[test]
930 fn test_prefix_globs_matcher_wildcard_any() {
931 let m = new_prefix_globs_matcher(&[(RepoPath::root(), glob("*"))]);
937 assert!(!m.matches(RepoPath::root()));
938 assert!(m.matches(repo_path("x")));
939 assert!(m.matches(repo_path("x.rs")));
940 assert!(m.matches(repo_path("foo/bar.rs")));
941 assert_eq!(m.visit(RepoPath::root()), Visit::AllRecursively);
942
943 let m = new_prefix_globs_matcher(&[(repo_path("foo"), glob("*"))]);
945 assert!(!m.matches(RepoPath::root()));
946 assert!(!m.matches(repo_path("foo")));
947 assert!(m.matches(repo_path("foo/x")));
948 assert!(m.matches(repo_path("foo/bar/baz")));
949 assert_eq!(
950 m.visit(RepoPath::root()),
951 Visit::Specific {
952 dirs: VisitDirs::Set(hashset! {repo_path_component_buf("foo")}),
953 files: VisitFiles::Set(hashset! {}),
954 }
955 );
956 assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
957 assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
958 }
959
960 #[test]
961 fn test_prefix_globs_matcher_wildcard_suffix() {
962 let m = new_prefix_globs_matcher(&[(repo_path("foo"), glob("**"))]);
964 assert!(!m.matches(repo_path("foo")));
965 assert!(m.matches(repo_path("foo/bar")));
966 assert!(m.matches(repo_path("foo/bar/baz")));
967 assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
968 assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
969 assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
970 }
971
972 #[test]
973 fn test_union_matcher_concatenate_roots() {
974 let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
975 let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]);
976 let m = UnionMatcher::new(&m1, &m2);
977
978 assert!(m.matches(repo_path("foo")));
979 assert!(m.matches(repo_path("foo/bar")));
980 assert!(m.matches(repo_path("bar")));
981 assert!(m.matches(repo_path("bar/foo")));
982 assert!(m.matches(repo_path("baz")));
983 assert!(m.matches(repo_path("baz/foo")));
984 assert!(!m.matches(repo_path("qux")));
985 assert!(!m.matches(repo_path("qux/foo")));
986
987 assert_eq!(
988 m.visit(RepoPath::root()),
989 Visit::sets(
990 hashset! {
991 repo_path_component_buf("foo"),
992 repo_path_component_buf("bar"),
993 repo_path_component_buf("baz"),
994 },
995 hashset! {
996 repo_path_component_buf("foo"),
997 repo_path_component_buf("bar"),
998 repo_path_component_buf("baz"),
999 },
1000 )
1001 );
1002 assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
1003 assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
1004 assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
1005 assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
1006 assert_eq!(m.visit(repo_path("baz")), Visit::AllRecursively);
1007 assert_eq!(m.visit(repo_path("baz/foo")), Visit::AllRecursively);
1008 assert_eq!(m.visit(repo_path("qux")), Visit::Nothing);
1009 assert_eq!(m.visit(repo_path("qux/foo")), Visit::Nothing);
1010 }
1011
1012 #[test]
1013 fn test_union_matcher_concatenate_subdirs() {
1014 let m1 = PrefixMatcher::new([repo_path("common/bar"), repo_path("1/foo")]);
1015 let m2 = PrefixMatcher::new([repo_path("common/baz"), repo_path("2/qux")]);
1016 let m = UnionMatcher::new(&m1, &m2);
1017
1018 assert!(!m.matches(repo_path("common")));
1019 assert!(!m.matches(repo_path("1")));
1020 assert!(!m.matches(repo_path("2")));
1021 assert!(m.matches(repo_path("common/bar")));
1022 assert!(m.matches(repo_path("common/bar/baz")));
1023 assert!(m.matches(repo_path("common/baz")));
1024 assert!(m.matches(repo_path("1/foo")));
1025 assert!(m.matches(repo_path("1/foo/qux")));
1026 assert!(m.matches(repo_path("2/qux")));
1027 assert!(!m.matches(repo_path("2/quux")));
1028
1029 assert_eq!(
1030 m.visit(RepoPath::root()),
1031 Visit::sets(
1032 hashset! {
1033 repo_path_component_buf("common"),
1034 repo_path_component_buf("1"),
1035 repo_path_component_buf("2"),
1036 },
1037 hashset! {},
1038 )
1039 );
1040 assert_eq!(
1041 m.visit(repo_path("common")),
1042 Visit::sets(
1043 hashset! {
1044 repo_path_component_buf("bar"),
1045 repo_path_component_buf("baz"),
1046 },
1047 hashset! {
1048 repo_path_component_buf("bar"),
1049 repo_path_component_buf("baz"),
1050 },
1051 )
1052 );
1053 assert_eq!(
1054 m.visit(repo_path("1")),
1055 Visit::sets(
1056 hashset! {repo_path_component_buf("foo")},
1057 hashset! {repo_path_component_buf("foo")},
1058 )
1059 );
1060 assert_eq!(
1061 m.visit(repo_path("2")),
1062 Visit::sets(
1063 hashset! {repo_path_component_buf("qux")},
1064 hashset! {repo_path_component_buf("qux")},
1065 )
1066 );
1067 assert_eq!(m.visit(repo_path("common/bar")), Visit::AllRecursively);
1068 assert_eq!(m.visit(repo_path("1/foo")), Visit::AllRecursively);
1069 assert_eq!(m.visit(repo_path("2/qux")), Visit::AllRecursively);
1070 assert_eq!(m.visit(repo_path("2/quux")), Visit::Nothing);
1071 }
1072
1073 #[test]
1074 fn test_difference_matcher_remove_subdir() {
1075 let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
1076 let m2 = PrefixMatcher::new([repo_path("foo/bar")]);
1077 let m = DifferenceMatcher::new(&m1, &m2);
1078
1079 assert!(m.matches(repo_path("foo")));
1080 assert!(!m.matches(repo_path("foo/bar")));
1081 assert!(!m.matches(repo_path("foo/bar/baz")));
1082 assert!(m.matches(repo_path("foo/baz")));
1083 assert!(m.matches(repo_path("bar")));
1084
1085 assert_eq!(
1086 m.visit(RepoPath::root()),
1087 Visit::sets(
1088 hashset! {
1089 repo_path_component_buf("foo"),
1090 repo_path_component_buf("bar"),
1091 },
1092 hashset! {
1093 repo_path_component_buf("foo"),
1094 repo_path_component_buf("bar"),
1095 },
1096 )
1097 );
1098 assert_eq!(m.visit(repo_path("foo")), Visit::SOME);
1099 assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
1100 assert_eq!(m.visit(repo_path("foo/baz")), Visit::AllRecursively);
1101 assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
1102 }
1103
1104 #[test]
1105 fn test_difference_matcher_shared_patterns() {
1106 let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
1107 let m2 = PrefixMatcher::new([repo_path("foo")]);
1108 let m = DifferenceMatcher::new(&m1, &m2);
1109
1110 assert!(!m.matches(repo_path("foo")));
1111 assert!(!m.matches(repo_path("foo/bar")));
1112 assert!(m.matches(repo_path("bar")));
1113 assert!(m.matches(repo_path("bar/foo")));
1114
1115 assert_eq!(
1116 m.visit(RepoPath::root()),
1117 Visit::sets(
1118 hashset! {
1119 repo_path_component_buf("foo"),
1120 repo_path_component_buf("bar"),
1121 },
1122 hashset! {
1123 repo_path_component_buf("foo"),
1124 repo_path_component_buf("bar"),
1125 },
1126 )
1127 );
1128 assert_eq!(m.visit(repo_path("foo")), Visit::Nothing);
1129 assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
1130 assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
1131 assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
1132 }
1133
1134 #[test]
1135 fn test_intersection_matcher_intersecting_roots() {
1136 let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
1137 let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]);
1138 let m = IntersectionMatcher::new(&m1, &m2);
1139
1140 assert!(!m.matches(repo_path("foo")));
1141 assert!(!m.matches(repo_path("foo/bar")));
1142 assert!(m.matches(repo_path("bar")));
1143 assert!(m.matches(repo_path("bar/foo")));
1144 assert!(!m.matches(repo_path("baz")));
1145 assert!(!m.matches(repo_path("baz/foo")));
1146
1147 assert_eq!(
1148 m.visit(RepoPath::root()),
1149 Visit::sets(
1150 hashset! {repo_path_component_buf("bar")},
1151 hashset! {repo_path_component_buf("bar")}
1152 )
1153 );
1154 assert_eq!(m.visit(repo_path("foo")), Visit::Nothing);
1155 assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
1156 assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
1157 assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
1158 assert_eq!(m.visit(repo_path("baz")), Visit::Nothing);
1159 assert_eq!(m.visit(repo_path("baz/foo")), Visit::Nothing);
1160 }
1161
1162 #[test]
1163 fn test_intersection_matcher_subdir() {
1164 let m1 = PrefixMatcher::new([repo_path("foo")]);
1165 let m2 = PrefixMatcher::new([repo_path("foo/bar")]);
1166 let m = IntersectionMatcher::new(&m1, &m2);
1167
1168 assert!(!m.matches(repo_path("foo")));
1169 assert!(!m.matches(repo_path("bar")));
1170 assert!(m.matches(repo_path("foo/bar")));
1171 assert!(m.matches(repo_path("foo/bar/baz")));
1172 assert!(!m.matches(repo_path("foo/baz")));
1173
1174 assert_eq!(
1175 m.visit(RepoPath::root()),
1176 Visit::sets(hashset! {repo_path_component_buf("foo")}, hashset! {})
1177 );
1178 assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
1179 assert_eq!(
1180 m.visit(repo_path("foo")),
1181 Visit::sets(
1182 hashset! {repo_path_component_buf("bar")},
1183 hashset! {repo_path_component_buf("bar")}
1184 )
1185 );
1186 assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
1187 }
1188}