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