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