mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-31 16:33:10 +00:00
matchers: add a matcher for path prefixes (#52)
It's useful to be able to match path prefixes for many commands, e.g. to allow `jj restore src` to restore all files in under `src/` (or a file called `src`). I also plan to use it for sparse checkouts. We'll need to be able to match path prefixes
This commit is contained in:
parent
d49892431b
commit
cce12261d8
2 changed files with 210 additions and 11 deletions
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
|
|
||||||
use crate::repo_path::{RepoPath, RepoPathComponent};
|
use crate::repo_path::{RepoPath, RepoPathComponent};
|
||||||
|
|
||||||
|
@ -24,6 +24,15 @@ pub struct Visit {
|
||||||
files: VisitFiles,
|
files: VisitFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Visit {
|
||||||
|
pub fn all() -> Self {
|
||||||
|
Self {
|
||||||
|
dirs: VisitDirs::All,
|
||||||
|
files: VisitFiles::All,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub enum VisitDirs {
|
pub enum VisitDirs {
|
||||||
All,
|
All,
|
||||||
|
@ -88,6 +97,52 @@ impl Matcher for FilesMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PrefixMatcher {
|
||||||
|
prefixes: BTreeSet<RepoPath>,
|
||||||
|
dirs: Dirs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrefixMatcher {
|
||||||
|
pub fn new(prefixes: &[RepoPath]) -> Self {
|
||||||
|
let prefixes = prefixes.iter().cloned().collect();
|
||||||
|
let mut dirs = Dirs::new();
|
||||||
|
for prefix in &prefixes {
|
||||||
|
dirs.add_dir(prefix);
|
||||||
|
if !prefix.is_root() {
|
||||||
|
dirs.add_file(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PrefixMatcher { prefixes, dirs }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Matcher for PrefixMatcher {
|
||||||
|
fn matches(&self, file: &RepoPath) -> bool {
|
||||||
|
let components = file.components();
|
||||||
|
// TODO: Make Dirs a trie instead, so this can just walk that trie.
|
||||||
|
for i in 0..components.len() + 1 {
|
||||||
|
let prefix = RepoPath::from_components(components[0..i].to_vec());
|
||||||
|
if self.prefixes.contains(&prefix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit(&self, dir: &RepoPath) -> Visit {
|
||||||
|
if self.matches(dir) {
|
||||||
|
Visit::all()
|
||||||
|
} else {
|
||||||
|
let dirs = self.dirs.get_dirs(dir);
|
||||||
|
let files = self.dirs.get_files(dir);
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::Set(dirs),
|
||||||
|
files: VisitFiles::Set(files),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Keeps track of which subdirectories and files of each directory need to be
|
/// Keeps track of which subdirectories and files of each directory need to be
|
||||||
/// visited.
|
/// visited.
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
@ -104,7 +159,8 @@ impl Dirs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_dir(&mut self, mut dir: RepoPath) {
|
fn add_dir(&mut self, dir: &RepoPath) {
|
||||||
|
let mut dir = dir.clone();
|
||||||
let mut maybe_child = None;
|
let mut maybe_child = None;
|
||||||
loop {
|
loop {
|
||||||
let was_present = self.dirs.contains_key(&dir);
|
let was_present = self.dirs.contains_key(&dir);
|
||||||
|
@ -129,7 +185,7 @@ impl Dirs {
|
||||||
let (dir, basename) = file
|
let (dir, basename) = file
|
||||||
.split()
|
.split()
|
||||||
.unwrap_or_else(|| panic!("got empty filename: {:?}", file));
|
.unwrap_or_else(|| panic!("got empty filename: {:?}", file));
|
||||||
self.add_dir(dir.clone());
|
self.add_dir(&dir);
|
||||||
self.files.entry(dir).or_default().insert(basename.clone());
|
self.files.entry(dir).or_default().insert(basename.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,22 +204,22 @@ mod tests {
|
||||||
use crate::repo_path::{RepoPath, RepoPathComponent};
|
use crate::repo_path::{RepoPath, RepoPathComponent};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dirs_empty() {
|
fn test_dirs_empty() {
|
||||||
let dirs = Dirs::new();
|
let dirs = Dirs::new();
|
||||||
assert_eq!(dirs.get_dirs(&RepoPath::root()), hashset! {});
|
assert_eq!(dirs.get_dirs(&RepoPath::root()), hashset! {});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dirs_root() {
|
fn test_dirs_root() {
|
||||||
let mut dirs = Dirs::new();
|
let mut dirs = Dirs::new();
|
||||||
dirs.add_dir(RepoPath::root());
|
dirs.add_dir(&RepoPath::root());
|
||||||
assert_eq!(dirs.get_dirs(&RepoPath::root()), hashset! {});
|
assert_eq!(dirs.get_dirs(&RepoPath::root()), hashset! {});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dirs_dir() {
|
fn test_dirs_dir() {
|
||||||
let mut dirs = Dirs::new();
|
let mut dirs = Dirs::new();
|
||||||
dirs.add_dir(RepoPath::from_internal_string("dir"));
|
dirs.add_dir(&RepoPath::from_internal_string("dir"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dirs.get_dirs(&RepoPath::root()),
|
dirs.get_dirs(&RepoPath::root()),
|
||||||
hashset! {RepoPathComponent::from("dir")}
|
hashset! {RepoPathComponent::from("dir")}
|
||||||
|
@ -171,7 +227,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dirs_file() {
|
fn test_dirs_file() {
|
||||||
let mut dirs = Dirs::new();
|
let mut dirs = Dirs::new();
|
||||||
dirs.add_file(&RepoPath::from_internal_string("dir/file"));
|
dirs.add_file(&RepoPath::from_internal_string("dir/file"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -182,7 +238,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn filesmatcher_empty() {
|
fn test_filesmatcher_empty() {
|
||||||
let m = FilesMatcher::new(hashset! {});
|
let m = FilesMatcher::new(hashset! {});
|
||||||
assert!(!m.matches(&RepoPath::from_internal_string("file")));
|
assert!(!m.matches(&RepoPath::from_internal_string("file")));
|
||||||
assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
|
assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
|
||||||
|
@ -196,7 +252,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn filesmatcher_nonempty() {
|
fn test_filesmatcher_nonempty() {
|
||||||
let m = FilesMatcher::new(hashset! {
|
let m = FilesMatcher::new(hashset! {
|
||||||
RepoPath::from_internal_string("dir1/subdir1/file1"),
|
RepoPath::from_internal_string("dir1/subdir1/file1"),
|
||||||
RepoPath::from_internal_string("dir1/subdir1/file2"),
|
RepoPath::from_internal_string("dir1/subdir1/file2"),
|
||||||
|
@ -237,4 +293,143 @@ mod tests {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prefixmatcher_empty() {
|
||||||
|
let m = PrefixMatcher::new(&[]);
|
||||||
|
assert!(!m.matches(&RepoPath::from_internal_string("file")));
|
||||||
|
assert!(!m.matches(&RepoPath::from_internal_string("dir/file")));
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::root()),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::Set(hashset! {}),
|
||||||
|
files: VisitFiles::Set(hashset! {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prefixmatcher_root() {
|
||||||
|
let m = PrefixMatcher::new(&[RepoPath::root()]);
|
||||||
|
// Matches all files
|
||||||
|
assert!(m.matches(&RepoPath::from_internal_string("file")));
|
||||||
|
assert!(m.matches(&RepoPath::from_internal_string("dir/file")));
|
||||||
|
// Visits all directories
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::root()),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::All,
|
||||||
|
files: VisitFiles::All,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::from_internal_string("foo/bar")),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::All,
|
||||||
|
files: VisitFiles::All,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prefixmatcher_single_prefix() {
|
||||||
|
let m = PrefixMatcher::new(&[RepoPath::from_internal_string("foo/bar")]);
|
||||||
|
|
||||||
|
// Parts of the prefix should not match
|
||||||
|
assert!(!m.matches(&RepoPath::from_internal_string("foo")));
|
||||||
|
assert!(!m.matches(&RepoPath::from_internal_string("bar")));
|
||||||
|
// A file matching the prefix exactly should match
|
||||||
|
assert!(m.matches(&RepoPath::from_internal_string("foo/bar")));
|
||||||
|
// Files in subdirectories should match
|
||||||
|
assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz")));
|
||||||
|
assert!(m.matches(&RepoPath::from_internal_string("foo/bar/baz/qux")));
|
||||||
|
// Sibling files should not match
|
||||||
|
assert!(!m.matches(&RepoPath::from_internal_string("foo/foo")));
|
||||||
|
// An unrooted "foo/bar" should not match
|
||||||
|
assert!(!m.matches(&RepoPath::from_internal_string("bar/foo/bar")));
|
||||||
|
|
||||||
|
// The matcher should only visit directory foo/ in the root (file "foo"
|
||||||
|
// shouldn't be visited)
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::root()),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::Set(hashset! {RepoPathComponent::from("foo")}),
|
||||||
|
files: VisitFiles::Set(hashset! {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Inside parent directory "foo/", both subdirectory "bar" and file "bar" may
|
||||||
|
// match
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::from_internal_string("foo")),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::Set(hashset! {RepoPathComponent::from("bar")}),
|
||||||
|
files: VisitFiles::Set(hashset! {RepoPathComponent::from("bar")}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Inside a directory that matches the prefix, everything may match (in does in
|
||||||
|
// fact match, as tested by m.matches() earlier)
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::from_internal_string("foo/bar")),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::All,
|
||||||
|
files: VisitFiles::All,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Same thing in subdirectories of the prefix
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::from_internal_string("foo/bar/baz")),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::All,
|
||||||
|
files: VisitFiles::All,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Nothing in directories that are siblings of the prefix can match, so don't
|
||||||
|
// visit
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::from_internal_string("bar")),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::Set(hashset! {}),
|
||||||
|
files: VisitFiles::Set(hashset! {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prefixmatcher_nested_prefixes() {
|
||||||
|
let m = PrefixMatcher::new(&[
|
||||||
|
RepoPath::from_internal_string("foo"),
|
||||||
|
RepoPath::from_internal_string("foo/bar/baz"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
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")));
|
||||||
|
// Matches because the the "foo" pattern matches
|
||||||
|
assert!(m.matches(&RepoPath::from_internal_string("foo/baz/foo")));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::root()),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::Set(hashset! {RepoPathComponent::from("foo")}),
|
||||||
|
files: VisitFiles::Set(hashset! {RepoPathComponent::from("foo")}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Inside a directory that matches the prefix, everything may match (in does in
|
||||||
|
// fact match, as tested by m.matches() earlier)
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::from_internal_string("foo")),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::All,
|
||||||
|
files: VisitFiles::All,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Same thing in subdirectories of the prefix
|
||||||
|
assert_eq!(
|
||||||
|
m.visit(&RepoPath::from_internal_string("foo/bar/baz")),
|
||||||
|
Visit {
|
||||||
|
dirs: VisitDirs::All,
|
||||||
|
files: VisitFiles::All,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,10 @@ impl RepoPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_components(components: Vec<RepoPathComponent>) -> Self {
|
||||||
|
RepoPath { components }
|
||||||
|
}
|
||||||
|
|
||||||
/// The full string form used internally, not for presenting to users (where
|
/// The full string form used internally, not for presenting to users (where
|
||||||
/// we may want to use the platform's separator). This format includes a
|
/// we may want to use the platform's separator). This format includes a
|
||||||
/// trailing slash, unless this path represents the root directory. That
|
/// trailing slash, unless this path represents the root directory. That
|
||||||
|
|
Loading…
Reference in a new issue