jj/lib/src/matchers.rs
Martin von Zweigbergk 03ae1b747c repo_path: remove FileRepoPath in favor of just RepoPath
I had initially hoped that the type-safety provided by the separate
`FileRepoPath` and `DirRepoPath` types would help prevent bugs. I'm
not sure if it has prevented any bugs so far. It has turned out that
there are more cases than I had hoped where it's unknown whether a
path is for a directory or a file. One such example is for the path of
a conflict. Since it can be conflict between a directory and a file,
it doesn't make sense to use either. Instead we end up with quite a
bit of conversion between the types. I feel like they are not worth
the extra complexity. This patch therefore starts simplifying it by
replacing uses of `FileRepoPath` by `RepoPath`. `DirRepoPath` is a
little more complicated because its string form ends with a '/'. I'll
address that in separate patches.
2021-05-19 15:11:04 -07:00

250 lines
6.9 KiB
Rust

// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(dead_code)]
use std::collections::{HashMap, HashSet};
use crate::repo_path::{DirRepoPath, DirRepoPathComponent, RepoPath, RepoPathComponent};
#[derive(PartialEq, Eq, Debug)]
pub struct Visit<'a> {
dirs: VisitDirs<'a>,
files: VisitFiles<'a>,
}
#[derive(PartialEq, Eq, Debug)]
pub enum VisitDirs<'a> {
All,
Set(&'a HashSet<DirRepoPathComponent>),
}
#[derive(PartialEq, Eq, Debug)]
pub enum VisitFiles<'a> {
All,
Set(&'a HashSet<RepoPathComponent>),
}
pub trait Matcher {
fn matches(&self, file: &RepoPath) -> bool;
fn visit(&self, dir: &DirRepoPath) -> Visit;
}
#[derive(PartialEq, Eq, Debug)]
pub struct EverythingMatcher;
impl Matcher for EverythingMatcher {
fn matches(&self, _file: &RepoPath) -> bool {
true
}
fn visit(&self, _dir: &DirRepoPath) -> Visit {
Visit {
dirs: VisitDirs::All,
files: VisitFiles::All,
}
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct FilesMatcher {
files: HashSet<RepoPath>,
dirs: Dirs,
}
impl FilesMatcher {
fn new(files: HashSet<RepoPath>) -> Self {
let mut dirs = Dirs::new();
for f in &files {
dirs.add_file(f);
}
FilesMatcher { files, dirs }
}
}
impl Matcher for FilesMatcher {
fn matches(&self, file: &RepoPath) -> bool {
self.files.contains(file)
}
fn visit(&self, dir: &DirRepoPath) -> Visit {
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
/// visited.
#[derive(PartialEq, Eq, Debug)]
struct Dirs {
dirs: HashMap<DirRepoPath, HashSet<DirRepoPathComponent>>,
files: HashMap<DirRepoPath, HashSet<RepoPathComponent>>,
empty_dirs: HashSet<DirRepoPathComponent>,
empty_files: HashSet<RepoPathComponent>,
}
impl Dirs {
fn new() -> Self {
Dirs {
dirs: HashMap::new(),
files: HashMap::new(),
empty_dirs: HashSet::new(),
empty_files: HashSet::new(),
}
}
fn add_dir(&mut self, mut dir: DirRepoPath) {
let mut maybe_child = None;
loop {
let was_present = self.dirs.contains_key(&dir);
let children = self.dirs.entry(dir.clone()).or_default();
if let Some(child) = maybe_child {
children.insert(child);
}
if was_present {
break;
}
match dir.split() {
None => break,
Some((new_dir, new_child)) => {
dir = new_dir;
maybe_child = Some(new_child);
}
};
}
}
fn add_file(&mut self, file: &RepoPath) {
let (dir, basename) = file
.split()
.unwrap_or_else(|| panic!("got empty filename: {:?}", file));
self.add_dir(dir.clone());
self.files
.entry(dir.clone())
.or_default()
.insert(basename.clone());
}
fn get_dirs(&self, dir: &DirRepoPath) -> &HashSet<DirRepoPathComponent> {
self.dirs.get(&dir).unwrap_or(&self.empty_dirs)
}
fn get_files(&self, dir: &DirRepoPath) -> &HashSet<RepoPathComponent> {
self.files.get(&dir).unwrap_or(&self.empty_files)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::repo_path::{DirRepoPath, DirRepoPathComponent, RepoPath, RepoPathComponent};
#[test]
fn dirs_empty() {
let dirs = Dirs::new();
assert_eq!(dirs.get_dirs(&DirRepoPath::root()), &hashset! {});
}
#[test]
fn dirs_root() {
let mut dirs = Dirs::new();
dirs.add_dir(DirRepoPath::root());
assert_eq!(dirs.get_dirs(&DirRepoPath::root()), &hashset! {});
}
#[test]
fn dirs_dir() {
let mut dirs = Dirs::new();
dirs.add_dir(DirRepoPath::from("dir/"));
assert_eq!(
dirs.get_dirs(&DirRepoPath::root()),
&hashset! {DirRepoPathComponent::from("dir")}
);
}
#[test]
fn dirs_file() {
let mut dirs = Dirs::new();
dirs.add_file(&RepoPath::from("dir/file"));
assert_eq!(
dirs.get_dirs(&DirRepoPath::root()),
&hashset! {DirRepoPathComponent::from("dir")}
);
assert_eq!(dirs.get_files(&DirRepoPath::root()), &hashset! {});
}
#[test]
fn filesmatcher_empty() {
let m = FilesMatcher::new(HashSet::new());
assert!(!m.matches(&RepoPath::from("file")));
assert!(!m.matches(&RepoPath::from("dir/file")));
assert_eq!(
m.visit(&DirRepoPath::root()),
Visit {
dirs: VisitDirs::Set(&HashSet::new()),
files: VisitFiles::Set(&HashSet::new()),
}
);
}
#[test]
fn filesmatcher_nonempty() {
let m = FilesMatcher::new(hashset! {
RepoPath::from("dir1/subdir1/file1"),
RepoPath::from("dir1/subdir1/file2"),
RepoPath::from("dir1/subdir2/file3"),
RepoPath::from("file4"),
});
assert_eq!(
m.visit(&DirRepoPath::root()),
Visit {
dirs: VisitDirs::Set(&hashset! {DirRepoPathComponent::from("dir1")}),
files: VisitFiles::Set(&hashset! {RepoPathComponent::from("file4")}),
}
);
assert_eq!(
m.visit(&DirRepoPath::from("dir1/")),
Visit {
dirs: VisitDirs::Set(
&hashset! {DirRepoPathComponent::from("subdir1"), DirRepoPathComponent::from("subdir2")}
),
files: VisitFiles::Set(&hashset! {}),
}
);
assert_eq!(
m.visit(&DirRepoPath::from("dir1/subdir1/")),
Visit {
dirs: VisitDirs::Set(&hashset! {}),
files: VisitFiles::Set(
&hashset! {RepoPathComponent::from("file1"), RepoPathComponent::from("file2")}
),
}
);
assert_eq!(
m.visit(&DirRepoPath::from("dir1/subdir2/")),
Visit {
dirs: VisitDirs::Set(&hashset! {}),
files: VisitFiles::Set(&hashset! {RepoPathComponent::from("file3")}),
}
);
}
}