From dc47aac6b7223b84508705a4d9637900b5514fe3 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Thu, 4 Apr 2024 19:08:16 +0900 Subject: [PATCH 1/2] matchers: abstract matcher combinators over Matcher trait In order to implement a fileset, we'll need owned variants of these matchers. We can of course let callers move Box into these adapters, but we might need to somehow clone Box. So, I simply made adapters generic. --- lib/src/matchers.rs | 46 +++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/lib/src/matchers.rs b/lib/src/matchers.rs index e87911e651..24284915ba 100644 --- a/lib/src/matchers.rs +++ b/lib/src/matchers.rs @@ -72,6 +72,26 @@ pub trait Matcher: Sync { fn visit(&self, dir: &RepoPath) -> Visit; } +impl Matcher for &T { + fn matches(&self, file: &RepoPath) -> bool { + ::matches(self, file) + } + + fn visit(&self, dir: &RepoPath) -> Visit { + ::visit(self, dir) + } +} + +impl Matcher for Box { + fn matches(&self, file: &RepoPath) -> bool { + ::matches(self, file) + } + + fn visit(&self, dir: &RepoPath) -> Visit { + ::visit(self, dir) + } +} + #[derive(PartialEq, Eq, Debug)] pub struct NothingMatcher; @@ -162,20 +182,21 @@ impl Matcher for PrefixMatcher { /// Matches paths that are matched by the first input matcher but not by the /// second. -pub struct DifferenceMatcher<'input> { +#[derive(Clone, Debug)] +pub struct DifferenceMatcher { /// The minuend - wanted: &'input dyn Matcher, + wanted: M1, /// The subtrahend - unwanted: &'input dyn Matcher, + unwanted: M2, } -impl<'input> DifferenceMatcher<'input> { - pub fn new(wanted: &'input dyn Matcher, unwanted: &'input dyn Matcher) -> Self { +impl DifferenceMatcher { + pub fn new(wanted: M1, unwanted: M2) -> Self { Self { wanted, unwanted } } } -impl Matcher for DifferenceMatcher<'_> { +impl Matcher for DifferenceMatcher { fn matches(&self, file: &RepoPath) -> bool { self.wanted.matches(file) && !self.unwanted.matches(file) } @@ -196,18 +217,19 @@ impl Matcher for DifferenceMatcher<'_> { } /// Matches paths that are matched by both input matchers. -pub struct IntersectionMatcher<'input> { - input1: &'input dyn Matcher, - input2: &'input dyn Matcher, +#[derive(Clone, Debug)] +pub struct IntersectionMatcher { + input1: M1, + input2: M2, } -impl<'input> IntersectionMatcher<'input> { - pub fn new(input1: &'input dyn Matcher, input2: &'input dyn Matcher) -> Self { +impl IntersectionMatcher { + pub fn new(input1: M1, input2: M2) -> Self { Self { input1, input2 } } } -impl Matcher for IntersectionMatcher<'_> { +impl Matcher for IntersectionMatcher { fn matches(&self, file: &RepoPath) -> bool { self.input1.matches(file) && self.input2.matches(file) } From 3be9da14dc11d5fb4d8a2e0716d1454c84a594a3 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Thu, 4 Apr 2024 19:17:30 +0900 Subject: [PATCH 2/2] matchers: add binary UnionMatcher This will be needed to concatenate patterns of different types (such as "prefix/dir" exact:"file/path".) The implementation is basically a copy of IntersectionMatcher, with some logical adjustments. In Mercurial, unionmatcher supports list of matchers as input, but I think binary version is good enough. --- lib/src/matchers.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/lib/src/matchers.rs b/lib/src/matchers.rs index 24284915ba..b29e624b53 100644 --- a/lib/src/matchers.rs +++ b/lib/src/matchers.rs @@ -180,6 +180,60 @@ impl Matcher for PrefixMatcher { } } +/// Matches paths that are matched by any of the input matchers. +#[derive(Clone, Debug)] +pub struct UnionMatcher { + input1: M1, + input2: M2, +} + +impl UnionMatcher { + pub fn new(input1: M1, input2: M2) -> Self { + Self { input1, input2 } + } +} + +impl Matcher for UnionMatcher { + fn matches(&self, file: &RepoPath) -> bool { + self.input1.matches(file) || self.input2.matches(file) + } + + fn visit(&self, dir: &RepoPath) -> Visit { + match self.input1.visit(dir) { + Visit::AllRecursively => Visit::AllRecursively, + Visit::Nothing => self.input2.visit(dir), + Visit::Specific { + dirs: dirs1, + files: files1, + } => match self.input2.visit(dir) { + Visit::AllRecursively => Visit::AllRecursively, + Visit::Nothing => Visit::Specific { + dirs: dirs1, + files: files1, + }, + Visit::Specific { + dirs: dirs2, + files: files2, + } => { + let dirs = match (dirs1, dirs2) { + (VisitDirs::All, _) | (_, VisitDirs::All) => VisitDirs::All, + (VisitDirs::Set(dirs1), VisitDirs::Set(dirs2)) => { + VisitDirs::Set(dirs1.iter().chain(&dirs2).cloned().collect()) + } + }; + let files = match (files1, files2) { + (VisitFiles::All, _) | (_, VisitFiles::All) => VisitFiles::All, + (VisitFiles::Set(files1), VisitFiles::Set(files2)) => { + VisitFiles::Set(files1.iter().chain(&files2).cloned().collect()) + } + }; + Visit::Specific { dirs, files } + } + }, + } + } +} + /// Matches paths that are matched by the first input matcher but not by the /// second. #[derive(Clone, Debug)] @@ -558,6 +612,107 @@ mod tests { assert_eq!(m.visit(repo_path("foo/bar/baz")), Visit::AllRecursively); } + #[test] + fn test_unionmatcher_concatenate_roots() { + let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]); + let m2 = PrefixMatcher::new([repo_path("bar"), repo_path("baz")]); + let m = UnionMatcher::new(&m1, &m2); + + assert!(m.matches(repo_path("foo"))); + assert!(m.matches(repo_path("foo/bar"))); + assert!(m.matches(repo_path("bar"))); + assert!(m.matches(repo_path("bar/foo"))); + assert!(m.matches(repo_path("baz"))); + assert!(m.matches(repo_path("baz/foo"))); + assert!(!m.matches(repo_path("qux"))); + assert!(!m.matches(repo_path("qux/foo"))); + + assert_eq!( + m.visit(RepoPath::root()), + Visit::sets( + hashset! { + RepoPathComponentBuf::from("foo"), + RepoPathComponentBuf::from("bar"), + RepoPathComponentBuf::from("baz"), + }, + hashset! { + RepoPathComponentBuf::from("foo"), + RepoPathComponentBuf::from("bar"), + RepoPathComponentBuf::from("baz"), + }, + ) + ); + assert_eq!(m.visit(repo_path("foo")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("foo/bar")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("bar")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("bar/foo")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("baz")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("baz/foo")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("qux")), Visit::Nothing); + assert_eq!(m.visit(repo_path("qux/foo")), Visit::Nothing); + } + + #[test] + fn test_unionmatcher_concatenate_subdirs() { + let m1 = PrefixMatcher::new([repo_path("common/bar"), repo_path("1/foo")]); + let m2 = PrefixMatcher::new([repo_path("common/baz"), repo_path("2/qux")]); + let m = UnionMatcher::new(&m1, &m2); + + assert!(!m.matches(repo_path("common"))); + assert!(!m.matches(repo_path("1"))); + assert!(!m.matches(repo_path("2"))); + assert!(m.matches(repo_path("common/bar"))); + assert!(m.matches(repo_path("common/bar/baz"))); + assert!(m.matches(repo_path("common/baz"))); + assert!(m.matches(repo_path("1/foo"))); + assert!(m.matches(repo_path("1/foo/qux"))); + assert!(m.matches(repo_path("2/qux"))); + assert!(!m.matches(repo_path("2/quux"))); + + assert_eq!( + m.visit(RepoPath::root()), + Visit::sets( + hashset! { + RepoPathComponentBuf::from("common"), + RepoPathComponentBuf::from("1"), + RepoPathComponentBuf::from("2"), + }, + hashset! {}, + ) + ); + assert_eq!( + m.visit(repo_path("common")), + Visit::sets( + hashset! { + RepoPathComponentBuf::from("bar"), + RepoPathComponentBuf::from("baz"), + }, + hashset! { + RepoPathComponentBuf::from("bar"), + RepoPathComponentBuf::from("baz"), + }, + ) + ); + assert_eq!( + m.visit(repo_path("1")), + Visit::sets( + hashset! {RepoPathComponentBuf::from("foo")}, + hashset! {RepoPathComponentBuf::from("foo")}, + ) + ); + assert_eq!( + m.visit(repo_path("2")), + Visit::sets( + hashset! {RepoPathComponentBuf::from("qux")}, + hashset! {RepoPathComponentBuf::from("qux")}, + ) + ); + assert_eq!(m.visit(repo_path("common/bar")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("1/foo")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("2/qux")), Visit::AllRecursively); + assert_eq!(m.visit(repo_path("2/quux")), Visit::Nothing); + } + #[test] fn test_differencematcher_remove_subdir() { let m1 = PrefixMatcher::new([repo_path("foo"), repo_path("bar")]);