1#![allow(dead_code)]
16
17use std::collections::{HashMap, HashSet};
18use std::iter;
19
20use crate::repo_path::{RepoPath, RepoPathComponent};
21
22#[derive(PartialEq, Eq, Debug)]
23pub enum Visit {
24 AllRecursively,
27 Specific {
28 dirs: VisitDirs,
29 files: VisitFiles,
30 },
31 Nothing,
37}
38
39impl Visit {
40 fn sets(dirs: HashSet<RepoPathComponent>, files: HashSet<RepoPathComponent>) -> Self {
41 if dirs.is_empty() && files.is_empty() {
42 Self::Nothing
43 } else {
44 Self::Specific {
45 dirs: VisitDirs::Set(dirs),
46 files: VisitFiles::Set(files),
47 }
48 }
49 }
50
51 pub fn is_nothing(&self) -> bool {
52 *self == Visit::Nothing
53 }
54}
55
56#[derive(PartialEq, Eq, Debug)]
57pub enum VisitDirs {
58 All,
59 Set(HashSet<RepoPathComponent>),
60}
61
62#[derive(PartialEq, Eq, Debug)]
63pub enum VisitFiles {
64 All,
65 Set(HashSet<RepoPathComponent>),
66}
67
68pub trait Matcher {
69 fn matches(&self, file: &RepoPath) -> bool;
70 fn visit(&self, dir: &RepoPath) -> Visit;
71}
72
73#[derive(PartialEq, Eq, Debug)]
74pub struct NothingMatcher;
75
76impl Matcher for NothingMatcher {
77 fn matches(&self, _file: &RepoPath) -> bool {
78 false
79 }
80
81 fn visit(&self, _dir: &RepoPath) -> Visit {
82 Visit::Nothing
83 }
84}
85
86#[derive(PartialEq, Eq, Debug)]
87pub struct EverythingMatcher;
88
89impl Matcher for EverythingMatcher {
90 fn matches(&self, _file: &RepoPath) -> bool {
91 true
92 }
93
94 fn visit(&self, _dir: &RepoPath) -> Visit {
95 Visit::AllRecursively
96 }
97}
98
99#[derive(PartialEq, Eq, Debug)]
100pub struct FilesMatcher {
101 tree: RepoPathTree,
102}
103
104impl FilesMatcher {
105 pub fn new(files: &[RepoPath]) -> Self {
106 let mut tree = RepoPathTree::new();
107 for f in files {
108 tree.add_file(f);
109 }
110 FilesMatcher { tree }
111 }
112}
113
114impl Matcher for FilesMatcher {
115 fn matches(&self, file: &RepoPath) -> bool {
116 self.tree.get(file).map(|sub| sub.is_file).unwrap_or(false)
117 }
118
119 fn visit(&self, dir: &RepoPath) -> Visit {
120 self.tree.get_visit_sets(dir)
121 }
122}
123
124pub struct PrefixMatcher {
125 tree: RepoPathTree,
126}
127
128impl PrefixMatcher {
129 pub fn new(prefixes: &[RepoPath]) -> Self {
130 let mut tree = RepoPathTree::new();
131 for prefix in prefixes {
132 let sub = tree.add(prefix);
133 sub.is_dir = true;
134 sub.is_file = true;
135 }
136 PrefixMatcher { tree }
137 }
138}
139
140impl Matcher for PrefixMatcher {
141 fn matches(&self, file: &RepoPath) -> bool {
142 self.tree.walk_to(file).any(|(sub, _)| sub.is_file)
143 }
144
145 fn visit(&self, dir: &RepoPath) -> Visit {
146 for (sub, tail_components) in self.tree.walk_to(dir) {
147 if sub.is_file {
149 return Visit::AllRecursively;
150 }
151 if tail_components.is_empty() {
153 return sub.to_visit_sets();
154 }
155 }
156 Visit::Nothing
157 }
158}
159
160pub struct DifferenceMatcher<'input> {
163 wanted: &'input dyn Matcher,
165 unwanted: &'input dyn Matcher,
167}
168
169impl<'input> DifferenceMatcher<'input> {
170 pub fn new(wanted: &'input dyn Matcher, unwanted: &'input dyn Matcher) -> Self {
171 Self { wanted, unwanted }
172 }
173}
174
175impl Matcher for DifferenceMatcher<'_> {
176 fn matches(&self, file: &RepoPath) -> bool {
177 self.wanted.matches(file) && !self.unwanted.matches(file)
178 }
179
180 fn visit(&self, dir: &RepoPath) -> Visit {
181 match self.unwanted.visit(dir) {
182 Visit::AllRecursively => Visit::Nothing,
183 Visit::Nothing => self.wanted.visit(dir),
184 Visit::Specific { .. } => match self.wanted.visit(dir) {
185 Visit::AllRecursively => Visit::Specific {
186 dirs: VisitDirs::All,
187 files: VisitFiles::All,
188 },
189 wanted_visit => wanted_visit,
190 },
191 }
192 }
193}
194
195pub struct IntersectionMatcher<'input> {
197 input1: &'input dyn Matcher,
198 input2: &'input dyn Matcher,
199}
200
201impl<'input> IntersectionMatcher<'input> {
202 pub fn new(input1: &'input dyn Matcher, input2: &'input dyn Matcher) -> Self {
203 Self { input1, input2 }
204 }
205}
206
207impl Matcher for IntersectionMatcher<'_> {
208 fn matches(&self, file: &RepoPath) -> bool {
209 self.input1.matches(file) && self.input2.matches(file)
210 }
211
212 fn visit(&self, dir: &RepoPath) -> Visit {
213 match self.input1.visit(dir) {
214 Visit::AllRecursively => self.input2.visit(dir),
215 Visit::Nothing => Visit::Nothing,
216 Visit::Specific {
217 dirs: dirs1,
218 files: files1,
219 } => match self.input2.visit(dir) {
220 Visit::AllRecursively => Visit::Specific {
221 dirs: dirs1,
222 files: files1,
223 },
224 Visit::Nothing => Visit::Nothing,
225 Visit::Specific {
226 dirs: dirs2,
227 files: files2,
228 } => {
229 let dirs = match (dirs1, dirs2) {
230 (VisitDirs::All, VisitDirs::All) => VisitDirs::All,
231 (dirs1, VisitDirs::All) => dirs1,
232 (VisitDirs::All, dirs2) => dirs2,
233 (VisitDirs::Set(dirs1), VisitDirs::Set(dirs2)) => {
234 VisitDirs::Set(dirs1.intersection(&dirs2).cloned().collect())
235 }
236 };
237 let files = match (files1, files2) {
238 (VisitFiles::All, VisitFiles::All) => VisitFiles::All,
239 (files1, VisitFiles::All) => files1,
240 (VisitFiles::All, files2) => files2,
241 (VisitFiles::Set(files1), VisitFiles::Set(files2)) => {
242 VisitFiles::Set(files1.intersection(&files2).cloned().collect())
243 }
244 };
245 match (&dirs, &files) {
246 (VisitDirs::Set(dirs), VisitFiles::Set(files))
247 if dirs.is_empty() && files.is_empty() =>
248 {
249 Visit::Nothing
250 }
251 _ => Visit::Specific { dirs, files },
252 }
253 }
254 },
255 }
256 }
257}
258
259#[derive(PartialEq, Eq, Debug)]
262struct RepoPathTree {
263 entries: HashMap<RepoPathComponent, RepoPathTree>,
264 is_dir: bool,
267 is_file: bool,
268}
269
270impl RepoPathTree {
271 fn new() -> Self {
272 RepoPathTree {
273 entries: HashMap::new(),
274 is_dir: false,
275 is_file: false,
276 }
277 }
278
279 fn add(&mut self, dir: &RepoPath) -> &mut RepoPathTree {
280 dir.components().iter().fold(self, |sub, name| {
281 if !sub.entries.contains_key(name) {
283 sub.is_dir = true;
284 sub.entries.insert(name.clone(), RepoPathTree::new());
285 }
286 sub.entries.get_mut(name).unwrap()
287 })
288 }
289
290 fn add_dir(&mut self, dir: &RepoPath) {
291 self.add(dir).is_dir = true;
292 }
293
294 fn add_file(&mut self, file: &RepoPath) {
295 self.add(file).is_file = true;
296 }
297
298 fn get(&self, dir: &RepoPath) -> Option<&RepoPathTree> {
299 dir.components()
300 .iter()
301 .try_fold(self, |sub, name| sub.entries.get(name))
302 }
303
304 fn get_visit_sets(&self, dir: &RepoPath) -> Visit {
305 self.get(dir)
306 .map(RepoPathTree::to_visit_sets)
307 .unwrap_or(Visit::Nothing)
308 }
309
310 fn walk_to<'a>(
311 &'a self,
312 dir: &'a RepoPath,
313 ) -> impl Iterator<Item = (&RepoPathTree, &[RepoPathComponent])> + 'a {
314 iter::successors(
315 Some((self, dir.components().as_slice())),
316 |(sub, components)| {
317 let (name, tail) = components.split_first()?;
318 Some((sub.entries.get(name)?, tail))
319 },
320 )
321 }
322
323 fn to_visit_sets(&self) -> Visit {
324 let mut dirs = HashSet::new();
325 let mut files = HashSet::new();
326 for (name, sub) in &self.entries {
327 if sub.is_dir {
328 dirs.insert(name.clone());
329 }
330 if sub.is_file {
331 files.insert(name.clone());
332 }
333 }
334 Visit::sets(dirs, files)
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use maplit::hashset;
341
342 use super::*;
343 use crate::repo_path::{RepoPath, RepoPathComponent};
344
345 #[test]
346 fn test_repo_path_tree_empty() {
347 let tree = RepoPathTree::new();
348 assert_eq!(tree.get_visit_sets(&RepoPath::root()), Visit::Nothing);
349 }
350
351 #[test]
352 fn test_repo_path_tree_root() {
353 let mut tree = RepoPathTree::new();
354 tree.add_dir(&RepoPath::root());
355 assert_eq!(tree.get_visit_sets(&RepoPath::root()), Visit::Nothing);
356 }
357
358 #[test]
359 fn test_repo_path_tree_dir() {
360 let mut tree = RepoPathTree::new();
361 tree.add_dir(&RepoPath::from_internal_string("dir"));
362 assert_eq!(
363 tree.get_visit_sets(&RepoPath::root()),
364 Visit::sets(hashset! {RepoPathComponent::from("dir")}, hashset! {}),
365 );
366 tree.add_dir(&RepoPath::from_internal_string("dir/sub"));
367 assert_eq!(
368 tree.get_visit_sets(&RepoPath::from_internal_string("dir")),
369 Visit::sets(hashset! {RepoPathComponent::from("sub")}, hashset! {}),
370 );
371 }
372
373 #[test]
374 fn test_repo_path_tree_file() {
375 let mut tree = RepoPathTree::new();
376 tree.add_file(&RepoPath::from_internal_string("dir/file"));
377 assert_eq!(
378 tree.get_visit_sets(&RepoPath::root()),
379 Visit::sets(hashset! {RepoPathComponent::from("dir")}, hashset! {}),
380 );
381 assert_eq!(
382 tree.get_visit_sets(&RepoPath::from_internal_string("dir")),
383 Visit::sets(hashset! {}, hashset! {RepoPathComponent::from("file")}),
384 );
385 }
386
387 #[test]
388 fn test_nothingmatcher() {
389 let m = NothingMatcher;
390 assert!(!m.matches(&RepoPath::from_internal_string("file")));
391 assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
392 assert_eq!(m.visit(&RepoPath::root()), Visit::Nothing);
393 }
394
395 #[test]
396 fn test_filesmatcher_empty() {
397 let m = FilesMatcher::new(&[]);
398 assert!(!m.matches(&RepoPath::from_internal_string("file")));
399 assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
400 assert_eq!(m.visit(&RepoPath::root()), Visit::Nothing);
401 }
402
403 #[test]
404 fn test_filesmatcher_nonempty() {
405 let m = FilesMatcher::new(&[
406 RepoPath::from_internal_string("dir1/subdir1/file1"),
407 RepoPath::from_internal_string("dir1/subdir1/file2"),
408 RepoPath::from_internal_string("dir1/subdir2/file3"),
409 RepoPath::from_internal_string("file4"),
410 ]);
411
412 assert!(!m.matches(&RepoPath::from_internal_string("dir1")));
413 assert!(!m.matches(&RepoPath::from_internal_string("dir1/subdir1")));
414 assert!(m.matches(&RepoPath::from_internal_string("dir1/subdir1/file1")));
415 assert!(m.matches(&RepoPath::from_internal_string("dir1/subdir1/file2")));
416 assert!(!m.matches(&RepoPath::from_internal_string("dir1/subdir1/file3")));
417
418 assert_eq!(
419 m.visit(&RepoPath::root()),
420 Visit::sets(
421 hashset! {RepoPathComponent::from("dir1")},
422 hashset! {RepoPathComponent::from("file4")}
423 )
424 );
425 assert_eq!(
426 m.visit(&RepoPath::from_internal_string("dir1")),
427 Visit::sets(
428 hashset! {RepoPathComponent::from("subdir1"), RepoPathComponent::from("subdir2")},
429 hashset! {}
430 )
431 );
432 assert_eq!(
433 m.visit(&RepoPath::from_internal_string("dir1/subdir1")),
434 Visit::sets(
435 hashset! {},
436 hashset! {RepoPathComponent::from("file1"), RepoPathComponent::from("file2")}
437 )
438 );
439 assert_eq!(
440 m.visit(&RepoPath::from_internal_string("dir1/subdir2")),
441 Visit::sets(hashset! {}, hashset! {RepoPathComponent::from("file3")})
442 );
443 }
444
445 #[test]
446 fn test_prefixmatcher_empty() {
447 let m = PrefixMatcher::new(&[]);
448 assert!(!m.matches(&RepoPath::from_internal_string("file")));
449 assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
450 assert_eq!(m.visit(&RepoPath::root()), Visit::Nothing);
451 }
452
453 #[test]
454 fn test_prefixmatcher_root() {
455 let m = PrefixMatcher::new(&[RepoPath::root()]);
456 assert!(m.matches(&RepoPath::from_internal_string("file")));
458 assert!(m.matches(&RepoPath::from_internal_string("dir/file")));
459 assert_eq!(m.visit(&RepoPath::root()), Visit::AllRecursively);
461 assert_eq!(
462 m.visit(&RepoPath::from_internal_string("foo/bar")),
463 Visit::AllRecursively
464 );
465 }
466
467 #[test]
468 fn test_prefixmatcher_single_prefix() {
469 let m = PrefixMatcher::new(&[RepoPath::from_internal_string("foo/bar")]);
470
471 assert!(!m.matches(&RepoPath::from_internal_string("foo")));
473 assert!(!m.matches(&RepoPath::from_internal_string("bar")));
474 assert!(m.matches(&RepoPath::from_internal_string("foo/bar")));
476 assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz")));
478 assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz/qux")));
479 assert!(!m.matches(&RepoPath::from_internal_string("foo/foo")));
481 assert!(!m.matches(&RepoPath::from_internal_string("bar/foo/bar")));
483
484 assert_eq!(
487 m.visit(&RepoPath::root()),
488 Visit::sets(hashset! {RepoPathComponent::from("foo")}, hashset! {})
489 );
490 assert_eq!(
493 m.visit(&RepoPath::from_internal_string("foo")),
494 Visit::sets(
495 hashset! {RepoPathComponent::from("bar")},
496 hashset! {RepoPathComponent::from("bar")}
497 )
498 );
499 assert_eq!(
501 m.visit(&RepoPath::from_internal_string("foo/bar")),
502 Visit::AllRecursively
503 );
504 assert_eq!(
506 m.visit(&RepoPath::from_internal_string("foo/bar/baz")),
507 Visit::AllRecursively
508 );
509 assert_eq!(
512 m.visit(&RepoPath::from_internal_string("bar")),
513 Visit::Nothing
514 );
515 }
516
517 #[test]
518 fn test_prefixmatcher_nested_prefixes() {
519 let m = PrefixMatcher::new(&[
520 RepoPath::from_internal_string("foo"),
521 RepoPath::from_internal_string("foo/bar/baz"),
522 ]);
523
524 assert!(m.matches(&RepoPath::from_internal_string("foo")));
525 assert!(!m.matches(&RepoPath::from_internal_string("bar")));
526 assert!(m.matches(&RepoPath::from_internal_string("foo/bar")));
527 assert!(m.matches(&RepoPath::from_internal_string("foo/baz/foo")));
529
530 assert_eq!(
531 m.visit(&RepoPath::root()),
532 Visit::sets(
533 hashset! {RepoPathComponent::from("foo")},
534 hashset! {RepoPathComponent::from("foo")}
535 )
536 );
537 assert_eq!(
539 m.visit(&RepoPath::from_internal_string("foo")),
540 Visit::AllRecursively
541 );
542 assert_eq!(
544 m.visit(&RepoPath::from_internal_string("foo/bar/baz")),
545 Visit::AllRecursively
546 );
547 }
548
549 #[test]
550 fn test_differencematcher_remove_subdir() {
551 let m1 = PrefixMatcher::new(&[
552 RepoPath::from_internal_string("foo"),
553 RepoPath::from_internal_string("bar"),
554 ]);
555 let m2 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo/bar")]);
556 let m = DifferenceMatcher::new(&m1, &m2);
557
558 assert!(m.matches(&RepoPath::from_internal_string("foo")));
559 assert!(!m.matches(&RepoPath::from_internal_string("foo/bar")));
560 assert!(!m.matches(&RepoPath::from_internal_string("foo/bar/baz")));
561 assert!(m.matches(&RepoPath::from_internal_string("foo/baz")));
562 assert!(m.matches(&RepoPath::from_internal_string("bar")));
563
564 assert_eq!(
565 m.visit(&RepoPath::root()),
566 Visit::sets(
567 hashset! {RepoPathComponent::from("foo"), RepoPathComponent::from("bar")},
568 hashset! {RepoPathComponent::from("foo"), RepoPathComponent::from("bar")}
569 )
570 );
571 assert_eq!(
572 m.visit(&RepoPath::from_internal_string("foo")),
573 Visit::Specific {
574 dirs: VisitDirs::All,
575 files: VisitFiles::All,
576 }
577 );
578 assert_eq!(
579 m.visit(&RepoPath::from_internal_string("foo/bar")),
580 Visit::Nothing
581 );
582 assert_eq!(
583 m.visit(&RepoPath::from_internal_string("foo/baz")),
584 Visit::AllRecursively
585 );
586 assert_eq!(
587 m.visit(&RepoPath::from_internal_string("bar")),
588 Visit::AllRecursively
589 );
590 }
591
592 #[test]
593 fn test_differencematcher_shared_patterns() {
594 let m1 = PrefixMatcher::new(&[
595 RepoPath::from_internal_string("foo"),
596 RepoPath::from_internal_string("bar"),
597 ]);
598 let m2 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo")]);
599 let m = DifferenceMatcher::new(&m1, &m2);
600
601 assert!(!m.matches(&RepoPath::from_internal_string("foo")));
602 assert!(!m.matches(&RepoPath::from_internal_string("foo/bar")));
603 assert!(m.matches(&RepoPath::from_internal_string("bar")));
604 assert!(m.matches(&RepoPath::from_internal_string("bar/foo")));
605
606 assert_eq!(
607 m.visit(&RepoPath::root()),
608 Visit::sets(
609 hashset! {RepoPathComponent::from("foo"), RepoPathComponent::from("bar")},
610 hashset! {RepoPathComponent::from("foo"), RepoPathComponent::from("bar")}
611 )
612 );
613 assert_eq!(
614 m.visit(&RepoPath::from_internal_string("foo")),
615 Visit::Nothing
616 );
617 assert_eq!(
618 m.visit(&RepoPath::from_internal_string("foo/bar")),
619 Visit::Nothing
620 );
621 assert_eq!(
622 m.visit(&RepoPath::from_internal_string("bar")),
623 Visit::AllRecursively
624 );
625 assert_eq!(
626 m.visit(&RepoPath::from_internal_string("bar/foo")),
627 Visit::AllRecursively
628 );
629 }
630
631 #[test]
632 fn test_intersectionmatcher_intersecting_roots() {
633 let m1 = PrefixMatcher::new(&[
634 RepoPath::from_internal_string("foo"),
635 RepoPath::from_internal_string("bar"),
636 ]);
637 let m2 = PrefixMatcher::new(&[
638 RepoPath::from_internal_string("bar"),
639 RepoPath::from_internal_string("baz"),
640 ]);
641 let m = IntersectionMatcher::new(&m1, &m2);
642
643 assert!(!m.matches(&RepoPath::from_internal_string("foo")));
644 assert!(!m.matches(&RepoPath::from_internal_string("foo/bar")));
645 assert!(m.matches(&RepoPath::from_internal_string("bar")));
646 assert!(m.matches(&RepoPath::from_internal_string("bar/foo")));
647 assert!(!m.matches(&RepoPath::from_internal_string("baz")));
648 assert!(!m.matches(&RepoPath::from_internal_string("baz/foo")));
649
650 assert_eq!(
651 m.visit(&RepoPath::root()),
652 Visit::sets(
653 hashset! {RepoPathComponent::from("bar")},
654 hashset! {RepoPathComponent::from("bar")}
655 )
656 );
657 assert_eq!(
658 m.visit(&RepoPath::from_internal_string("foo")),
659 Visit::Nothing
660 );
661 assert_eq!(
662 m.visit(&RepoPath::from_internal_string("foo/bar")),
663 Visit::Nothing
664 );
665 assert_eq!(
666 m.visit(&RepoPath::from_internal_string("bar")),
667 Visit::AllRecursively
668 );
669 assert_eq!(
670 m.visit(&RepoPath::from_internal_string("bar/foo")),
671 Visit::AllRecursively
672 );
673 assert_eq!(
674 m.visit(&RepoPath::from_internal_string("baz")),
675 Visit::Nothing
676 );
677 assert_eq!(
678 m.visit(&RepoPath::from_internal_string("baz/foo")),
679 Visit::Nothing
680 );
681 }
682
683 #[test]
684 fn test_intersectionmatcher_subdir() {
685 let m1 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo")]);
686 let m2 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo/bar")]);
687 let m = IntersectionMatcher::new(&m1, &m2);
688
689 assert!(!m.matches(&RepoPath::from_internal_string("foo")));
690 assert!(!m.matches(&RepoPath::from_internal_string("bar")));
691 assert!(m.matches(&RepoPath::from_internal_string("foo/bar")));
692 assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz")));
693 assert!(!m.matches(&RepoPath::from_internal_string("foo/baz")));
694
695 assert_eq!(
696 m.visit(&RepoPath::root()),
697 Visit::sets(hashset! {RepoPathComponent::from("foo")}, hashset! {})
698 );
699 assert_eq!(
700 m.visit(&RepoPath::from_internal_string("bar")),
701 Visit::Nothing
702 );
703 assert_eq!(
704 m.visit(&RepoPath::from_internal_string("foo")),
705 Visit::sets(
706 hashset! {RepoPathComponent::from("bar")},
707 hashset! {RepoPathComponent::from("bar")}
708 )
709 );
710 assert_eq!(
711 m.visit(&RepoPath::from_internal_string("foo/bar")),
712 Visit::AllRecursively
713 );
714 }
715}