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