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