Implement path filtering

This commit is contained in:
Félix Saparelli 2021-09-30 04:03:46 +13:00
parent 288ce9d2f4
commit 07878f8357
No known key found for this signature in database
GPG key ID: B948C4BAE44FC474
7 changed files with 87 additions and 23 deletions

View file

@ -93,7 +93,7 @@ fn runtime(args: &ArgMatches<'static>) -> Result<(RuntimeConfig, Arc<TaggedFilte
let print_events = args.is_present("print-events");
let once = args.is_present("once");
let filterer = TaggedFilterer::new(".", ".");
let filterer = TaggedFilterer::new(".", ".")?;
config.filterer(filterer.clone());
config.on_action(move |action: Action| {

View file

@ -41,6 +41,7 @@ impl fmt::Debug for WorkingData {
.field("shell", &self.shell)
.field("command", &self.command)
.field("grouped", &self.grouped)
.field("filterer", &self.filterer)
.finish_non_exhaustive()
}
}

View file

@ -5,7 +5,7 @@ use crate::{error::RuntimeError, event::Event};
pub mod globset;
pub mod tagged;
pub trait Filterer: Send + Sync {
pub trait Filterer: std::fmt::Debug + Send + Sync {
fn check_event(&self, event: &Event) -> Result<bool, RuntimeError>;
}

View file

@ -6,6 +6,7 @@ use crate::error::RuntimeError;
use crate::event::Event;
use crate::filter::Filterer;
#[derive(Debug)]
pub struct GlobsetFilterer {
_root: PathBuf,
}

View file

@ -1,7 +1,9 @@
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use dunce::canonicalize;
use globset::GlobMatcher;
use regex::Regex;
use tracing::{debug, trace, warn};
@ -14,6 +16,7 @@ use crate::filter::Filterer;
mod parse;
pub mod swaplock;
#[derive(Debug)]
pub struct TaggedFilterer {
/// The directory the project is in, its "root".
///
@ -104,19 +107,50 @@ pub struct Filter {
}
impl TaggedFilterer {
pub fn new(root: impl Into<PathBuf>, workdir: impl Into<PathBuf>) -> Arc<Self> {
Arc::new(Self {
root: root.into(),
workdir: workdir.into(),
pub fn new(
root: impl Into<PathBuf>,
workdir: impl Into<PathBuf>,
) -> Result<Arc<Self>, RuntimeError> {
// TODO: make it criticalerror
Ok(Arc::new(Self {
root: canonicalize(root.into())?,
workdir: canonicalize(workdir.into())?,
filters: swaplock::SwapLock::new(HashMap::new()),
})
}))
}
// filter ctx event path filter outcome
// /foo/bar /foo/bar/baz.txt baz.txt pass
// /foo/bar /foo/bar/baz.txt /baz.txt pass
// /foo/bar /foo/bar/baz.txt /baz.* pass
// /foo/bar /foo/bar/baz.txt /blah fail
// /foo/quz /foo/bar/baz.txt /baz.* skip
// TODO: lots of tests
// Ok(Some(bool)) => the match was applied, bool is the result
// Ok(None) => for some precondition, the match was not done (mismatched tag, out of context, …)
fn match_tag(&self, filter: &Filter, tag: &Tag) -> Result<Option<bool>, RuntimeError> {
trace!(?tag, matcher=?filter.on, "matching filter to tag");
match (tag, filter.on) {
(tag, Matcher::Tag) => filter.matches(tag.discriminant_name()),
(Tag::Path(_path), Matcher::Path) => todo!("tagged filterer: path matcher"),
(Tag::Path(path), Matcher::Path) => {
let resolved = if let Some(ctx) = &filter.in_path {
if let Ok(suffix) = path.strip_prefix(ctx) {
suffix.strip_prefix("/").unwrap_or(suffix)
} else {
return Ok(None);
}
} else if let Ok(suffix) = path.strip_prefix(&self.workdir) {
suffix.strip_prefix("/").unwrap_or(suffix)
} else if let Ok(suffix) = path.strip_prefix(&self.root) {
suffix.strip_prefix("/").unwrap_or(suffix)
} else {
path.strip_prefix("/").unwrap_or(path)
};
trace!(?resolved, "resolved path to match filter against");
filter.matches(resolved.to_string_lossy())
}
(Tag::FileEventKind(kind), Matcher::FileEventKind) => {
filter.matches(format!("{:?}", kind))
}
@ -134,8 +168,14 @@ impl TaggedFilterer {
.map(Some)
}
pub async fn add_filter(&self, filter: Filter) -> Result<(), RuntimeError> {
pub async fn add_filter(&self, mut filter: Filter) -> Result<(), RuntimeError> {
debug!(?filter, "adding filter to filterer");
if let Some(ctx) = &mut filter.in_path {
*ctx = canonicalize(&ctx)?;
trace!(canon=?ctx, "canonicalised in_path");
}
self.filters
.change(|filters| {
filters.entry(filter.on).or_default().push(filter);
@ -146,13 +186,23 @@ impl TaggedFilterer {
}
pub async fn remove_filter(&self, filter: &Filter) -> Result<(), RuntimeError> {
let filter = if let Some(ctx) = &filter.in_path {
let f = filter.clone();
Cow::Owned(Filter {
in_path: Some(canonicalize(ctx)?),
..f
})
} else {
Cow::Borrowed(filter)
};
debug!(?filter, "removing filter from filterer");
self.filters
.change(|filters| {
filters
.entry(filter.on)
.or_default()
.retain(|f| f != filter);
.retain(|f| f != filter.as_ref());
})
.await
.map_err(|err| RuntimeError::FilterChange {
@ -176,6 +226,7 @@ impl TaggedFilterer {
}
impl Filter {
// TODO non-unicode matching
pub fn matches(&self, subject: impl AsRef<str>) -> Result<bool, RuntimeError> {
let subject = subject.as_ref();

View file

@ -96,19 +96,26 @@ impl FromStr for Filter {
},
pat: match (o, m) {
// TODO: carry regex/glob errors through
(Op::Auto | Op::Glob, Matcher::Path) => {
Pattern::Glob(Glob::new(p).map_err(drop)?.compile_matcher())
(Op::Auto | Op::Glob, Matcher::Path) | (Op::Glob | Op::NotGlob, _) => {
Pattern::Glob(
if let Some(bare) = p.strip_prefix('/') {
trace!(original=?p, ?bare, "glob pattern is absolute, stripping prefix /");
Glob::new(bare)
} else {
trace!(original=?p, "glob pattern is relative, so prefixing with `**/`");
Glob::new(&format!("**/{}", p))
}
(Op::Equal | Op::NotEqual, _) => Pattern::Exact(p.to_string()),
(Op::Glob | Op::NotGlob, _) => {
Pattern::Glob(Glob::new(p).map_err(drop)?.compile_matcher())
}
(Op::Regex | Op::NotRegex, _) => {
Pattern::Regex(Regex::new(p).map_err(drop)?)
.map_err(drop)?
.compile_matcher(),
)
}
(Op::Auto | Op::InSet | Op::NotInSet, _) => {
Pattern::Set(p.split(',').map(|s| s.trim().to_string()).collect())
}
(Op::Regex | Op::NotRegex, _) => {
Pattern::Regex(Regex::new(p).map_err(drop)?)
}
(Op::Equal | Op::NotEqual, _) => Pattern::Exact(p.to_string()),
},
negate: n.is_some(),
})

View file

@ -62,9 +62,13 @@ pub struct WorkingData {
///
/// While you can run several, you should only have one.
///
/// This only does a bare minimum of setup; to actually start the work, you need to set a non-empty pathset on the
/// [`WorkingData`] with the [`watch`] channel, and send a notification. Take care _not_ to drop the watch sender:
/// this will cause the worker to stop gracefully, which may not be what was expected.
/// This only does a bare minimum of setup; to actually start the work, you need to set a non-empty
/// pathset on the [`WorkingData`] with the [`watch`] channel, and send a notification. Take care
/// _not_ to drop the watch sender: this will cause the worker to stop gracefully, which may not be
/// what was expected.
///
/// Note that the paths emitted by the watcher are canonicalised. No guarantee is made about the
/// implementation or output of that canonicalisation (i.e. it might not be `std`'s).
///
/// # Examples
///
@ -146,7 +150,7 @@ pub async fn worker(
}
}) {
Ok(w) => {
watcher.insert(w);
watcher = Some(w);
watcher_type = kind;
}
Err(e) => {