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).unwrap()
520    }
521
522    fn repo_path_component_buf(value: &str) -> RepoPathComponentBuf {
523        RepoPathComponentBuf::new(value).unwrap()
524    }
525
526    #[test]
527    fn test_nothingmatcher() {
528        let m = NothingMatcher;
529        assert!(!m.matches(repo_path("file")));
530        assert!(!m.matches(repo_path("dir/file")));
531        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
532    }
533
534    #[test]
535    fn test_filesmatcher_empty() {
536        let m = FilesMatcher::new([] as [&RepoPath; 0]);
537        assert!(!m.matches(repo_path("file")));
538        assert!(!m.matches(repo_path("dir/file")));
539        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
540    }
541
542    #[test]
543    fn test_filesmatcher_nonempty() {
544        let m = FilesMatcher::new([
545            repo_path("dir1/subdir1/file1"),
546            repo_path("dir1/subdir1/file2"),
547            repo_path("dir1/subdir2/file3"),
548            repo_path("file4"),
549        ]);
550
551        assert!(!m.matches(repo_path("dir1")));
552        assert!(!m.matches(repo_path("dir1/subdir1")));
553        assert!(m.matches(repo_path("dir1/subdir1/file1")));
554        assert!(m.matches(repo_path("dir1/subdir1/file2")));
555        assert!(!m.matches(repo_path("dir1/subdir1/file3")));
556
557        assert_eq!(
558            m.visit(RepoPath::root()),
559            Visit::sets(
560                hashset! {repo_path_component_buf("dir1")},
561                hashset! {repo_path_component_buf("file4")}
562            )
563        );
564        assert_eq!(
565            m.visit(repo_path("dir1")),
566            Visit::sets(
567                hashset! {
568                    repo_path_component_buf("subdir1"),
569                    repo_path_component_buf("subdir2"),
570                },
571                hashset! {}
572            )
573        );
574        assert_eq!(
575            m.visit(repo_path("dir1/subdir1")),
576            Visit::sets(
577                hashset! {},
578                hashset! {
579                    repo_path_component_buf("file1"),
580                    repo_path_component_buf("file2"),
581                },
582            )
583        );
584        assert_eq!(
585            m.visit(repo_path("dir1/subdir2")),
586            Visit::sets(hashset! {}, hashset! {repo_path_component_buf("file3")})
587        );
588    }
589
590    #[test]
591    fn test_prefixmatcher_empty() {
592        let m = PrefixMatcher::new([] as [&RepoPath; 0]);
593        assert!(!m.matches(repo_path("file")));
594        assert!(!m.matches(repo_path("dir/file")));
595        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
596    }
597
598    #[test]
599    fn test_prefixmatcher_root() {
600        let m = PrefixMatcher::new([RepoPath::root()]);
601        // Matches all files
602        assert!(m.matches(repo_path("file")));
603        assert!(m.matches(repo_path("dir/file")));
604        // Visits all directories
605        assert_eq!(m.visit(RepoPath::root()), Visit::AllRecursively);
606        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
607    }
608
609    #[test]
610    fn test_prefixmatcher_single_prefix() {
611        let m = PrefixMatcher::new([repo_path("foo/bar")]);
612
613        // Parts of the prefix should not match
614        assert!(!m.matches(repo_path("foo")));
615        assert!(!m.matches(repo_path("bar")));
616        // A file matching the prefix exactly should match
617        assert!(m.matches(repo_path("foo/bar")));
618        // Files in subdirectories should match
619        assert!(m.matches(repo_path("foo/bar/baz")));
620        assert!(m.matches(repo_path("foo/bar/baz/qux")));
621        // Sibling files should not match
622        assert!(!m.matches(repo_path("foo/foo")));
623        // An unrooted "foo/bar" should not match
624        assert!(!m.matches(repo_path("bar/foo/bar")));
625
626        // The matcher should only visit directory foo/ in the root (file "foo"
627        // shouldn't be visited)
628        assert_eq!(
629            m.visit(RepoPath::root()),
630            Visit::sets(hashset! {repo_path_component_buf("foo")}, hashset! {})
631        );
632        // Inside parent directory "foo/", both subdirectory "bar" and file "bar" may
633        // match
634        assert_eq!(
635            m.visit(repo_path("foo")),
636            Visit::sets(
637                hashset! {repo_path_component_buf("bar")},
638                hashset! {repo_path_component_buf("bar")}
639            )
640        );
641        // Inside a directory that matches the prefix, everything matches recursively
642        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
643        // Same thing in subdirectories of the prefix
644        assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
645        // Nothing in directories that are siblings of the prefix can match, so don't
646        // visit
647        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
648    }
649
650    #[test]
651    fn test_prefixmatcher_nested_prefixes() {
652        let m = PrefixMatcher::new([repo_path("foo"), repo_path("foo/bar/baz")]);
653
654        assert!(m.matches(repo_path("foo")));
655        assert!(!m.matches(repo_path("bar")));
656        assert!(m.matches(repo_path("foo/bar")));
657        // Matches because the "foo" pattern matches
658        assert!(m.matches(repo_path("foo/baz/foo")));
659
660        assert_eq!(
661            m.visit(RepoPath::root()),
662            Visit::sets(
663                hashset! {repo_path_component_buf("foo")},
664                hashset! {repo_path_component_buf("foo")}
665            )
666        );
667        // Inside a directory that matches the prefix, everything matches recursively
668        assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
669        // Same thing in subdirectories of the prefix
670        assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
671    }
672
673    #[test]
674    fn test_fileglobsmatcher_rooted() {
675        let to_pattern = |s| glob::Pattern::new(s).unwrap();
676
677        let m = FileGlobsMatcher::new([(RepoPath::root(), to_pattern("*.rs"))]);
678        assert!(!m.matches(repo_path("foo")));
679        assert!(m.matches(repo_path("foo.rs")));
680        assert!(!m.matches(repo_path("foo.rss")));
681        assert!(!m.matches(repo_path("foo.rs/bar.rs")));
682        assert!(!m.matches(repo_path("foo/bar.rs")));
683        assert_eq!(
684            m.visit(RepoPath::root()),
685            Visit::Specific {
686                dirs: VisitDirs::All,
687                files: VisitFiles::All
688            }
689        );
690
691        // Multiple patterns at the same directory
692        let m = FileGlobsMatcher::new([
693            (RepoPath::root(), to_pattern("foo?")),
694            (RepoPath::root(), to_pattern("**/*.rs")),
695        ]);
696        assert!(!m.matches(repo_path("foo")));
697        assert!(m.matches(repo_path("foo1")));
698        assert!(!m.matches(repo_path("Foo1")));
699        assert!(!m.matches(repo_path("foo1/foo2")));
700        assert!(m.matches(repo_path("foo.rs")));
701        assert!(m.matches(repo_path("foo.rs/bar.rs")));
702        assert!(m.matches(repo_path("foo/bar.rs")));
703        assert_eq!(
704            m.visit(RepoPath::root()),
705            Visit::Specific {
706                dirs: VisitDirs::All,
707                files: VisitFiles::All
708            }
709        );
710        assert_eq!(
711            m.visit(repo_path("foo")),
712            Visit::Specific {
713                dirs: VisitDirs::All,
714                files: VisitFiles::All
715            }
716        );
717        assert_eq!(
718            m.visit(repo_path("bar/baz")),
719            Visit::Specific {
720                dirs: VisitDirs::All,
721                files: VisitFiles::All
722            }
723        );
724    }
725
726    #[test]
727    fn test_fileglobsmatcher_nested() {
728        let to_pattern = |s| glob::Pattern::new(s).unwrap();
729
730        let m = FileGlobsMatcher::new([
731            (repo_path("foo"), to_pattern("**/*.a")),
732            (repo_path("foo/bar"), to_pattern("*.b")),
733            (repo_path("baz"), to_pattern("?*")),
734        ]);
735        assert!(!m.matches(repo_path("foo")));
736        assert!(m.matches(repo_path("foo/x.a")));
737        assert!(!m.matches(repo_path("foo/x.b")));
738        assert!(m.matches(repo_path("foo/bar/x.a")));
739        assert!(m.matches(repo_path("foo/bar/x.b")));
740        assert!(m.matches(repo_path("foo/bar/baz/x.a")));
741        assert!(!m.matches(repo_path("foo/bar/baz/x.b")));
742        assert!(!m.matches(repo_path("baz")));
743        assert!(m.matches(repo_path("baz/x")));
744        assert_eq!(
745            m.visit(RepoPath::root()),
746            Visit::Specific {
747                dirs: VisitDirs::Set(hashset! {
748                    repo_path_component_buf("foo"),
749                    repo_path_component_buf("baz"),
750                }),
751                files: VisitFiles::Set(hashset! {}),
752            }
753        );
754        assert_eq!(
755            m.visit(repo_path("foo")),
756            Visit::Specific {
757                dirs: VisitDirs::All,
758                files: VisitFiles::All,
759            }
760        );
761        assert_eq!(
762            m.visit(repo_path("foo/bar")),
763            Visit::Specific {
764                dirs: VisitDirs::All,
765                files: VisitFiles::All,
766            }
767        );
768        assert_eq!(
769            m.visit(repo_path("foo/bar/baz")),
770            Visit::Specific {
771                dirs: VisitDirs::All,
772                files: VisitFiles::All,
773            }
774        );
775        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
776        assert_eq!(
777            m.visit(repo_path("baz")),
778            Visit::Specific {
779                dirs: VisitDirs::All,
780                files: VisitFiles::All,
781            }
782        );
783    }
784
785    #[test]
786    fn test_fileglobsmatcher_wildcard_any() {
787        let to_pattern = |s| glob::Pattern::new(s).unwrap();
788
789        // "*" could match the root path, but it doesn't matter since the root
790        // isn't a valid file path.
791        let m = FileGlobsMatcher::new([(RepoPath::root(), to_pattern("*"))]);
792        assert!(!m.matches(RepoPath::root())); // doesn't matter
793        assert!(m.matches(repo_path("x")));
794        assert!(m.matches(repo_path("x.rs")));
795        assert!(!m.matches(repo_path("foo/bar.rs")));
796        assert_eq!(
797            m.visit(RepoPath::root()),
798            Visit::Specific {
799                dirs: VisitDirs::All,
800                files: VisitFiles::All
801            }
802        );
803
804        // "foo/*" shouldn't match "foo"
805        let m = FileGlobsMatcher::new([(repo_path("foo"), to_pattern("*"))]);
806        assert!(!m.matches(RepoPath::root()));
807        assert!(!m.matches(repo_path("foo")));
808        assert!(m.matches(repo_path("foo/x")));
809        assert!(!m.matches(repo_path("foo/bar/baz")));
810        assert_eq!(
811            m.visit(RepoPath::root()),
812            Visit::Specific {
813                dirs: VisitDirs::Set(hashset! {repo_path_component_buf("foo")}),
814                files: VisitFiles::Set(hashset! {}),
815            }
816        );
817        assert_eq!(
818            m.visit(repo_path("foo")),
819            Visit::Specific {
820                dirs: VisitDirs::All,
821                files: VisitFiles::All
822            }
823        );
824        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
825    }
826
827    #[test]
828    fn test_unionmatcher_concatenate_roots() {
829        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
830        let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]);
831        let m = UnionMatcher::new(&m1, &m2);
832
833        assert!(m.matches(repo_path("foo")));
834        assert!(m.matches(repo_path("foo/bar")));
835        assert!(m.matches(repo_path("bar")));
836        assert!(m.matches(repo_path("bar/foo")));
837        assert!(m.matches(repo_path("baz")));
838        assert!(m.matches(repo_path("baz/foo")));
839        assert!(!m.matches(repo_path("qux")));
840        assert!(!m.matches(repo_path("qux/foo")));
841
842        assert_eq!(
843            m.visit(RepoPath::root()),
844            Visit::sets(
845                hashset! {
846                    repo_path_component_buf("foo"),
847                    repo_path_component_buf("bar"),
848                    repo_path_component_buf("baz"),
849                },
850                hashset! {
851                    repo_path_component_buf("foo"),
852                    repo_path_component_buf("bar"),
853                    repo_path_component_buf("baz"),
854                },
855            )
856        );
857        assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
858        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
859        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
860        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
861        assert_eq!(m.visit(repo_path("baz")), Visit::AllRecursively);
862        assert_eq!(m.visit(repo_path("baz/foo")), Visit::AllRecursively);
863        assert_eq!(m.visit(repo_path("qux")), Visit::Nothing);
864        assert_eq!(m.visit(repo_path("qux/foo")), Visit::Nothing);
865    }
866
867    #[test]
868    fn test_unionmatcher_concatenate_subdirs() {
869        let m1 = PrefixMatcher::new([repo_path("common/bar"), repo_path("1/foo")]);
870        let m2 = PrefixMatcher::new([repo_path("common/baz"), repo_path("2/qux")]);
871        let m = UnionMatcher::new(&m1, &m2);
872
873        assert!(!m.matches(repo_path("common")));
874        assert!(!m.matches(repo_path("1")));
875        assert!(!m.matches(repo_path("2")));
876        assert!(m.matches(repo_path("common/bar")));
877        assert!(m.matches(repo_path("common/bar/baz")));
878        assert!(m.matches(repo_path("common/baz")));
879        assert!(m.matches(repo_path("1/foo")));
880        assert!(m.matches(repo_path("1/foo/qux")));
881        assert!(m.matches(repo_path("2/qux")));
882        assert!(!m.matches(repo_path("2/quux")));
883
884        assert_eq!(
885            m.visit(RepoPath::root()),
886            Visit::sets(
887                hashset! {
888                    repo_path_component_buf("common"),
889                    repo_path_component_buf("1"),
890                    repo_path_component_buf("2"),
891                },
892                hashset! {},
893            )
894        );
895        assert_eq!(
896            m.visit(repo_path("common")),
897            Visit::sets(
898                hashset! {
899                    repo_path_component_buf("bar"),
900                    repo_path_component_buf("baz"),
901                },
902                hashset! {
903                    repo_path_component_buf("bar"),
904                    repo_path_component_buf("baz"),
905                },
906            )
907        );
908        assert_eq!(
909            m.visit(repo_path("1")),
910            Visit::sets(
911                hashset! {repo_path_component_buf("foo")},
912                hashset! {repo_path_component_buf("foo")},
913            )
914        );
915        assert_eq!(
916            m.visit(repo_path("2")),
917            Visit::sets(
918                hashset! {repo_path_component_buf("qux")},
919                hashset! {repo_path_component_buf("qux")},
920            )
921        );
922        assert_eq!(m.visit(repo_path("common/bar")), Visit::AllRecursively);
923        assert_eq!(m.visit(repo_path("1/foo")), Visit::AllRecursively);
924        assert_eq!(m.visit(repo_path("2/qux")), Visit::AllRecursively);
925        assert_eq!(m.visit(repo_path("2/quux")), Visit::Nothing);
926    }
927
928    #[test]
929    fn test_differencematcher_remove_subdir() {
930        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
931        let m2 = PrefixMatcher::new([repo_path("foo/bar")]);
932        let m = DifferenceMatcher::new(&m1, &m2);
933
934        assert!(m.matches(repo_path("foo")));
935        assert!(!m.matches(repo_path("foo/bar")));
936        assert!(!m.matches(repo_path("foo/bar/baz")));
937        assert!(m.matches(repo_path("foo/baz")));
938        assert!(m.matches(repo_path("bar")));
939
940        assert_eq!(
941            m.visit(RepoPath::root()),
942            Visit::sets(
943                hashset! {
944                    repo_path_component_buf("foo"),
945                    repo_path_component_buf("bar"),
946                },
947                hashset! {
948                    repo_path_component_buf("foo"),
949                    repo_path_component_buf("bar"),
950                },
951            )
952        );
953        assert_eq!(
954            m.visit(repo_path("foo")),
955            Visit::Specific {
956                dirs: VisitDirs::All,
957                files: VisitFiles::All,
958            }
959        );
960        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
961        assert_eq!(m.visit(repo_path("foo/baz")), Visit::AllRecursively);
962        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
963    }
964
965    #[test]
966    fn test_differencematcher_shared_patterns() {
967        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
968        let m2 = PrefixMatcher::new([repo_path("foo")]);
969        let m = DifferenceMatcher::new(&m1, &m2);
970
971        assert!(!m.matches(repo_path("foo")));
972        assert!(!m.matches(repo_path("foo/bar")));
973        assert!(m.matches(repo_path("bar")));
974        assert!(m.matches(repo_path("bar/foo")));
975
976        assert_eq!(
977            m.visit(RepoPath::root()),
978            Visit::sets(
979                hashset! {
980                    repo_path_component_buf("foo"),
981                    repo_path_component_buf("bar"),
982                },
983                hashset! {
984                    repo_path_component_buf("foo"),
985                    repo_path_component_buf("bar"),
986                },
987            )
988        );
989        assert_eq!(m.visit(repo_path("foo")), Visit::Nothing);
990        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
991        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
992        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
993    }
994
995    #[test]
996    fn test_intersectionmatcher_intersecting_roots() {
997        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
998        let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]);
999        let m = IntersectionMatcher::new(&m1, &m2);
1000
1001        assert!(!m.matches(repo_path("foo")));
1002        assert!(!m.matches(repo_path("foo/bar")));
1003        assert!(m.matches(repo_path("bar")));
1004        assert!(m.matches(repo_path("bar/foo")));
1005        assert!(!m.matches(repo_path("baz")));
1006        assert!(!m.matches(repo_path("baz/foo")));
1007
1008        assert_eq!(
1009            m.visit(RepoPath::root()),
1010            Visit::sets(
1011                hashset! {repo_path_component_buf("bar")},
1012                hashset! {repo_path_component_buf("bar")}
1013            )
1014        );
1015        assert_eq!(m.visit(repo_path("foo")), Visit::Nothing);
1016        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
1017        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
1018        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
1019        assert_eq!(m.visit(repo_path("baz")), Visit::Nothing);
1020        assert_eq!(m.visit(repo_path("baz/foo")), Visit::Nothing);
1021    }
1022
1023    #[test]
1024    fn test_intersectionmatcher_subdir() {
1025        let m1 = PrefixMatcher::new([repo_path("foo")]);
1026        let m2 = PrefixMatcher::new([repo_path("foo/bar")]);
1027        let m = IntersectionMatcher::new(&m1, &m2);
1028
1029        assert!(!m.matches(repo_path("foo")));
1030        assert!(!m.matches(repo_path("bar")));
1031        assert!(m.matches(repo_path("foo/bar")));
1032        assert!(m.matches(repo_path("foo/bar/baz")));
1033        assert!(!m.matches(repo_path("foo/baz")));
1034
1035        assert_eq!(
1036            m.visit(RepoPath::root()),
1037            Visit::sets(hashset! {repo_path_component_buf("foo")}, hashset! {})
1038        );
1039        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
1040        assert_eq!(
1041            m.visit(repo_path("foo")),
1042            Visit::sets(
1043                hashset! {repo_path_component_buf("bar")},
1044                hashset! {repo_path_component_buf("bar")}
1045            )
1046        );
1047        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
1048    }
1049}