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