diff --git a/cli/src/config.rs b/cli/src/config.rs index 294e3f93..a04af1a1 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -93,7 +93,7 @@ fn runtime(args: &ArgMatches<'static>) -> Result<(RuntimeConfig, Arc Result; } diff --git a/lib/src/filter/globset.rs b/lib/src/filter/globset.rs index 6aa3bf3c..bbbcde3a 100644 --- a/lib/src/filter/globset.rs +++ b/lib/src/filter/globset.rs @@ -6,6 +6,7 @@ use crate::error::RuntimeError; use crate::event::Event; use crate::filter::Filterer; +#[derive(Debug)] pub struct GlobsetFilterer { _root: PathBuf, } diff --git a/lib/src/filter/tagged.rs b/lib/src/filter/tagged.rs index 85909c86..ab3cf51c 100644 --- a/lib/src/filter/tagged.rs +++ b/lib/src/filter/tagged.rs @@ -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, workdir: impl Into) -> Arc { - Arc::new(Self { - root: root.into(), - workdir: workdir.into(), + pub fn new( + root: impl Into, + workdir: impl Into, + ) -> Result, 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, 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) -> Result { let subject = subject.as_ref(); diff --git a/lib/src/filter/tagged/parse.rs b/lib/src/filter/tagged/parse.rs index 56fb1b50..f51989bd 100644 --- a/lib/src/filter/tagged/parse.rs +++ b/lib/src/filter/tagged/parse.rs @@ -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::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)?) + (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)) + } + .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(), }) diff --git a/lib/src/fs.rs b/lib/src/fs.rs index 83eef853..e09071ce 100644 --- a/lib/src/fs.rs +++ b/lib/src/fs.rs @@ -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) => {