Skip to content

Commit

Permalink
matchers: add IntersectionMatcher
Browse files Browse the repository at this point in the history
I plan to use this matcher for some future `jj add` command (for
#323). The idea is that we'll do a path-restricted walk of the working
copy based on the intersection of the sparse patterns and any patterns
specified by the user. However, I think it will be useful before that,
for @arxanas's fsmonitor feature (#362).
  • Loading branch information
martinvonz committed Jun 10, 2022
1 parent 932ce9b commit 5eabc18
Showing 1 changed file with 151 additions and 0 deletions.
151 changes: 151 additions & 0 deletions lib/src/matchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ impl Matcher for PrefixMatcher {
}
}

/// Matches paths that are matched by the first input matcher but not by the
/// second.
pub struct DifferenceMatcher<'input> {
/// The minuend
wanted: &'input dyn Matcher,
Expand Down Expand Up @@ -199,6 +201,70 @@ 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,
}

impl<'input> IntersectionMatcher<'input> {
pub fn new(input1: &'input dyn Matcher, input2: &'input dyn Matcher) -> Self {
Self { input1, input2 }
}
}

impl Matcher for IntersectionMatcher<'_> {
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 => self.input2.visit(dir),
Visit::Nothing => Visit::Nothing,
Visit::Specific {
dirs: dirs1,
files: files1,
} => match self.input2.visit(dir) {
Visit::AllRecursively => Visit::Specific {
dirs: dirs1,
files: files1,
},
Visit::Nothing => Visit::Nothing,
Visit::Specific {
dirs: dirs2,
files: files2,
} => {
let dirs = match (dirs1, dirs2) {
(VisitDirs::All, VisitDirs::All) => VisitDirs::All,
(dirs1, VisitDirs::All) => dirs1,
(VisitDirs::All, dirs2) => dirs2,
(VisitDirs::Set(dirs1), VisitDirs::Set(dirs2)) => {
VisitDirs::Set(dirs1.intersection(&dirs2).cloned().collect())
}
};
let files = match (files1, files2) {
(VisitFiles::All, VisitFiles::All) => VisitFiles::All,
(files1, VisitFiles::All) => files1,
(VisitFiles::All, files2) => files2,
(VisitFiles::Set(files1), VisitFiles::Set(files2)) => {
VisitFiles::Set(files1.intersection(&files2).cloned().collect())
}
};
match (&dirs, &files) {
(VisitDirs::Set(dirs), VisitFiles::Set(files))
if dirs.is_empty() && files.is_empty() =>
{
Visit::Nothing
}
_ => Visit::Specific { dirs, files },
}
}
},
}
}
}

/// Keeps track of which subdirectories and files of each directory need to be
/// visited.
#[derive(PartialEq, Eq, Debug)]
Expand Down Expand Up @@ -530,4 +596,89 @@ mod tests {
Visit::AllRecursively
);
}

#[test]
fn test_intersectionmatcher_intersecting_roots() {
let m1 = PrefixMatcher::new(&[
RepoPath::from_internal_string("foo"),
RepoPath::from_internal_string("bar"),
]);
let m2 = PrefixMatcher::new(&[
RepoPath::from_internal_string("bar"),
RepoPath::from_internal_string("baz"),
]);
let m = IntersectionMatcher::new(&m1, &m2);

assert!(!m.matches(&RepoPath::from_internal_string("foo")));
assert!(!m.matches(&RepoPath::from_internal_string("foo/bar")));
assert!(m.matches(&RepoPath::from_internal_string("bar")));
assert!(m.matches(&RepoPath::from_internal_string("bar/foo")));
assert!(!m.matches(&RepoPath::from_internal_string("baz")));
assert!(!m.matches(&RepoPath::from_internal_string("baz/foo")));

assert_eq!(
m.visit(&RepoPath::root()),
Visit::sets(
hashset! {RepoPathComponent::from("bar")},
hashset! {RepoPathComponent::from("bar")}
)
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("foo")),
Visit::Nothing
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("foo/bar")),
Visit::Nothing
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("bar")),
Visit::AllRecursively
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("bar/foo")),
Visit::AllRecursively
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("baz")),
Visit::Nothing
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("baz/foo")),
Visit::Nothing
);
}

#[test]
fn test_intersectionmatcher_subdir() {
let m1 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo")]);
let m2 = PrefixMatcher::new(&[RepoPath::from_internal_string("foo/bar")]);
let m = IntersectionMatcher::new(&m1, &m2);

assert!(!m.matches(&RepoPath::from_internal_string("foo")));
assert!(!m.matches(&RepoPath::from_internal_string("bar")));
assert!(m.matches(&RepoPath::from_internal_string("foo/bar")));
assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz")));
assert!(!m.matches(&RepoPath::from_internal_string("foo/baz")));

assert_eq!(
m.visit(&RepoPath::root()),
Visit::sets(hashset! {RepoPathComponent::from("foo")}, hashset! {})
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("bar")),
Visit::Nothing
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("foo")),
Visit::sets(
hashset! {RepoPathComponent::from("bar")},
hashset! {RepoPathComponent::from("bar")}
)
);
assert_eq!(
m.visit(&RepoPath::from_internal_string("foo/bar")),
Visit::AllRecursively
);
}
}

0 comments on commit 5eabc18

Please sign in to comment.