jj_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(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    /// Everything in the directory is *guaranteed* to match, no need to check
32    /// descendants
33    AllRecursively,
34    Specific {
35        dirs: VisitDirs,
36        files: VisitFiles,
37    },
38    /// Nothing in the directory or its subdirectories will match.
39    ///
40    /// This is the same as `Specific` with no directories or files. Use
41    /// `Visit::set()` to get create an instance that's `Specific` or
42    /// `Nothing` depending on the values at runtime.
43    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    /// Represents an intermediate directory.
158    #[default]
159    Dir,
160    /// Represents a file (which might also be an intermediate directory.)
161    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        // should visit only intermediate directories
169        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            // ancestor of 'dir' matches prefix paths
205            if sub.value == PrefixNodeKind::Prefix {
206                return Visit::AllRecursively;
207            }
208            // 'dir' found, and is an ancestor of prefix paths
209            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    /// Represents an intermediate directory.
220    #[default]
221    Dir,
222    /// Represents a file and prefix directory.
223    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        // should visit both intermediate and prefix directories
231        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/// Matches file paths with glob patterns.
240///
241/// Patterns are provided as `(dir, pattern)` pairs, where `dir` should be the
242/// longest directory path that contains no glob meta characters, and `pattern`
243/// will be evaluated relative to `dir`.
244#[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        // TODO: glob::Pattern relies on path::is_separator() internally, but
264        // RepoPath separator should be '/'. One way to address this problem is
265        // to switch to globset::Glob, and use the underlying regex pattern.
266        const OPTIONS: glob::MatchOptions = glob::MatchOptions {
267            case_sensitive: true,
268            require_literal_separator: true,
269            require_literal_leading_dot: false,
270        };
271        // check if any ancestor (dir, patterns) matches 'file'
272        self.tree
273            .walk_to(file)
274            .take_while(|(_, tail_path)| !tail_path.is_root()) // only dirs
275            .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            // ancestor of 'dir' has patterns, can't narrow visit anymore
284            if !sub.value.is_empty() {
285                return Visit::Specific {
286                    dirs: VisitDirs::All,
287                    files: VisitFiles::All,
288                };
289            }
290            // 'dir' found, and is an ancestor of pattern paths
291            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/// Matches paths that are matched by any of the input matchers.
301#[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/// Matches paths that are matched by the first input matcher but not by the
355/// second.
356#[derive(Clone, Debug)]
357pub struct DifferenceMatcher<M1, M2> {
358    /// The minuend
359    wanted: M1,
360    /// The subtrahend
361    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/// Matches paths that are matched by both input matchers.
391#[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/// Tree that maps `RepoPath` to value of type `V`.
456#[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            // Avoid name.clone() if entry already exists.
469            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    /// Walks the tree from the root to the given `dir`, yielding each sub tree
482    /// and remaining path.
483    fn walk_to<'a, 'b>(
484        &'a self,
485        dir: &'b RepoPath,
486        // TODO: V doesn't have to be captured, but "currently, all type
487        // parameters are required to be mentioned in the precise captures list"
488        // as of rustc 1.85.0.
489    ) -> 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)
520    }
521
522    #[test]
523    fn test_nothingmatcher() {
524        let m = NothingMatcher;
525        assert!(!m.matches(repo_path("file")));
526        assert!(!m.matches(repo_path("dir/file")));
527        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
528    }
529
530    #[test]
531    fn test_filesmatcher_empty() {
532        let m = FilesMatcher::new([] as [&RepoPath; 0]);
533        assert!(!m.matches(repo_path("file")));
534        assert!(!m.matches(repo_path("dir/file")));
535        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
536    }
537
538    #[test]
539    fn test_filesmatcher_nonempty() {
540        let m = FilesMatcher::new([
541            repo_path("dir1/subdir1/file1"),
542            repo_path("dir1/subdir1/file2"),
543            repo_path("dir1/subdir2/file3"),
544            repo_path("file4"),
545        ]);
546
547        assert!(!m.matches(repo_path("dir1")));
548        assert!(!m.matches(repo_path("dir1/subdir1")));
549        assert!(m.matches(repo_path("dir1/subdir1/file1")));
550        assert!(m.matches(repo_path("dir1/subdir1/file2")));
551        assert!(!m.matches(repo_path("dir1/subdir1/file3")));
552
553        assert_eq!(
554            m.visit(RepoPath::root()),
555            Visit::sets(
556                hashset! {RepoPathComponentBuf::from("dir1")},
557                hashset! {RepoPathComponentBuf::from("file4")}
558            )
559        );
560        assert_eq!(
561            m.visit(repo_path("dir1")),
562            Visit::sets(
563                hashset! {
564                    RepoPathComponentBuf::from("subdir1"),
565                    RepoPathComponentBuf::from("subdir2"),
566                },
567                hashset! {}
568            )
569        );
570        assert_eq!(
571            m.visit(repo_path("dir1/subdir1")),
572            Visit::sets(
573                hashset! {},
574                hashset! {
575                    RepoPathComponentBuf::from("file1"),
576                    RepoPathComponentBuf::from("file2"),
577                },
578            )
579        );
580        assert_eq!(
581            m.visit(repo_path("dir1/subdir2")),
582            Visit::sets(hashset! {}, hashset! {RepoPathComponentBuf::from("file3")})
583        );
584    }
585
586    #[test]
587    fn test_prefixmatcher_empty() {
588        let m = PrefixMatcher::new([] as [&RepoPath; 0]);
589        assert!(!m.matches(repo_path("file")));
590        assert!(!m.matches(repo_path("dir/file")));
591        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
592    }
593
594    #[test]
595    fn test_prefixmatcher_root() {
596        let m = PrefixMatcher::new([RepoPath::root()]);
597        // Matches all files
598        assert!(m.matches(repo_path("file")));
599        assert!(m.matches(repo_path("dir/file")));
600        // Visits all directories
601        assert_eq!(m.visit(RepoPath::root()), Visit::AllRecursively);
602        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
603    }
604
605    #[test]
606    fn test_prefixmatcher_single_prefix() {
607        let m = PrefixMatcher::new([repo_path("foo/bar")]);
608
609        // Parts of the prefix should not match
610        assert!(!m.matches(repo_path("foo")));
611        assert!(!m.matches(repo_path("bar")));
612        // A file matching the prefix exactly should match
613        assert!(m.matches(repo_path("foo/bar")));
614        // Files in subdirectories should match
615        assert!(m.matches(repo_path("foo/bar/baz")));
616        assert!(m.matches(repo_path("foo/bar/baz/qux")));
617        // Sibling files should not match
618        assert!(!m.matches(repo_path("foo/foo")));
619        // An unrooted "foo/bar" should not match
620        assert!(!m.matches(repo_path("bar/foo/bar")));
621
622        // The matcher should only visit directory foo/ in the root (file "foo"
623        // shouldn't be visited)
624        assert_eq!(
625            m.visit(RepoPath::root()),
626            Visit::sets(hashset! {RepoPathComponentBuf::from("foo")}, hashset! {})
627        );
628        // Inside parent directory "foo/", both subdirectory "bar" and file "bar" may
629        // match
630        assert_eq!(
631            m.visit(repo_path("foo")),
632            Visit::sets(
633                hashset! {RepoPathComponentBuf::from("bar")},
634                hashset! {RepoPathComponentBuf::from("bar")}
635            )
636        );
637        // Inside a directory that matches the prefix, everything matches recursively
638        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
639        // Same thing in subdirectories of the prefix
640        assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
641        // Nothing in directories that are siblings of the prefix can match, so don't
642        // visit
643        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
644    }
645
646    #[test]
647    fn test_prefixmatcher_nested_prefixes() {
648        let m = PrefixMatcher::new([repo_path("foo"), repo_path("foo/bar/baz")]);
649
650        assert!(m.matches(repo_path("foo")));
651        assert!(!m.matches(repo_path("bar")));
652        assert!(m.matches(repo_path("foo/bar")));
653        // Matches because the "foo" pattern matches
654        assert!(m.matches(repo_path("foo/baz/foo")));
655
656        assert_eq!(
657            m.visit(RepoPath::root()),
658            Visit::sets(
659                hashset! {RepoPathComponentBuf::from("foo")},
660                hashset! {RepoPathComponentBuf::from("foo")}
661            )
662        );
663        // Inside a directory that matches the prefix, everything matches recursively
664        assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
665        // Same thing in subdirectories of the prefix
666        assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
667    }
668
669    #[test]
670    fn test_fileglobsmatcher_rooted() {
671        let to_pattern = |s| glob::Pattern::new(s).unwrap();
672
673        let m = FileGlobsMatcher::new([(RepoPath::root(), to_pattern("*.rs"))]);
674        assert!(!m.matches(repo_path("foo")));
675        assert!(m.matches(repo_path("foo.rs")));
676        assert!(!m.matches(repo_path("foo.rss")));
677        assert!(!m.matches(repo_path("foo.rs/bar.rs")));
678        assert!(!m.matches(repo_path("foo/bar.rs")));
679        assert_eq!(
680            m.visit(RepoPath::root()),
681            Visit::Specific {
682                dirs: VisitDirs::All,
683                files: VisitFiles::All
684            }
685        );
686
687        // Multiple patterns at the same directory
688        let m = FileGlobsMatcher::new([
689            (RepoPath::root(), to_pattern("foo?")),
690            (RepoPath::root(), to_pattern("**/*.rs")),
691        ]);
692        assert!(!m.matches(repo_path("foo")));
693        assert!(m.matches(repo_path("foo1")));
694        assert!(!m.matches(repo_path("Foo1")));
695        assert!(!m.matches(repo_path("foo1/foo2")));
696        assert!(m.matches(repo_path("foo.rs")));
697        assert!(m.matches(repo_path("foo.rs/bar.rs")));
698        assert!(m.matches(repo_path("foo/bar.rs")));
699        assert_eq!(
700            m.visit(RepoPath::root()),
701            Visit::Specific {
702                dirs: VisitDirs::All,
703                files: VisitFiles::All
704            }
705        );
706        assert_eq!(
707            m.visit(repo_path("foo")),
708            Visit::Specific {
709                dirs: VisitDirs::All,
710                files: VisitFiles::All
711            }
712        );
713        assert_eq!(
714            m.visit(repo_path("bar/baz")),
715            Visit::Specific {
716                dirs: VisitDirs::All,
717                files: VisitFiles::All
718            }
719        );
720    }
721
722    #[test]
723    fn test_fileglobsmatcher_nested() {
724        let to_pattern = |s| glob::Pattern::new(s).unwrap();
725
726        let m = FileGlobsMatcher::new([
727            (repo_path("foo"), to_pattern("**/*.a")),
728            (repo_path("foo/bar"), to_pattern("*.b")),
729            (repo_path("baz"), to_pattern("?*")),
730        ]);
731        assert!(!m.matches(repo_path("foo")));
732        assert!(m.matches(repo_path("foo/x.a")));
733        assert!(!m.matches(repo_path("foo/x.b")));
734        assert!(m.matches(repo_path("foo/bar/x.a")));
735        assert!(m.matches(repo_path("foo/bar/x.b")));
736        assert!(m.matches(repo_path("foo/bar/baz/x.a")));
737        assert!(!m.matches(repo_path("foo/bar/baz/x.b")));
738        assert!(!m.matches(repo_path("baz")));
739        assert!(m.matches(repo_path("baz/x")));
740        assert_eq!(
741            m.visit(RepoPath::root()),
742            Visit::Specific {
743                dirs: VisitDirs::Set(hashset! {
744                    RepoPathComponentBuf::from("foo"),
745                    RepoPathComponentBuf::from("baz"),
746                }),
747                files: VisitFiles::Set(hashset! {}),
748            }
749        );
750        assert_eq!(
751            m.visit(repo_path("foo")),
752            Visit::Specific {
753                dirs: VisitDirs::All,
754                files: VisitFiles::All,
755            }
756        );
757        assert_eq!(
758            m.visit(repo_path("foo/bar")),
759            Visit::Specific {
760                dirs: VisitDirs::All,
761                files: VisitFiles::All,
762            }
763        );
764        assert_eq!(
765            m.visit(repo_path("foo/bar/baz")),
766            Visit::Specific {
767                dirs: VisitDirs::All,
768                files: VisitFiles::All,
769            }
770        );
771        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
772        assert_eq!(
773            m.visit(repo_path("baz")),
774            Visit::Specific {
775                dirs: VisitDirs::All,
776                files: VisitFiles::All,
777            }
778        );
779    }
780
781    #[test]
782    fn test_fileglobsmatcher_wildcard_any() {
783        let to_pattern = |s| glob::Pattern::new(s).unwrap();
784
785        // "*" could match the root path, but it doesn't matter since the root
786        // isn't a valid file path.
787        let m = FileGlobsMatcher::new([(RepoPath::root(), to_pattern("*"))]);
788        assert!(!m.matches(RepoPath::root())); // doesn't matter
789        assert!(m.matches(repo_path("x")));
790        assert!(m.matches(repo_path("x.rs")));
791        assert!(!m.matches(repo_path("foo/bar.rs")));
792        assert_eq!(
793            m.visit(RepoPath::root()),
794            Visit::Specific {
795                dirs: VisitDirs::All,
796                files: VisitFiles::All
797            }
798        );
799
800        // "foo/*" shouldn't match "foo"
801        let m = FileGlobsMatcher::new([(repo_path("foo"), to_pattern("*"))]);
802        assert!(!m.matches(RepoPath::root()));
803        assert!(!m.matches(repo_path("foo")));
804        assert!(m.matches(repo_path("foo/x")));
805        assert!(!m.matches(repo_path("foo/bar/baz")));
806        assert_eq!(
807            m.visit(RepoPath::root()),
808            Visit::Specific {
809                dirs: VisitDirs::Set(hashset! {RepoPathComponentBuf::from("foo")}),
810                files: VisitFiles::Set(hashset! {}),
811            }
812        );
813        assert_eq!(
814            m.visit(repo_path("foo")),
815            Visit::Specific {
816                dirs: VisitDirs::All,
817                files: VisitFiles::All
818            }
819        );
820        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
821    }
822
823    #[test]
824    fn test_unionmatcher_concatenate_roots() {
825        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
826        let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]);
827        let m = UnionMatcher::new(&m1, &m2);
828
829        assert!(m.matches(repo_path("foo")));
830        assert!(m.matches(repo_path("foo/bar")));
831        assert!(m.matches(repo_path("bar")));
832        assert!(m.matches(repo_path("bar/foo")));
833        assert!(m.matches(repo_path("baz")));
834        assert!(m.matches(repo_path("baz/foo")));
835        assert!(!m.matches(repo_path("qux")));
836        assert!(!m.matches(repo_path("qux/foo")));
837
838        assert_eq!(
839            m.visit(RepoPath::root()),
840            Visit::sets(
841                hashset! {
842                    RepoPathComponentBuf::from("foo"),
843                    RepoPathComponentBuf::from("bar"),
844                    RepoPathComponentBuf::from("baz"),
845                },
846                hashset! {
847                    RepoPathComponentBuf::from("foo"),
848                    RepoPathComponentBuf::from("bar"),
849                    RepoPathComponentBuf::from("baz"),
850                },
851            )
852        );
853        assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
854        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
855        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
856        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
857        assert_eq!(m.visit(repo_path("baz")), Visit::AllRecursively);
858        assert_eq!(m.visit(repo_path("baz/foo")), Visit::AllRecursively);
859        assert_eq!(m.visit(repo_path("qux")), Visit::Nothing);
860        assert_eq!(m.visit(repo_path("qux/foo")), Visit::Nothing);
861    }
862
863    #[test]
864    fn test_unionmatcher_concatenate_subdirs() {
865        let m1 = PrefixMatcher::new([repo_path("common/bar"), repo_path("1/foo")]);
866        let m2 = PrefixMatcher::new([repo_path("common/baz"), repo_path("2/qux")]);
867        let m = UnionMatcher::new(&m1, &m2);
868
869        assert!(!m.matches(repo_path("common")));
870        assert!(!m.matches(repo_path("1")));
871        assert!(!m.matches(repo_path("2")));
872        assert!(m.matches(repo_path("common/bar")));
873        assert!(m.matches(repo_path("common/bar/baz")));
874        assert!(m.matches(repo_path("common/baz")));
875        assert!(m.matches(repo_path("1/foo")));
876        assert!(m.matches(repo_path("1/foo/qux")));
877        assert!(m.matches(repo_path("2/qux")));
878        assert!(!m.matches(repo_path("2/quux")));
879
880        assert_eq!(
881            m.visit(RepoPath::root()),
882            Visit::sets(
883                hashset! {
884                    RepoPathComponentBuf::from("common"),
885                    RepoPathComponentBuf::from("1"),
886                    RepoPathComponentBuf::from("2"),
887                },
888                hashset! {},
889            )
890        );
891        assert_eq!(
892            m.visit(repo_path("common")),
893            Visit::sets(
894                hashset! {
895                    RepoPathComponentBuf::from("bar"),
896                    RepoPathComponentBuf::from("baz"),
897                },
898                hashset! {
899                    RepoPathComponentBuf::from("bar"),
900                    RepoPathComponentBuf::from("baz"),
901                },
902            )
903        );
904        assert_eq!(
905            m.visit(repo_path("1")),
906            Visit::sets(
907                hashset! {RepoPathComponentBuf::from("foo")},
908                hashset! {RepoPathComponentBuf::from("foo")},
909            )
910        );
911        assert_eq!(
912            m.visit(repo_path("2")),
913            Visit::sets(
914                hashset! {RepoPathComponentBuf::from("qux")},
915                hashset! {RepoPathComponentBuf::from("qux")},
916            )
917        );
918        assert_eq!(m.visit(repo_path("common/bar")), Visit::AllRecursively);
919        assert_eq!(m.visit(repo_path("1/foo")), Visit::AllRecursively);
920        assert_eq!(m.visit(repo_path("2/qux")), Visit::AllRecursively);
921        assert_eq!(m.visit(repo_path("2/quux")), Visit::Nothing);
922    }
923
924    #[test]
925    fn test_differencematcher_remove_subdir() {
926        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
927        let m2 = PrefixMatcher::new([repo_path("foo/bar")]);
928        let m = DifferenceMatcher::new(&m1, &m2);
929
930        assert!(m.matches(repo_path("foo")));
931        assert!(!m.matches(repo_path("foo/bar")));
932        assert!(!m.matches(repo_path("foo/bar/baz")));
933        assert!(m.matches(repo_path("foo/baz")));
934        assert!(m.matches(repo_path("bar")));
935
936        assert_eq!(
937            m.visit(RepoPath::root()),
938            Visit::sets(
939                hashset! {
940                    RepoPathComponentBuf::from("foo"),
941                    RepoPathComponentBuf::from("bar"),
942                },
943                hashset! {
944                    RepoPathComponentBuf::from("foo"),
945                    RepoPathComponentBuf::from("bar"),
946                },
947            )
948        );
949        assert_eq!(
950            m.visit(repo_path("foo")),
951            Visit::Specific {
952                dirs: VisitDirs::All,
953                files: VisitFiles::All,
954            }
955        );
956        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
957        assert_eq!(m.visit(repo_path("foo/baz")), Visit::AllRecursively);
958        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
959    }
960
961    #[test]
962    fn test_differencematcher_shared_patterns() {
963        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
964        let m2 = PrefixMatcher::new([repo_path("foo")]);
965        let m = DifferenceMatcher::new(&m1, &m2);
966
967        assert!(!m.matches(repo_path("foo")));
968        assert!(!m.matches(repo_path("foo/bar")));
969        assert!(m.matches(repo_path("bar")));
970        assert!(m.matches(repo_path("bar/foo")));
971
972        assert_eq!(
973            m.visit(RepoPath::root()),
974            Visit::sets(
975                hashset! {
976                    RepoPathComponentBuf::from("foo"),
977                    RepoPathComponentBuf::from("bar"),
978                },
979                hashset! {
980                    RepoPathComponentBuf::from("foo"),
981                    RepoPathComponentBuf::from("bar"),
982                },
983            )
984        );
985        assert_eq!(m.visit(repo_path("foo")), Visit::Nothing);
986        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
987        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
988        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
989    }
990
991    #[test]
992    fn test_intersectionmatcher_intersecting_roots() {
993        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
994        let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]);
995        let m = IntersectionMatcher::new(&m1, &m2);
996
997        assert!(!m.matches(repo_path("foo")));
998        assert!(!m.matches(repo_path("foo/bar")));
999        assert!(m.matches(repo_path("bar")));
1000        assert!(m.matches(repo_path("bar/foo")));
1001        assert!(!m.matches(repo_path("baz")));
1002        assert!(!m.matches(repo_path("baz/foo")));
1003
1004        assert_eq!(
1005            m.visit(RepoPath::root()),
1006            Visit::sets(
1007                hashset! {RepoPathComponentBuf::from("bar")},
1008                hashset! {RepoPathComponentBuf::from("bar")}
1009            )
1010        );
1011        assert_eq!(m.visit(repo_path("foo")), Visit::Nothing);
1012        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
1013        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
1014        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
1015        assert_eq!(m.visit(repo_path("baz")), Visit::Nothing);
1016        assert_eq!(m.visit(repo_path("baz/foo")), Visit::Nothing);
1017    }
1018
1019    #[test]
1020    fn test_intersectionmatcher_subdir() {
1021        let m1 = PrefixMatcher::new([repo_path("foo")]);
1022        let m2 = PrefixMatcher::new([repo_path("foo/bar")]);
1023        let m = IntersectionMatcher::new(&m1, &m2);
1024
1025        assert!(!m.matches(repo_path("foo")));
1026        assert!(!m.matches(repo_path("bar")));
1027        assert!(m.matches(repo_path("foo/bar")));
1028        assert!(m.matches(repo_path("foo/bar/baz")));
1029        assert!(!m.matches(repo_path("foo/baz")));
1030
1031        assert_eq!(
1032            m.visit(RepoPath::root()),
1033            Visit::sets(hashset! {RepoPathComponentBuf::from("foo")}, hashset! {})
1034        );
1035        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
1036        assert_eq!(
1037            m.visit(repo_path("foo")),
1038            Visit::sets(
1039                hashset! {RepoPathComponentBuf::from("bar")},
1040                hashset! {RepoPathComponentBuf::from("bar")}
1041            )
1042        );
1043        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
1044    }
1045}