Skip to main content

jj_core/
matchers.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Contains the [`Matcher`] trait which is used for matching against
16//! [`RepoPath`]s and guiding walks of directory trees.
17
18use 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/// Describes which tree entries need to be visited.
32#[derive(PartialEq, Eq, Debug)]
33pub enum Visit {
34    /// Everything in the directory is *guaranteed* to match, no need to check
35    /// descendants
36    AllRecursively,
37    /// Visit only the specified directories or files.
38    Specific {
39        /// Visit these specific directories.
40        dirs: VisitDirs,
41        /// Visit these specific files.
42        files: VisitFiles,
43    },
44    /// Nothing in the directory or its subdirectories will match.
45    ///
46    /// This is the same as `Specific` with no directories or files. Use
47    /// `Visit::set()` to get create an instance that's `Specific` or
48    /// `Nothing` depending on the values at runtime.
49    Nothing,
50}
51
52impl Visit {
53    /// All entries in the directory need to be visited, but they are not
54    /// guaranteed to match.
55    const SOME: Self = Self::Specific {
56        dirs: VisitDirs::All,
57        files: VisitFiles::All,
58    };
59
60    /// Visit these sets of `dirs` and `files`.
61    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    /// Returns true if nothing is matched.
73    pub fn is_nothing(&self) -> bool {
74        *self == Self::Nothing
75    }
76}
77
78/// Describes which subdirectories to visit.
79#[derive(PartialEq, Eq, Debug)]
80pub enum VisitDirs {
81    /// Visit all possible directories.
82    All,
83    /// Visit the specified set of directories.
84    Set(HashSet<RepoPathComponentBuf>),
85}
86
87/// Describes which files to visit.
88#[derive(PartialEq, Eq, Debug)]
89pub enum VisitFiles {
90    /// Visit all possible files.
91    All,
92    /// Visit the specified set of files.
93    Set(HashSet<RepoPathComponentBuf>),
94}
95
96/// A [`Matcher`] matches against `RepoPath`s and helps guide a traversal of a
97/// directory files.
98pub trait Matcher: Debug + Send + Sync {
99    /// Returns true if the `file` matches the path.
100    fn matches(&self, file: &RepoPath) -> bool;
101    /// Returns a `Visit` which specifies how further traversal should commence.
102    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/// Matches no paths.
126#[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/// Matches all paths.
140#[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/// Matches the specified files.
154#[derive(PartialEq, Eq, Debug)]
155pub struct FilesMatcher {
156    tree: RepoPathTree<FilesNodeKind>,
157}
158
159impl FilesMatcher {
160    /// Create a new `FilesMatcher` for the given `files`.
161    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    /// Represents an intermediate directory.
187    #[default]
188    Dir,
189    /// Represents a file (which might also be an intermediate directory.)
190    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        // should visit only intermediate directories
198        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/// Matches paths on the given prefixes.
209#[derive(Debug)]
210pub struct PrefixMatcher {
211    tree: RepoPathTree<PrefixNodeKind>,
212}
213
214impl PrefixMatcher {
215    /// Create a new `PrefixMatcher` for the given `prefixes`.
216    #[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            // ancestor of 'dir' matches prefix paths
236            if *sub.value() == PrefixNodeKind::Prefix {
237                return Visit::AllRecursively;
238            }
239            // 'dir' found, and is an ancestor of prefix paths
240            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    /// Represents an intermediate directory.
251    #[default]
252    Dir,
253    /// Represents a file and prefix directory.
254    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        // should visit both intermediate and prefix directories
262        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/// Matches file or prefix paths with glob patterns.
271#[derive(Clone, Debug)]
272pub struct GlobsMatcher {
273    tree: RepoPathTree<Option<regex::bytes::RegexSet>>,
274    matches_prefix_paths: bool,
275}
276
277impl GlobsMatcher {
278    /// Returns new matcher builder.
279    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        // check if any ancestor (dir, patterns) matches 'file'
290        self.tree
291            .walk_to(file)
292            .take_while(|(_, tail_path)| !tail_path.is_root()) // only dirs
293            .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            // ancestor of 'dir' has patterns
303            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                    // 'dir' matches prefix patterns
307                    return Visit::AllRecursively;
308                } else {
309                    max_visit = Visit::SOME;
310                }
311                if !self.matches_prefix_paths {
312                    break; // can't narrow visit anymore
313                }
314            }
315            // 'dir' found, and is an ancestor of pattern paths
316            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/// Constructs [`GlobsMatcher`] from patterns.
326#[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    /// Whether or not the matcher will match prefix paths.
334    pub fn prefix_paths(mut self, yes: bool) -> Self {
335        self.matches_prefix_paths = yes;
336        self
337    }
338
339    /// Returns true if no patterns have been added yet.
340    pub fn is_empty(&self) -> bool {
341        self.dir_patterns.is_empty()
342    }
343
344    /// Adds `pattern` that should be evaluated relative to `dir`.
345    ///
346    /// The `dir` should be the longest directory path that contains no glob
347    /// meta characters.
348    pub fn add(&mut self, dir: &'a RepoPath, pattern: &'a PathGlobPattern) {
349        self.dir_patterns.push((dir, pattern));
350    }
351
352    /// Compiles matcher.
353    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            // Based on new_regex() in globset. We don't use GlobSet because
363            // RepoPath separator should be "/" on all platforms.
364            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/// Wrapper for a [`Glob`] parsed with `literal_separator = true`.
387#[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            // Omit opts and tokens
396            .finish_non_exhaustive()
397    }
398}
399
400impl PathGlobPattern {
401    /// Parses case-sensitive glob pattern.
402    pub fn parse(input: &str) -> Result<Self, globset::Error> {
403        Self::parse_inner(input, false)
404    }
405
406    /// Parses case-insensitive glob pattern.
407    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    /// Returns the original glob pattern.
420    pub fn as_str(&self) -> &str {
421        self.0.glob()
422    }
423
424    /// Returns the regular expression string for this glob.
425    pub fn as_regex(&self) -> &str {
426        self.0.regex()
427    }
428
429    fn to_prefix_regex(&self) -> String {
430        // Here we rely on the implementation detail of the globset crate.
431        // Alternatively, we can construct an anchored regex automaton and test
432        // prefix matching by feeding characters one by one.
433        let prefix = self
434            .0
435            .regex()
436            .strip_suffix('$')
437            .expect("glob regex should be anchored");
438        format!("{prefix}(?:/|$)")
439    }
440}
441
442/// Matches paths that are matched by any of the input matchers.
443#[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    /// Create a `UnionMatcher` matching when either of the inputs match.
451    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/// Matches paths that are matched by the first input matcher but not by the
498/// second.
499#[derive(Clone, Debug)]
500pub struct DifferenceMatcher<M1, M2> {
501    /// The minuend
502    wanted: M1,
503    /// The subtrahend
504    unwanted: M2,
505}
506
507impl<M1: Matcher, M2: Matcher> DifferenceMatcher<M1, M2> {
508    /// Create a new `DifferenceMatcher` matching when `wanted` matches and
509    /// `unwanted` does not.
510    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/// Matches paths that are matched by both input matchers.
533#[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    /// Create a `IntersectionMatcher` matching when both inputs match.
541    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        // No sub directories nor files will match
666        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        // Matches all files
732        assert!(m.matches(RepoPath::root()));
733        assert!(m.matches(repo_path("file")));
734        assert!(m.matches(repo_path("dir/file")));
735        // Visits all directories
736        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        // Parts of the prefix should not match
745        assert!(!m.matches(RepoPath::root()));
746        assert!(!m.matches(repo_path("foo")));
747        assert!(!m.matches(repo_path("bar")));
748        // A file matching the prefix exactly should match
749        assert!(m.matches(repo_path("foo/bar")));
750        // Files in subdirectories should match
751        assert!(m.matches(repo_path("foo/bar/baz")));
752        assert!(m.matches(repo_path("foo/bar/baz/qux")));
753        // Sibling files should not match
754        assert!(!m.matches(repo_path("foo/foo")));
755        // An unrooted "foo/bar" should not match
756        assert!(!m.matches(repo_path("bar/foo/bar")));
757
758        // The matcher should only visit directory foo/ in the root (file "foo"
759        // shouldn't be visited)
760        assert_eq!(
761            m.visit(RepoPath::root()),
762            Visit::sets(hashset! {repo_path_component_buf("foo")}, hashset! {})
763        );
764        // Inside parent directory "foo/", both subdirectory "bar" and file "bar" may
765        // match
766        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        // Inside a directory that matches the prefix, everything matches recursively
774        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
775        // Same thing in subdirectories of the prefix
776        assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
777        // Nothing in directories that are siblings of the prefix can match, so don't
778        // visit
779        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        // Matches because the "foo" pattern matches
790        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        // Inside a directory that matches the prefix, everything matches recursively
800        assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
801        // Same thing in subdirectories of the prefix
802        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"))); // "*" matches newline
811        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        // Multiple patterns at the same directory
817        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        // It's not obvious whether "*" should match the root directory path.
870        // Since "<dir>/*" shouldn't match "<dir>" itself, we can consider that
871        // "*" has an implicit "<root>/" prefix, and therefore it makes sense
872        // that "*" doesn't match the root. OTOH, if we compare paths as literal
873        // strings, "*" matches "". The current implementation is the former.
874        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        // "foo/*" shouldn't match "foo"
882        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"))); // "*" matches newline
904        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        // Multiple patterns at the same directory
914        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        // It's not obvious whether "*" should match the root directory path.
974        // Since "<dir>/*" shouldn't match "<dir>" itself, we can consider that
975        // "*" has an implicit "<root>/" prefix, and therefore it makes sense
976        // that "*" doesn't match the root. OTOH, if we compare paths as literal
977        // strings, "*" matches "". The current implementation is the former.
978        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        // "foo/*" shouldn't match "foo"
986        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        // explicit "/**" in pattern
1005        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}