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