jujutsu_lib/
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#![allow(dead_code)]
16
17use std::collections::{HashMap, HashSet};
18use std::iter;
19
20use crate::repo_path::{RepoPath, RepoPathComponent};
21
22#[derive(PartialEq, Eq, Debug)]
23pub enum Visit {
24    /// Everything in the directory is *guaranteed* to match, no need to check
25    /// descendants
26    AllRecursively,
27    Specific {
28        dirs: VisitDirs,
29        files: VisitFiles,
30    },
31    /// Nothing in the directory or its subdirectories will match.
32    ///
33    /// This is the same as `Specific` with no directories or files. Use
34    /// `Visit::set()` to get create an instance that's `Specific` or
35    /// `Nothing` depending on the values at runtime.
36    Nothing,
37}
38
39impl Visit {
40    fn sets(dirs: HashSet<RepoPathComponent>, files: HashSet<RepoPathComponent>) -> Self {
41        if dirs.is_empty() && files.is_empty() {
42            Self::Nothing
43        } else {
44            Self::Specific {
45                dirs: VisitDirs::Set(dirs),
46                files: VisitFiles::Set(files),
47            }
48        }
49    }
50
51    pub fn is_nothing(&self) -> bool {
52        *self == Visit::Nothing
53    }
54}
55
56#[derive(PartialEq, Eq, Debug)]
57pub enum VisitDirs {
58    All,
59    Set(HashSet<RepoPathComponent>),
60}
61
62#[derive(PartialEq, Eq, Debug)]
63pub enum VisitFiles {
64    All,
65    Set(HashSet<RepoPathComponent>),
66}
67
68pub trait Matcher {
69    fn matches(&self, file: &RepoPath) -> bool;
70    fn visit(&self, dir: &RepoPath) -> Visit;
71}
72
73#[derive(PartialEq, Eq, Debug)]
74pub struct NothingMatcher;
75
76impl Matcher for NothingMatcher {
77    fn matches(&self, _file: &RepoPath) -> bool {
78        false
79    }
80
81    fn visit(&self, _dir: &RepoPath) -> Visit {
82        Visit::Nothing
83    }
84}
85
86#[derive(PartialEq, Eq, Debug)]
87pub struct EverythingMatcher;
88
89impl Matcher for EverythingMatcher {
90    fn matches(&self, _file: &RepoPath) -> bool {
91        true
92    }
93
94    fn visit(&self, _dir: &RepoPath) -> Visit {
95        Visit::AllRecursively
96    }
97}
98
99#[derive(PartialEq, Eq, Debug)]
100pub struct FilesMatcher {
101    tree: RepoPathTree,
102}
103
104impl FilesMatcher {
105    pub fn new(files: &[RepoPath]) -> Self {
106        let mut tree = RepoPathTree::new();
107        for f in files {
108            tree.add_file(f);
109        }
110        FilesMatcher { tree }
111    }
112}
113
114impl Matcher for FilesMatcher {
115    fn matches(&self, file: &RepoPath) -> bool {
116        self.tree.get(file).map(|sub| sub.is_file).unwrap_or(false)
117    }
118
119    fn visit(&self, dir: &RepoPath) -> Visit {
120        self.tree.get_visit_sets(dir)
121    }
122}
123
124pub struct PrefixMatcher {
125    tree: RepoPathTree,
126}
127
128impl PrefixMatcher {
129    pub fn new(prefixes: &[RepoPath]) -> Self {
130        let mut tree = RepoPathTree::new();
131        for prefix in prefixes {
132            let sub = tree.add(prefix);
133            sub.is_dir = true;
134            sub.is_file = true;
135        }
136        PrefixMatcher { tree }
137    }
138}
139
140impl Matcher for PrefixMatcher {
141    fn matches(&self, file: &RepoPath) -> bool {
142        self.tree.walk_to(file).any(|(sub, _)| sub.is_file)
143    }
144
145    fn visit(&self, dir: &RepoPath) -> Visit {
146        for (sub, tail_components) in self.tree.walk_to(dir) {
147            // 'is_file' means the current path matches prefix paths
148            if sub.is_file {
149                return Visit::AllRecursively;
150            }
151            // 'dir' found, and is an ancestor of prefix paths
152            if tail_components.is_empty() {
153                return sub.to_visit_sets();
154            }
155        }
156        Visit::Nothing
157    }
158}
159
160/// Matches paths that are matched by the first input matcher but not by the
161/// second.
162pub struct DifferenceMatcher<'input> {
163    /// The minuend
164    wanted: &'input dyn Matcher,
165    /// The subtrahend
166    unwanted: &'input dyn Matcher,
167}
168
169impl<'input> DifferenceMatcher<'input> {
170    pub fn new(wanted: &'input dyn Matcher, unwanted: &'input dyn Matcher) -> Self {
171        Self { wanted, unwanted }
172    }
173}
174
175impl Matcher for DifferenceMatcher<'_> {
176    fn matches(&self, file: &RepoPath) -> bool {
177        self.wanted.matches(file) && !self.unwanted.matches(file)
178    }
179
180    fn visit(&self, dir: &RepoPath) -> Visit {
181        match self.unwanted.visit(dir) {
182            Visit::AllRecursively => Visit::Nothing,
183            Visit::Nothing => self.wanted.visit(dir),
184            Visit::Specific { .. } => match self.wanted.visit(dir) {
185                Visit::AllRecursively => Visit::Specific {
186                    dirs: VisitDirs::All,
187                    files: VisitFiles::All,
188                },
189                wanted_visit => wanted_visit,
190            },
191        }
192    }
193}
194
195/// Matches paths that are matched by both input matchers.
196pub struct IntersectionMatcher<'input> {
197    input1: &'input dyn Matcher,
198    input2: &'input dyn Matcher,
199}
200
201impl<'input> IntersectionMatcher<'input> {
202    pub fn new(input1: &'input dyn Matcher, input2: &'input dyn Matcher) -> Self {
203        Self { input1, input2 }
204    }
205}
206
207impl Matcher for IntersectionMatcher<'_> {
208    fn matches(&self, file: &RepoPath) -> bool {
209        self.input1.matches(file) && self.input2.matches(file)
210    }
211
212    fn visit(&self, dir: &RepoPath) -> Visit {
213        match self.input1.visit(dir) {
214            Visit::AllRecursively => self.input2.visit(dir),
215            Visit::Nothing => Visit::Nothing,
216            Visit::Specific {
217                dirs: dirs1,
218                files: files1,
219            } => match self.input2.visit(dir) {
220                Visit::AllRecursively => Visit::Specific {
221                    dirs: dirs1,
222                    files: files1,
223                },
224                Visit::Nothing => Visit::Nothing,
225                Visit::Specific {
226                    dirs: dirs2,
227                    files: files2,
228                } => {
229                    let dirs = match (dirs1, dirs2) {
230                        (VisitDirs::All, VisitDirs::All) => VisitDirs::All,
231                        (dirs1, VisitDirs::All) => dirs1,
232                        (VisitDirs::All, dirs2) => dirs2,
233                        (VisitDirs::Set(dirs1), VisitDirs::Set(dirs2)) => {
234                            VisitDirs::Set(dirs1.intersection(&dirs2).cloned().collect())
235                        }
236                    };
237                    let files = match (files1, files2) {
238                        (VisitFiles::All, VisitFiles::All) => VisitFiles::All,
239                        (files1, VisitFiles::All) => files1,
240                        (VisitFiles::All, files2) => files2,
241                        (VisitFiles::Set(files1), VisitFiles::Set(files2)) => {
242                            VisitFiles::Set(files1.intersection(&files2).cloned().collect())
243                        }
244                    };
245                    match (&dirs, &files) {
246                        (VisitDirs::Set(dirs), VisitFiles::Set(files))
247                            if dirs.is_empty() && files.is_empty() =>
248                        {
249                            Visit::Nothing
250                        }
251                        _ => Visit::Specific { dirs, files },
252                    }
253                }
254            },
255        }
256    }
257}
258
259/// Keeps track of which subdirectories and files of each directory need to be
260/// visited.
261#[derive(PartialEq, Eq, Debug)]
262struct RepoPathTree {
263    entries: HashMap<RepoPathComponent, RepoPathTree>,
264    // is_dir/is_file aren't exclusive, both can be set to true. If entries is not empty,
265    // is_dir should be set.
266    is_dir: bool,
267    is_file: bool,
268}
269
270impl RepoPathTree {
271    fn new() -> Self {
272        RepoPathTree {
273            entries: HashMap::new(),
274            is_dir: false,
275            is_file: false,
276        }
277    }
278
279    fn add(&mut self, dir: &RepoPath) -> &mut RepoPathTree {
280        dir.components().iter().fold(self, |sub, name| {
281            // Avoid name.clone() if entry already exists.
282            if !sub.entries.contains_key(name) {
283                sub.is_dir = true;
284                sub.entries.insert(name.clone(), RepoPathTree::new());
285            }
286            sub.entries.get_mut(name).unwrap()
287        })
288    }
289
290    fn add_dir(&mut self, dir: &RepoPath) {
291        self.add(dir).is_dir = true;
292    }
293
294    fn add_file(&mut self, file: &RepoPath) {
295        self.add(file).is_file = true;
296    }
297
298    fn get(&self, dir: &RepoPath) -> Option<&RepoPathTree> {
299        dir.components()
300            .iter()
301            .try_fold(self, |sub, name| sub.entries.get(name))
302    }
303
304    fn get_visit_sets(&self, dir: &RepoPath) -> Visit {
305        self.get(dir)
306            .map(RepoPathTree::to_visit_sets)
307            .unwrap_or(Visit::Nothing)
308    }
309
310    fn walk_to<'a>(
311        &'a self,
312        dir: &'a RepoPath,
313    ) -> impl Iterator<Item = (&RepoPathTree, &[RepoPathComponent])> + 'a {
314        iter::successors(
315            Some((self, dir.components().as_slice())),
316            |(sub, components)| {
317                let (name, tail) = components.split_first()?;
318                Some((sub.entries.get(name)?, tail))
319            },
320        )
321    }
322
323    fn to_visit_sets(&self) -> Visit {
324        let mut dirs = HashSet::new();
325        let mut files = HashSet::new();
326        for (name, sub) in &self.entries {
327            if sub.is_dir {
328                dirs.insert(name.clone());
329            }
330            if sub.is_file {
331                files.insert(name.clone());
332            }
333        }
334        Visit::sets(dirs, files)
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use maplit::hashset;
341
342    use super::*;
343    use crate::repo_path::{RepoPath, RepoPathComponent};
344
345    #[test]
346    fn test_repo_path_tree_empty() {
347        let tree = RepoPathTree::new();
348        assert_eq!(tree.get_visit_sets(&RepoPath::root()), Visit::Nothing);
349    }
350
351    #[test]
352    fn test_repo_path_tree_root() {
353        let mut tree = RepoPathTree::new();
354        tree.add_dir(&RepoPath::root());
355        assert_eq!(tree.get_visit_sets(&RepoPath::root()), Visit::Nothing);
356    }
357
358    #[test]
359    fn test_repo_path_tree_dir() {
360        let mut tree = RepoPathTree::new();
361        tree.add_dir(&RepoPath::from_internal_string("dir"));
362        assert_eq!(
363            tree.get_visit_sets(&RepoPath::root()),
364            Visit::sets(hashset! {RepoPathComponent::from("dir")}, hashset! {}),
365        );
366        tree.add_dir(&RepoPath::from_internal_string("dir/sub"));
367        assert_eq!(
368            tree.get_visit_sets(&RepoPath::from_internal_string("dir")),
369            Visit::sets(hashset! {RepoPathComponent::from("sub")}, hashset! {}),
370        );
371    }
372
373    #[test]
374    fn test_repo_path_tree_file() {
375        let mut tree = RepoPathTree::new();
376        tree.add_file(&RepoPath::from_internal_string("dir/file"));
377        assert_eq!(
378            tree.get_visit_sets(&RepoPath::root()),
379            Visit::sets(hashset! {RepoPathComponent::from("dir")}, hashset! {}),
380        );
381        assert_eq!(
382            tree.get_visit_sets(&RepoPath::from_internal_string("dir")),
383            Visit::sets(hashset! {}, hashset! {RepoPathComponent::from("file")}),
384        );
385    }
386
387    #[test]
388    fn test_nothingmatcher() {
389        let m = NothingMatcher;
390        assert!(!m.matches(&RepoPath::from_internal_string("file")));
391        assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
392        assert_eq!(m.visit(&RepoPath::root()), Visit::Nothing);
393    }
394
395    #[test]
396    fn test_filesmatcher_empty() {
397        let m = FilesMatcher::new(&[]);
398        assert!(!m.matches(&RepoPath::from_internal_string("file")));
399        assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
400        assert_eq!(m.visit(&RepoPath::root()), Visit::Nothing);
401    }
402
403    #[test]
404    fn test_filesmatcher_nonempty() {
405        let m = FilesMatcher::new(&[
406            RepoPath::from_internal_string("dir1/subdir1/file1"),
407            RepoPath::from_internal_string("dir1/subdir1/file2"),
408            RepoPath::from_internal_string("dir1/subdir2/file3"),
409            RepoPath::from_internal_string("file4"),
410        ]);
411
412        assert!(!m.matches(&RepoPath::from_internal_string("dir1")));
413        assert!(!m.matches(&RepoPath::from_internal_string("dir1/subdir1")));
414        assert!(m.matches(&RepoPath::from_internal_string("dir1/subdir1/file1")));
415        assert!(m.matches(&RepoPath::from_internal_string("dir1/subdir1/file2")));
416        assert!(!m.matches(&RepoPath::from_internal_string("dir1/subdir1/file3")));
417
418        assert_eq!(
419            m.visit(&RepoPath::root()),
420            Visit::sets(
421                hashset! {RepoPathComponent::from("dir1")},
422                hashset! {RepoPathComponent::from("file4")}
423            )
424        );
425        assert_eq!(
426            m.visit(&RepoPath::from_internal_string("dir1")),
427            Visit::sets(
428                hashset! {RepoPathComponent::from("subdir1"), RepoPathComponent::from("subdir2")},
429                hashset! {}
430            )
431        );
432        assert_eq!(
433            m.visit(&RepoPath::from_internal_string("dir1/subdir1")),
434            Visit::sets(
435                hashset! {},
436                hashset! {RepoPathComponent::from("file1"), RepoPathComponent::from("file2")}
437            )
438        );
439        assert_eq!(
440            m.visit(&RepoPath::from_internal_string("dir1/subdir2")),
441            Visit::sets(hashset! {}, hashset! {RepoPathComponent::from("file3")})
442        );
443    }
444
445    #[test]
446    fn test_prefixmatcher_empty() {
447        let m = PrefixMatcher::new(&[]);
448        assert!(!m.matches(&RepoPath::from_internal_string("file")));
449        assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
450        assert_eq!(m.visit(&RepoPath::root()), Visit::Nothing);
451    }
452
453    #[test]
454    fn test_prefixmatcher_root() {
455        let m = PrefixMatcher::new(&[RepoPath::root()]);
456        // Matches all files
457        assert!(m.matches(&RepoPath::from_internal_string("file")));
458        assert!(m.matches(&RepoPath::from_internal_string("dir/file")));
459        // Visits all directories
460        assert_eq!(m.visit(&RepoPath::root()), Visit::AllRecursively);
461        assert_eq!(
462            m.visit(&RepoPath::from_internal_string("foo/bar")),
463            Visit::AllRecursively
464        );
465    }
466
467    #[test]
468    fn test_prefixmatcher_single_prefix() {
469        let m = PrefixMatcher::new(&[RepoPath::from_internal_string("foo/bar")]);
470
471        // Parts of the prefix should not match
472        assert!(!m.matches(&RepoPath::from_internal_string("foo")));
473        assert!(!m.matches(&RepoPath::from_internal_string("bar")));
474        // A file matching the prefix exactly should match
475        assert!(m.matches(&RepoPath::from_internal_string("foo/bar")));
476        // Files in subdirectories should match
477        assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz")));
478        assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz/qux")));
479        // Sibling files should not match
480        assert!(!m.matches(&RepoPath::from_internal_string("foo/foo")));
481        // An unrooted "foo/bar" should not match
482        assert!(!m.matches(&RepoPath::from_internal_string("bar/foo/bar")));
483
484        // The matcher should only visit directory foo/ in the root (file "foo"
485        // shouldn't be visited)
486        assert_eq!(
487            m.visit(&RepoPath::root()),
488            Visit::sets(hashset! {RepoPathComponent::from("foo")}, hashset! {})
489        );
490        // Inside parent directory "foo/", both subdirectory "bar" and file "bar" may
491        // match
492        assert_eq!(
493            m.visit(&RepoPath::from_internal_string("foo")),
494            Visit::sets(
495                hashset! {RepoPathComponent::from("bar")},
496                hashset! {RepoPathComponent::from("bar")}
497            )
498        );
499        // Inside a directory that matches the prefix, everything matches recursively
500        assert_eq!(
501            m.visit(&RepoPath::from_internal_string("foo/bar")),
502            Visit::AllRecursively
503        );
504        // Same thing in subdirectories of the prefix
505        assert_eq!(
506            m.visit(&RepoPath::from_internal_string("foo/bar/baz")),
507            Visit::AllRecursively
508        );
509        // Nothing in directories that are siblings of the prefix can match, so don't
510        // visit
511        assert_eq!(
512            m.visit(&RepoPath::from_internal_string("bar")),
513            Visit::Nothing
514        );
515    }
516
517    #[test]
518    fn test_prefixmatcher_nested_prefixes() {
519        let m = PrefixMatcher::new(&[
520            RepoPath::from_internal_string("foo"),
521            RepoPath::from_internal_string("foo/bar/baz"),
522        ]);
523
524        assert!(m.matches(&RepoPath::from_internal_string("foo")));
525        assert!(!m.matches(&RepoPath::from_internal_string("bar")));
526        assert!(m.matches(&RepoPath::from_internal_string("foo/bar")));
527        // Matches because the "foo" pattern matches
528        assert!(m.matches(&RepoPath::from_internal_string("foo/baz/foo")));
529
530        assert_eq!(
531            m.visit(&RepoPath::root()),
532            Visit::sets(
533                hashset! {RepoPathComponent::from("foo")},
534                hashset! {RepoPathComponent::from("foo")}
535            )
536        );
537        // Inside a directory that matches the prefix, everything matches recursively
538        assert_eq!(
539            m.visit(&RepoPath::from_internal_string("foo")),
540            Visit::AllRecursively
541        );
542        // Same thing in subdirectories of the prefix
543        assert_eq!(
544            m.visit(&RepoPath::from_internal_string("foo/bar/baz")),
545            Visit::AllRecursively
546        );
547    }
548
549    #[test]
550    fn test_differencematcher_remove_subdir() {
551        let m1 = PrefixMatcher::new(&[
552            RepoPath::from_internal_string("foo"),
553            RepoPath::from_internal_string("bar"),
554        ]);
555        let m2 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo/bar")]);
556        let m = DifferenceMatcher::new(&m1, &m2);
557
558        assert!(m.matches(&RepoPath::from_internal_string("foo")));
559        assert!(!m.matches(&RepoPath::from_internal_string("foo/bar")));
560        assert!(!m.matches(&RepoPath::from_internal_string("foo/bar/baz")));
561        assert!(m.matches(&RepoPath::from_internal_string("foo/baz")));
562        assert!(m.matches(&RepoPath::from_internal_string("bar")));
563
564        assert_eq!(
565            m.visit(&RepoPath::root()),
566            Visit::sets(
567                hashset! {RepoPathComponent::from("foo"), RepoPathComponent::from("bar")},
568                hashset! {RepoPathComponent::from("foo"), RepoPathComponent::from("bar")}
569            )
570        );
571        assert_eq!(
572            m.visit(&RepoPath::from_internal_string("foo")),
573            Visit::Specific {
574                dirs: VisitDirs::All,
575                files: VisitFiles::All,
576            }
577        );
578        assert_eq!(
579            m.visit(&RepoPath::from_internal_string("foo/bar")),
580            Visit::Nothing
581        );
582        assert_eq!(
583            m.visit(&RepoPath::from_internal_string("foo/baz")),
584            Visit::AllRecursively
585        );
586        assert_eq!(
587            m.visit(&RepoPath::from_internal_string("bar")),
588            Visit::AllRecursively
589        );
590    }
591
592    #[test]
593    fn test_differencematcher_shared_patterns() {
594        let m1 = PrefixMatcher::new(&[
595            RepoPath::from_internal_string("foo"),
596            RepoPath::from_internal_string("bar"),
597        ]);
598        let m2 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo")]);
599        let m = DifferenceMatcher::new(&m1, &m2);
600
601        assert!(!m.matches(&RepoPath::from_internal_string("foo")));
602        assert!(!m.matches(&RepoPath::from_internal_string("foo/bar")));
603        assert!(m.matches(&RepoPath::from_internal_string("bar")));
604        assert!(m.matches(&RepoPath::from_internal_string("bar/foo")));
605
606        assert_eq!(
607            m.visit(&RepoPath::root()),
608            Visit::sets(
609                hashset! {RepoPathComponent::from("foo"), RepoPathComponent::from("bar")},
610                hashset! {RepoPathComponent::from("foo"), RepoPathComponent::from("bar")}
611            )
612        );
613        assert_eq!(
614            m.visit(&RepoPath::from_internal_string("foo")),
615            Visit::Nothing
616        );
617        assert_eq!(
618            m.visit(&RepoPath::from_internal_string("foo/bar")),
619            Visit::Nothing
620        );
621        assert_eq!(
622            m.visit(&RepoPath::from_internal_string("bar")),
623            Visit::AllRecursively
624        );
625        assert_eq!(
626            m.visit(&RepoPath::from_internal_string("bar/foo")),
627            Visit::AllRecursively
628        );
629    }
630
631    #[test]
632    fn test_intersectionmatcher_intersecting_roots() {
633        let m1 = PrefixMatcher::new(&[
634            RepoPath::from_internal_string("foo"),
635            RepoPath::from_internal_string("bar"),
636        ]);
637        let m2 = PrefixMatcher::new(&[
638            RepoPath::from_internal_string("bar"),
639            RepoPath::from_internal_string("baz"),
640        ]);
641        let m = IntersectionMatcher::new(&m1, &m2);
642
643        assert!(!m.matches(&RepoPath::from_internal_string("foo")));
644        assert!(!m.matches(&RepoPath::from_internal_string("foo/bar")));
645        assert!(m.matches(&RepoPath::from_internal_string("bar")));
646        assert!(m.matches(&RepoPath::from_internal_string("bar/foo")));
647        assert!(!m.matches(&RepoPath::from_internal_string("baz")));
648        assert!(!m.matches(&RepoPath::from_internal_string("baz/foo")));
649
650        assert_eq!(
651            m.visit(&RepoPath::root()),
652            Visit::sets(
653                hashset! {RepoPathComponent::from("bar")},
654                hashset! {RepoPathComponent::from("bar")}
655            )
656        );
657        assert_eq!(
658            m.visit(&RepoPath::from_internal_string("foo")),
659            Visit::Nothing
660        );
661        assert_eq!(
662            m.visit(&RepoPath::from_internal_string("foo/bar")),
663            Visit::Nothing
664        );
665        assert_eq!(
666            m.visit(&RepoPath::from_internal_string("bar")),
667            Visit::AllRecursively
668        );
669        assert_eq!(
670            m.visit(&RepoPath::from_internal_string("bar/foo")),
671            Visit::AllRecursively
672        );
673        assert_eq!(
674            m.visit(&RepoPath::from_internal_string("baz")),
675            Visit::Nothing
676        );
677        assert_eq!(
678            m.visit(&RepoPath::from_internal_string("baz/foo")),
679            Visit::Nothing
680        );
681    }
682
683    #[test]
684    fn test_intersectionmatcher_subdir() {
685        let m1 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo")]);
686        let m2 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo/bar")]);
687        let m = IntersectionMatcher::new(&m1, &m2);
688
689        assert!(!m.matches(&RepoPath::from_internal_string("foo")));
690        assert!(!m.matches(&RepoPath::from_internal_string("bar")));
691        assert!(m.matches(&RepoPath::from_internal_string("foo/bar")));
692        assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz")));
693        assert!(!m.matches(&RepoPath::from_internal_string("foo/baz")));
694
695        assert_eq!(
696            m.visit(&RepoPath::root()),
697            Visit::sets(hashset! {RepoPathComponent::from("foo")}, hashset! {})
698        );
699        assert_eq!(
700            m.visit(&RepoPath::from_internal_string("bar")),
701            Visit::Nothing
702        );
703        assert_eq!(
704            m.visit(&RepoPath::from_internal_string("foo")),
705            Visit::sets(
706                hashset! {RepoPathComponent::from("bar")},
707                hashset! {RepoPathComponent::from("bar")}
708            )
709        );
710        assert_eq!(
711            m.visit(&RepoPath::from_internal_string("foo/bar")),
712            Visit::AllRecursively
713        );
714    }
715}