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#![expect(missing_docs)]
16
17use std::collections::HashMap;
18use std::collections::HashSet;
19use std::fmt;
20use std::fmt::Debug;
21use std::iter;
22
23use globset::Glob;
24use itertools::Itertools as _;
25use tracing::instrument;
26
27use crate::repo_path::RepoPath;
28use crate::repo_path::RepoPathComponentBuf;
29
30#[derive(PartialEq, Eq, Debug)]
31pub enum Visit {
32    /// Everything in the directory is *guaranteed* to match, no need to check
33    /// descendants
34    AllRecursively,
35    Specific {
36        dirs: VisitDirs,
37        files: VisitFiles,
38    },
39    /// Nothing in the directory or its subdirectories will match.
40    ///
41    /// This is the same as `Specific` with no directories or files. Use
42    /// `Visit::set()` to get create an instance that's `Specific` or
43    /// `Nothing` depending on the values at runtime.
44    Nothing,
45}
46
47impl Visit {
48    fn sets(dirs: HashSet<RepoPathComponentBuf>, files: HashSet<RepoPathComponentBuf>) -> Self {
49        if dirs.is_empty() && files.is_empty() {
50            Self::Nothing
51        } else {
52            Self::Specific {
53                dirs: VisitDirs::Set(dirs),
54                files: VisitFiles::Set(files),
55            }
56        }
57    }
58
59    pub fn is_nothing(&self) -> bool {
60        *self == Self::Nothing
61    }
62}
63
64#[derive(PartialEq, Eq, Debug)]
65pub enum VisitDirs {
66    All,
67    Set(HashSet<RepoPathComponentBuf>),
68}
69
70#[derive(PartialEq, Eq, Debug)]
71pub enum VisitFiles {
72    All,
73    Set(HashSet<RepoPathComponentBuf>),
74}
75
76pub trait Matcher: Debug + Send + Sync {
77    fn matches(&self, file: &RepoPath) -> bool;
78    fn visit(&self, dir: &RepoPath) -> Visit;
79}
80
81impl<T: Matcher + ?Sized> Matcher for &T {
82    fn matches(&self, file: &RepoPath) -> bool {
83        <T as Matcher>::matches(self, file)
84    }
85
86    fn visit(&self, dir: &RepoPath) -> Visit {
87        <T as Matcher>::visit(self, dir)
88    }
89}
90
91impl<T: Matcher + ?Sized> Matcher for Box<T> {
92    fn matches(&self, file: &RepoPath) -> bool {
93        <T as Matcher>::matches(self, file)
94    }
95
96    fn visit(&self, dir: &RepoPath) -> Visit {
97        <T as Matcher>::visit(self, dir)
98    }
99}
100
101#[derive(PartialEq, Eq, Debug)]
102pub struct NothingMatcher;
103
104impl Matcher for NothingMatcher {
105    fn matches(&self, _file: &RepoPath) -> bool {
106        false
107    }
108
109    fn visit(&self, _dir: &RepoPath) -> Visit {
110        Visit::Nothing
111    }
112}
113
114#[derive(PartialEq, Eq, Debug)]
115pub struct EverythingMatcher;
116
117impl Matcher for EverythingMatcher {
118    fn matches(&self, _file: &RepoPath) -> bool {
119        true
120    }
121
122    fn visit(&self, _dir: &RepoPath) -> Visit {
123        Visit::AllRecursively
124    }
125}
126
127#[derive(PartialEq, Eq, Debug)]
128pub struct FilesMatcher {
129    tree: RepoPathTree<FilesNodeKind>,
130}
131
132impl FilesMatcher {
133    pub fn new(files: impl IntoIterator<Item = impl AsRef<RepoPath>>) -> Self {
134        let mut tree = RepoPathTree::default();
135        for f in files {
136            tree.add(f.as_ref()).value = FilesNodeKind::File;
137        }
138        Self { tree }
139    }
140}
141
142impl Matcher for FilesMatcher {
143    fn matches(&self, file: &RepoPath) -> bool {
144        self.tree
145            .get(file)
146            .is_some_and(|sub| sub.value == FilesNodeKind::File)
147    }
148
149    fn visit(&self, dir: &RepoPath) -> Visit {
150        self.tree
151            .get(dir)
152            .map_or(Visit::Nothing, files_tree_to_visit_sets)
153    }
154}
155
156#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
157enum FilesNodeKind {
158    /// Represents an intermediate directory.
159    #[default]
160    Dir,
161    /// Represents a file (which might also be an intermediate directory.)
162    File,
163}
164
165fn files_tree_to_visit_sets(tree: &RepoPathTree<FilesNodeKind>) -> Visit {
166    let mut dirs = HashSet::new();
167    let mut files = HashSet::new();
168    for (name, sub) in &tree.entries {
169        // should visit only intermediate directories
170        if !sub.entries.is_empty() {
171            dirs.insert(name.clone());
172        }
173        if sub.value == FilesNodeKind::File {
174            files.insert(name.clone());
175        }
176    }
177    Visit::sets(dirs, files)
178}
179
180#[derive(Debug)]
181pub struct PrefixMatcher {
182    tree: RepoPathTree<PrefixNodeKind>,
183}
184
185impl PrefixMatcher {
186    #[instrument(skip(prefixes))]
187    pub fn new(prefixes: impl IntoIterator<Item = impl AsRef<RepoPath>>) -> Self {
188        let mut tree = RepoPathTree::default();
189        for prefix in prefixes {
190            tree.add(prefix.as_ref()).value = PrefixNodeKind::Prefix;
191        }
192        Self { tree }
193    }
194}
195
196impl Matcher for PrefixMatcher {
197    fn matches(&self, file: &RepoPath) -> bool {
198        self.tree
199            .walk_to(file)
200            .any(|(sub, _)| sub.value == PrefixNodeKind::Prefix)
201    }
202
203    fn visit(&self, dir: &RepoPath) -> Visit {
204        for (sub, tail_path) in self.tree.walk_to(dir) {
205            // ancestor of 'dir' matches prefix paths
206            if sub.value == PrefixNodeKind::Prefix {
207                return Visit::AllRecursively;
208            }
209            // 'dir' found, and is an ancestor of prefix paths
210            if tail_path.is_root() {
211                return prefix_tree_to_visit_sets(sub);
212            }
213        }
214        Visit::Nothing
215    }
216}
217
218#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
219enum PrefixNodeKind {
220    /// Represents an intermediate directory.
221    #[default]
222    Dir,
223    /// Represents a file and prefix directory.
224    Prefix,
225}
226
227fn prefix_tree_to_visit_sets(tree: &RepoPathTree<PrefixNodeKind>) -> Visit {
228    let mut dirs = HashSet::new();
229    let mut files = HashSet::new();
230    for (name, sub) in &tree.entries {
231        // should visit both intermediate and prefix directories
232        dirs.insert(name.clone());
233        if sub.value == PrefixNodeKind::Prefix {
234            files.insert(name.clone());
235        }
236    }
237    Visit::sets(dirs, files)
238}
239
240/// Matches file paths with glob patterns.
241#[derive(Clone, Debug)]
242pub struct GlobsMatcher {
243    tree: RepoPathTree<Option<regex::bytes::RegexSet>>,
244}
245
246impl GlobsMatcher {
247    /// Returns new matcher builder.
248    pub fn builder<'a>() -> GlobsMatcherBuilder<'a> {
249        GlobsMatcherBuilder {
250            dir_patterns: vec![],
251        }
252    }
253}
254
255impl Matcher for GlobsMatcher {
256    fn matches(&self, file: &RepoPath) -> bool {
257        // check if any ancestor (dir, patterns) matches 'file'
258        self.tree
259            .walk_to(file)
260            .take_while(|(_, tail_path)| !tail_path.is_root()) // only dirs
261            .any(|(sub, tail_path)| {
262                let tail = tail_path.as_internal_file_string().as_bytes();
263                sub.value.as_ref().is_some_and(|pat| pat.is_match(tail))
264            })
265    }
266
267    fn visit(&self, dir: &RepoPath) -> Visit {
268        for (sub, tail_path) in self.tree.walk_to(dir) {
269            // ancestor of 'dir' has patterns, can't narrow visit anymore
270            if sub.value.is_some() {
271                return Visit::Specific {
272                    dirs: VisitDirs::All,
273                    files: VisitFiles::All,
274                };
275            }
276            // 'dir' found, and is an ancestor of pattern paths
277            if tail_path.is_root() {
278                let sub_dirs = sub.entries.keys().cloned().collect();
279                return Visit::sets(sub_dirs, HashSet::new());
280            }
281        }
282        Visit::Nothing
283    }
284}
285
286/// Constructs [`GlobsMatcher`] from patterns.
287#[derive(Clone, Debug)]
288pub struct GlobsMatcherBuilder<'a> {
289    dir_patterns: Vec<(&'a RepoPath, &'a Glob)>,
290}
291
292impl<'a> GlobsMatcherBuilder<'a> {
293    /// Returns true if no patterns have been added yet.
294    pub fn is_empty(&self) -> bool {
295        self.dir_patterns.is_empty()
296    }
297
298    /// Adds `pattern` that should be evaluated relative to `dir`.
299    ///
300    /// The `dir` should be the longest directory path that contains no glob
301    /// meta characters.
302    pub fn add(&mut self, dir: &'a RepoPath, pattern: &'a Glob) {
303        self.dir_patterns.push((dir, pattern));
304    }
305
306    /// Compiles matcher.
307    pub fn build(self) -> GlobsMatcher {
308        let Self { mut dir_patterns } = self;
309        dir_patterns.sort_unstable_by_key(|&(dir, _)| dir);
310
311        let mut tree: RepoPathTree<Option<regex::bytes::RegexSet>> = Default::default();
312        for (dir, chunk) in &dir_patterns.into_iter().chunk_by(|&(dir, _)| dir) {
313            // Based on new_regex() in globset. We don't use GlobSet because
314            // RepoPath separator should be "/" on all platforms.
315            let regex =
316                regex::bytes::RegexSetBuilder::new(chunk.map(|(_, pattern)| pattern.regex()))
317                    .dot_matches_new_line(true)
318                    .build()
319                    .expect("glob regex should be valid");
320            let sub = tree.add(dir);
321            assert!(sub.value.is_none());
322            sub.value = Some(regex);
323        }
324
325        GlobsMatcher { tree }
326    }
327}
328
329/// Matches paths that are matched by any of the input matchers.
330#[derive(Clone, Debug)]
331pub struct UnionMatcher<M1, M2> {
332    input1: M1,
333    input2: M2,
334}
335
336impl<M1: Matcher, M2: Matcher> UnionMatcher<M1, M2> {
337    pub fn new(input1: M1, input2: M2) -> Self {
338        Self { input1, input2 }
339    }
340}
341
342impl<M1: Matcher, M2: Matcher> Matcher for UnionMatcher<M1, M2> {
343    fn matches(&self, file: &RepoPath) -> bool {
344        self.input1.matches(file) || self.input2.matches(file)
345    }
346
347    fn visit(&self, dir: &RepoPath) -> Visit {
348        match self.input1.visit(dir) {
349            Visit::AllRecursively => Visit::AllRecursively,
350            Visit::Nothing => self.input2.visit(dir),
351            Visit::Specific {
352                dirs: dirs1,
353                files: files1,
354            } => match self.input2.visit(dir) {
355                Visit::AllRecursively => Visit::AllRecursively,
356                Visit::Nothing => Visit::Specific {
357                    dirs: dirs1,
358                    files: files1,
359                },
360                Visit::Specific {
361                    dirs: dirs2,
362                    files: files2,
363                } => {
364                    let dirs = match (dirs1, dirs2) {
365                        (VisitDirs::All, _) | (_, VisitDirs::All) => VisitDirs::All,
366                        (VisitDirs::Set(dirs1), VisitDirs::Set(dirs2)) => {
367                            VisitDirs::Set(dirs1.iter().chain(&dirs2).cloned().collect())
368                        }
369                    };
370                    let files = match (files1, files2) {
371                        (VisitFiles::All, _) | (_, VisitFiles::All) => VisitFiles::All,
372                        (VisitFiles::Set(files1), VisitFiles::Set(files2)) => {
373                            VisitFiles::Set(files1.iter().chain(&files2).cloned().collect())
374                        }
375                    };
376                    Visit::Specific { dirs, files }
377                }
378            },
379        }
380    }
381}
382
383/// Matches paths that are matched by the first input matcher but not by the
384/// second.
385#[derive(Clone, Debug)]
386pub struct DifferenceMatcher<M1, M2> {
387    /// The minuend
388    wanted: M1,
389    /// The subtrahend
390    unwanted: M2,
391}
392
393impl<M1: Matcher, M2: Matcher> DifferenceMatcher<M1, M2> {
394    pub fn new(wanted: M1, unwanted: M2) -> Self {
395        Self { wanted, unwanted }
396    }
397}
398
399impl<M1: Matcher, M2: Matcher> Matcher for DifferenceMatcher<M1, M2> {
400    fn matches(&self, file: &RepoPath) -> bool {
401        self.wanted.matches(file) && !self.unwanted.matches(file)
402    }
403
404    fn visit(&self, dir: &RepoPath) -> Visit {
405        match self.unwanted.visit(dir) {
406            Visit::AllRecursively => Visit::Nothing,
407            Visit::Nothing => self.wanted.visit(dir),
408            Visit::Specific { .. } => match self.wanted.visit(dir) {
409                Visit::AllRecursively => Visit::Specific {
410                    dirs: VisitDirs::All,
411                    files: VisitFiles::All,
412                },
413                wanted_visit => wanted_visit,
414            },
415        }
416    }
417}
418
419/// Matches paths that are matched by both input matchers.
420#[derive(Clone, Debug)]
421pub struct IntersectionMatcher<M1, M2> {
422    input1: M1,
423    input2: M2,
424}
425
426impl<M1: Matcher, M2: Matcher> IntersectionMatcher<M1, M2> {
427    pub fn new(input1: M1, input2: M2) -> Self {
428        Self { input1, input2 }
429    }
430}
431
432impl<M1: Matcher, M2: Matcher> Matcher for IntersectionMatcher<M1, M2> {
433    fn matches(&self, file: &RepoPath) -> bool {
434        self.input1.matches(file) && self.input2.matches(file)
435    }
436
437    fn visit(&self, dir: &RepoPath) -> Visit {
438        match self.input1.visit(dir) {
439            Visit::AllRecursively => self.input2.visit(dir),
440            Visit::Nothing => Visit::Nothing,
441            Visit::Specific {
442                dirs: dirs1,
443                files: files1,
444            } => match self.input2.visit(dir) {
445                Visit::AllRecursively => Visit::Specific {
446                    dirs: dirs1,
447                    files: files1,
448                },
449                Visit::Nothing => Visit::Nothing,
450                Visit::Specific {
451                    dirs: dirs2,
452                    files: files2,
453                } => {
454                    let dirs = match (dirs1, dirs2) {
455                        (VisitDirs::All, VisitDirs::All) => VisitDirs::All,
456                        (dirs1, VisitDirs::All) => dirs1,
457                        (VisitDirs::All, dirs2) => dirs2,
458                        (VisitDirs::Set(dirs1), VisitDirs::Set(dirs2)) => {
459                            VisitDirs::Set(dirs1.intersection(&dirs2).cloned().collect())
460                        }
461                    };
462                    let files = match (files1, files2) {
463                        (VisitFiles::All, VisitFiles::All) => VisitFiles::All,
464                        (files1, VisitFiles::All) => files1,
465                        (VisitFiles::All, files2) => files2,
466                        (VisitFiles::Set(files1), VisitFiles::Set(files2)) => {
467                            VisitFiles::Set(files1.intersection(&files2).cloned().collect())
468                        }
469                    };
470                    match (&dirs, &files) {
471                        (VisitDirs::Set(dirs), VisitFiles::Set(files))
472                            if dirs.is_empty() && files.is_empty() =>
473                        {
474                            Visit::Nothing
475                        }
476                        _ => Visit::Specific { dirs, files },
477                    }
478                }
479            },
480        }
481    }
482}
483
484/// Tree that maps `RepoPath` to value of type `V`.
485#[derive(Clone, Default, Eq, PartialEq)]
486struct RepoPathTree<V> {
487    entries: HashMap<RepoPathComponentBuf, Self>,
488    value: V,
489}
490
491impl<V> RepoPathTree<V> {
492    fn add(&mut self, dir: &RepoPath) -> &mut Self
493    where
494        V: Default,
495    {
496        dir.components().fold(self, |sub, name| {
497            // Avoid name.clone() if entry already exists.
498            if !sub.entries.contains_key(name) {
499                sub.entries.insert(name.to_owned(), Self::default());
500            }
501            sub.entries.get_mut(name).unwrap()
502        })
503    }
504
505    fn get(&self, dir: &RepoPath) -> Option<&Self> {
506        dir.components()
507            .try_fold(self, |sub, name| sub.entries.get(name))
508    }
509
510    /// Walks the tree from the root to the given `dir`, yielding each sub tree
511    /// and remaining path.
512    fn walk_to<'a, 'b>(
513        &'a self,
514        dir: &'b RepoPath,
515    ) -> impl Iterator<Item = (&'a Self, &'b RepoPath)> {
516        iter::successors(Some((self, dir)), |(sub, dir)| {
517            let mut components = dir.components();
518            let name = components.next()?;
519            Some((sub.entries.get(name)?, components.as_path()))
520        })
521    }
522}
523
524impl<V: Debug> Debug for RepoPathTree<V> {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        self.value.fmt(f)?;
527        f.write_str(" ")?;
528        f.debug_map()
529            .entries(
530                self.entries
531                    .iter()
532                    .sorted_unstable_by_key(|&(name, _)| name),
533            )
534            .finish()
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use maplit::hashset;
541
542    use super::*;
543    use crate::fileset::parse_file_glob;
544
545    fn repo_path(value: &str) -> &RepoPath {
546        RepoPath::from_internal_string(value).unwrap()
547    }
548
549    fn repo_path_component_buf(value: &str) -> RepoPathComponentBuf {
550        RepoPathComponentBuf::new(value).unwrap()
551    }
552
553    fn glob(s: &str) -> Glob {
554        let icase = false;
555        parse_file_glob(s, icase).unwrap()
556    }
557
558    fn new_file_globs_matcher(dir_patterns: &[(&RepoPath, Glob)]) -> GlobsMatcher {
559        let mut builder = GlobsMatcher::builder();
560        for (dir, pattern) in dir_patterns {
561            builder.add(dir, pattern);
562        }
563        builder.build()
564    }
565
566    #[test]
567    fn test_nothing_matcher() {
568        let m = NothingMatcher;
569        assert!(!m.matches(repo_path("file")));
570        assert!(!m.matches(repo_path("dir/file")));
571        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
572    }
573
574    #[test]
575    fn test_files_matcher_empty() {
576        let m = FilesMatcher::new([] as [&RepoPath; 0]);
577        assert!(!m.matches(repo_path("file")));
578        assert!(!m.matches(repo_path("dir/file")));
579        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
580    }
581
582    #[test]
583    fn test_files_matcher_nonempty() {
584        let m = FilesMatcher::new([
585            repo_path("dir1/subdir1/file1"),
586            repo_path("dir1/subdir1/file2"),
587            repo_path("dir1/subdir2/file3"),
588            repo_path("file4"),
589        ]);
590
591        assert!(!m.matches(repo_path("dir1")));
592        assert!(!m.matches(repo_path("dir1/subdir1")));
593        assert!(m.matches(repo_path("dir1/subdir1/file1")));
594        assert!(m.matches(repo_path("dir1/subdir1/file2")));
595        assert!(!m.matches(repo_path("dir1/subdir1/file3")));
596
597        assert_eq!(
598            m.visit(RepoPath::root()),
599            Visit::sets(
600                hashset! {repo_path_component_buf("dir1")},
601                hashset! {repo_path_component_buf("file4")}
602            )
603        );
604        assert_eq!(
605            m.visit(repo_path("dir1")),
606            Visit::sets(
607                hashset! {
608                    repo_path_component_buf("subdir1"),
609                    repo_path_component_buf("subdir2"),
610                },
611                hashset! {}
612            )
613        );
614        assert_eq!(
615            m.visit(repo_path("dir1/subdir1")),
616            Visit::sets(
617                hashset! {},
618                hashset! {
619                    repo_path_component_buf("file1"),
620                    repo_path_component_buf("file2"),
621                },
622            )
623        );
624        assert_eq!(
625            m.visit(repo_path("dir1/subdir2")),
626            Visit::sets(hashset! {}, hashset! {repo_path_component_buf("file3")})
627        );
628    }
629
630    #[test]
631    fn test_prefix_matcher_empty() {
632        let m = PrefixMatcher::new([] as [&RepoPath; 0]);
633        assert!(!m.matches(repo_path("file")));
634        assert!(!m.matches(repo_path("dir/file")));
635        assert_eq!(m.visit(RepoPath::root()), Visit::Nothing);
636    }
637
638    #[test]
639    fn test_prefix_matcher_root() {
640        let m = PrefixMatcher::new([RepoPath::root()]);
641        // Matches all files
642        assert!(m.matches(repo_path("file")));
643        assert!(m.matches(repo_path("dir/file")));
644        // Visits all directories
645        assert_eq!(m.visit(RepoPath::root()), Visit::AllRecursively);
646        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
647    }
648
649    #[test]
650    fn test_prefix_matcher_single_prefix() {
651        let m = PrefixMatcher::new([repo_path("foo/bar")]);
652
653        // Parts of the prefix should not match
654        assert!(!m.matches(repo_path("foo")));
655        assert!(!m.matches(repo_path("bar")));
656        // A file matching the prefix exactly should match
657        assert!(m.matches(repo_path("foo/bar")));
658        // Files in subdirectories should match
659        assert!(m.matches(repo_path("foo/bar/baz")));
660        assert!(m.matches(repo_path("foo/bar/baz/qux")));
661        // Sibling files should not match
662        assert!(!m.matches(repo_path("foo/foo")));
663        // An unrooted "foo/bar" should not match
664        assert!(!m.matches(repo_path("bar/foo/bar")));
665
666        // The matcher should only visit directory foo/ in the root (file "foo"
667        // shouldn't be visited)
668        assert_eq!(
669            m.visit(RepoPath::root()),
670            Visit::sets(hashset! {repo_path_component_buf("foo")}, hashset! {})
671        );
672        // Inside parent directory "foo/", both subdirectory "bar" and file "bar" may
673        // match
674        assert_eq!(
675            m.visit(repo_path("foo")),
676            Visit::sets(
677                hashset! {repo_path_component_buf("bar")},
678                hashset! {repo_path_component_buf("bar")}
679            )
680        );
681        // Inside a directory that matches the prefix, everything matches recursively
682        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
683        // Same thing in subdirectories of the prefix
684        assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
685        // Nothing in directories that are siblings of the prefix can match, so don't
686        // visit
687        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
688    }
689
690    #[test]
691    fn test_prefix_matcher_nested_prefixes() {
692        let m = PrefixMatcher::new([repo_path("foo"), repo_path("foo/bar/baz")]);
693
694        assert!(m.matches(repo_path("foo")));
695        assert!(!m.matches(repo_path("bar")));
696        assert!(m.matches(repo_path("foo/bar")));
697        // Matches because the "foo" pattern matches
698        assert!(m.matches(repo_path("foo/baz/foo")));
699
700        assert_eq!(
701            m.visit(RepoPath::root()),
702            Visit::sets(
703                hashset! {repo_path_component_buf("foo")},
704                hashset! {repo_path_component_buf("foo")}
705            )
706        );
707        // Inside a directory that matches the prefix, everything matches recursively
708        assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
709        // Same thing in subdirectories of the prefix
710        assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively);
711    }
712
713    #[test]
714    fn test_file_globs_matcher_rooted() {
715        let m = new_file_globs_matcher(&[(RepoPath::root(), glob("*.rs"))]);
716        assert!(!m.matches(repo_path("foo")));
717        assert!(m.matches(repo_path("foo.rs")));
718        assert!(m.matches(repo_path("foo\n.rs"))); // "*" matches newline
719        assert!(!m.matches(repo_path("foo.rss")));
720        assert!(!m.matches(repo_path("foo.rs/bar.rs")));
721        assert!(!m.matches(repo_path("foo/bar.rs")));
722        assert_eq!(
723            m.visit(RepoPath::root()),
724            Visit::Specific {
725                dirs: VisitDirs::All,
726                files: VisitFiles::All
727            }
728        );
729
730        // Multiple patterns at the same directory
731        let m = new_file_globs_matcher(&[
732            (RepoPath::root(), glob("foo?")),
733            (repo_path("other"), glob("")),
734            (RepoPath::root(), glob("**/*.rs")),
735        ]);
736        assert!(!m.matches(repo_path("foo")));
737        assert!(m.matches(repo_path("foo1")));
738        assert!(!m.matches(repo_path("Foo1")));
739        assert!(!m.matches(repo_path("foo1/foo2")));
740        assert!(m.matches(repo_path("foo.rs")));
741        assert!(m.matches(repo_path("foo.rs/bar.rs")));
742        assert!(m.matches(repo_path("foo/bar.rs")));
743        assert_eq!(
744            m.visit(RepoPath::root()),
745            Visit::Specific {
746                dirs: VisitDirs::All,
747                files: VisitFiles::All
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("bar/baz")),
759            Visit::Specific {
760                dirs: VisitDirs::All,
761                files: VisitFiles::All
762            }
763        );
764    }
765
766    #[test]
767    fn test_file_globs_matcher_nested() {
768        let m = new_file_globs_matcher(&[
769            (repo_path("foo"), glob("**/*.a")),
770            (repo_path("foo/bar"), glob("*.b")),
771            (repo_path("baz"), glob("?*")),
772        ]);
773        assert!(!m.matches(repo_path("foo")));
774        assert!(m.matches(repo_path("foo/x.a")));
775        assert!(!m.matches(repo_path("foo/x.b")));
776        assert!(m.matches(repo_path("foo/bar/x.a")));
777        assert!(m.matches(repo_path("foo/bar/x.b")));
778        assert!(m.matches(repo_path("foo/bar/baz/x.a")));
779        assert!(!m.matches(repo_path("foo/bar/baz/x.b")));
780        assert!(!m.matches(repo_path("baz")));
781        assert!(m.matches(repo_path("baz/x")));
782        assert_eq!(
783            m.visit(RepoPath::root()),
784            Visit::Specific {
785                dirs: VisitDirs::Set(hashset! {
786                    repo_path_component_buf("foo"),
787                    repo_path_component_buf("baz"),
788                }),
789                files: VisitFiles::Set(hashset! {}),
790            }
791        );
792        assert_eq!(
793            m.visit(repo_path("foo")),
794            Visit::Specific {
795                dirs: VisitDirs::All,
796                files: VisitFiles::All,
797            }
798        );
799        assert_eq!(
800            m.visit(repo_path("foo/bar")),
801            Visit::Specific {
802                dirs: VisitDirs::All,
803                files: VisitFiles::All,
804            }
805        );
806        assert_eq!(
807            m.visit(repo_path("foo/bar/baz")),
808            Visit::Specific {
809                dirs: VisitDirs::All,
810                files: VisitFiles::All,
811            }
812        );
813        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
814        assert_eq!(
815            m.visit(repo_path("baz")),
816            Visit::Specific {
817                dirs: VisitDirs::All,
818                files: VisitFiles::All,
819            }
820        );
821    }
822
823    #[test]
824    fn test_file_globs_matcher_wildcard_any() {
825        // "*" could match the root path, but it doesn't matter since the root
826        // isn't a valid file path.
827        let m = new_file_globs_matcher(&[(RepoPath::root(), glob("*"))]);
828        assert!(!m.matches(RepoPath::root())); // doesn't matter
829        assert!(m.matches(repo_path("x")));
830        assert!(m.matches(repo_path("x.rs")));
831        assert!(!m.matches(repo_path("foo/bar.rs")));
832        assert_eq!(
833            m.visit(RepoPath::root()),
834            Visit::Specific {
835                dirs: VisitDirs::All,
836                files: VisitFiles::All
837            }
838        );
839
840        // "foo/*" shouldn't match "foo"
841        let m = new_file_globs_matcher(&[(repo_path("foo"), glob("*"))]);
842        assert!(!m.matches(RepoPath::root()));
843        assert!(!m.matches(repo_path("foo")));
844        assert!(m.matches(repo_path("foo/x")));
845        assert!(!m.matches(repo_path("foo/bar/baz")));
846        assert_eq!(
847            m.visit(RepoPath::root()),
848            Visit::Specific {
849                dirs: VisitDirs::Set(hashset! {repo_path_component_buf("foo")}),
850                files: VisitFiles::Set(hashset! {}),
851            }
852        );
853        assert_eq!(
854            m.visit(repo_path("foo")),
855            Visit::Specific {
856                dirs: VisitDirs::All,
857                files: VisitFiles::All
858            }
859        );
860        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
861    }
862
863    #[test]
864    fn test_union_matcher_concatenate_roots() {
865        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
866        let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]);
867        let m = UnionMatcher::new(&m1, &m2);
868
869        assert!(m.matches(repo_path("foo")));
870        assert!(m.matches(repo_path("foo/bar")));
871        assert!(m.matches(repo_path("bar")));
872        assert!(m.matches(repo_path("bar/foo")));
873        assert!(m.matches(repo_path("baz")));
874        assert!(m.matches(repo_path("baz/foo")));
875        assert!(!m.matches(repo_path("qux")));
876        assert!(!m.matches(repo_path("qux/foo")));
877
878        assert_eq!(
879            m.visit(RepoPath::root()),
880            Visit::sets(
881                hashset! {
882                    repo_path_component_buf("foo"),
883                    repo_path_component_buf("bar"),
884                    repo_path_component_buf("baz"),
885                },
886                hashset! {
887                    repo_path_component_buf("foo"),
888                    repo_path_component_buf("bar"),
889                    repo_path_component_buf("baz"),
890                },
891            )
892        );
893        assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively);
894        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
895        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
896        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
897        assert_eq!(m.visit(repo_path("baz")), Visit::AllRecursively);
898        assert_eq!(m.visit(repo_path("baz/foo")), Visit::AllRecursively);
899        assert_eq!(m.visit(repo_path("qux")), Visit::Nothing);
900        assert_eq!(m.visit(repo_path("qux/foo")), Visit::Nothing);
901    }
902
903    #[test]
904    fn test_union_matcher_concatenate_subdirs() {
905        let m1 = PrefixMatcher::new([repo_path("common/bar"), repo_path("1/foo")]);
906        let m2 = PrefixMatcher::new([repo_path("common/baz"), repo_path("2/qux")]);
907        let m = UnionMatcher::new(&m1, &m2);
908
909        assert!(!m.matches(repo_path("common")));
910        assert!(!m.matches(repo_path("1")));
911        assert!(!m.matches(repo_path("2")));
912        assert!(m.matches(repo_path("common/bar")));
913        assert!(m.matches(repo_path("common/bar/baz")));
914        assert!(m.matches(repo_path("common/baz")));
915        assert!(m.matches(repo_path("1/foo")));
916        assert!(m.matches(repo_path("1/foo/qux")));
917        assert!(m.matches(repo_path("2/qux")));
918        assert!(!m.matches(repo_path("2/quux")));
919
920        assert_eq!(
921            m.visit(RepoPath::root()),
922            Visit::sets(
923                hashset! {
924                    repo_path_component_buf("common"),
925                    repo_path_component_buf("1"),
926                    repo_path_component_buf("2"),
927                },
928                hashset! {},
929            )
930        );
931        assert_eq!(
932            m.visit(repo_path("common")),
933            Visit::sets(
934                hashset! {
935                    repo_path_component_buf("bar"),
936                    repo_path_component_buf("baz"),
937                },
938                hashset! {
939                    repo_path_component_buf("bar"),
940                    repo_path_component_buf("baz"),
941                },
942            )
943        );
944        assert_eq!(
945            m.visit(repo_path("1")),
946            Visit::sets(
947                hashset! {repo_path_component_buf("foo")},
948                hashset! {repo_path_component_buf("foo")},
949            )
950        );
951        assert_eq!(
952            m.visit(repo_path("2")),
953            Visit::sets(
954                hashset! {repo_path_component_buf("qux")},
955                hashset! {repo_path_component_buf("qux")},
956            )
957        );
958        assert_eq!(m.visit(repo_path("common/bar")), Visit::AllRecursively);
959        assert_eq!(m.visit(repo_path("1/foo")), Visit::AllRecursively);
960        assert_eq!(m.visit(repo_path("2/qux")), Visit::AllRecursively);
961        assert_eq!(m.visit(repo_path("2/quux")), Visit::Nothing);
962    }
963
964    #[test]
965    fn test_difference_matcher_remove_subdir() {
966        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
967        let m2 = PrefixMatcher::new([repo_path("foo/bar")]);
968        let m = DifferenceMatcher::new(&m1, &m2);
969
970        assert!(m.matches(repo_path("foo")));
971        assert!(!m.matches(repo_path("foo/bar")));
972        assert!(!m.matches(repo_path("foo/bar/baz")));
973        assert!(m.matches(repo_path("foo/baz")));
974        assert!(m.matches(repo_path("bar")));
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!(
990            m.visit(repo_path("foo")),
991            Visit::Specific {
992                dirs: VisitDirs::All,
993                files: VisitFiles::All,
994            }
995        );
996        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
997        assert_eq!(m.visit(repo_path("foo/baz")), Visit::AllRecursively);
998        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
999    }
1000
1001    #[test]
1002    fn test_difference_matcher_shared_patterns() {
1003        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
1004        let m2 = PrefixMatcher::new([repo_path("foo")]);
1005        let m = DifferenceMatcher::new(&m1, &m2);
1006
1007        assert!(!m.matches(repo_path("foo")));
1008        assert!(!m.matches(repo_path("foo/bar")));
1009        assert!(m.matches(repo_path("bar")));
1010        assert!(m.matches(repo_path("bar/foo")));
1011
1012        assert_eq!(
1013            m.visit(RepoPath::root()),
1014            Visit::sets(
1015                hashset! {
1016                    repo_path_component_buf("foo"),
1017                    repo_path_component_buf("bar"),
1018                },
1019                hashset! {
1020                    repo_path_component_buf("foo"),
1021                    repo_path_component_buf("bar"),
1022                },
1023            )
1024        );
1025        assert_eq!(m.visit(repo_path("foo")), Visit::Nothing);
1026        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
1027        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
1028        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
1029    }
1030
1031    #[test]
1032    fn test_intersection_matcher_intersecting_roots() {
1033        let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);
1034        let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]);
1035        let m = IntersectionMatcher::new(&m1, &m2);
1036
1037        assert!(!m.matches(repo_path("foo")));
1038        assert!(!m.matches(repo_path("foo/bar")));
1039        assert!(m.matches(repo_path("bar")));
1040        assert!(m.matches(repo_path("bar/foo")));
1041        assert!(!m.matches(repo_path("baz")));
1042        assert!(!m.matches(repo_path("baz/foo")));
1043
1044        assert_eq!(
1045            m.visit(RepoPath::root()),
1046            Visit::sets(
1047                hashset! {repo_path_component_buf("bar")},
1048                hashset! {repo_path_component_buf("bar")}
1049            )
1050        );
1051        assert_eq!(m.visit(repo_path("foo")), Visit::Nothing);
1052        assert_eq!(m.visit(repo_path("foo/bar")), Visit::Nothing);
1053        assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively);
1054        assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively);
1055        assert_eq!(m.visit(repo_path("baz")), Visit::Nothing);
1056        assert_eq!(m.visit(repo_path("baz/foo")), Visit::Nothing);
1057    }
1058
1059    #[test]
1060    fn test_intersection_matcher_subdir() {
1061        let m1 = PrefixMatcher::new([repo_path("foo")]);
1062        let m2 = PrefixMatcher::new([repo_path("foo/bar")]);
1063        let m = IntersectionMatcher::new(&m1, &m2);
1064
1065        assert!(!m.matches(repo_path("foo")));
1066        assert!(!m.matches(repo_path("bar")));
1067        assert!(m.matches(repo_path("foo/bar")));
1068        assert!(m.matches(repo_path("foo/bar/baz")));
1069        assert!(!m.matches(repo_path("foo/baz")));
1070
1071        assert_eq!(
1072            m.visit(RepoPath::root()),
1073            Visit::sets(hashset! {repo_path_component_buf("foo")}, hashset! {})
1074        );
1075        assert_eq!(m.visit(repo_path("bar")), Visit::Nothing);
1076        assert_eq!(
1077            m.visit(repo_path("foo")),
1078            Visit::sets(
1079                hashset! {repo_path_component_buf("bar")},
1080                hashset! {repo_path_component_buf("bar")}
1081            )
1082        );
1083        assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively);
1084    }
1085}