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