Skip to main content

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