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