mirror of
https://github.com/watchexec/watchexec.git
synced 2024-11-14 08:11:11 +01:00
Implement path filtering
This commit is contained in:
parent
288ce9d2f4
commit
07878f8357
7 changed files with 87 additions and 23 deletions
|
@ -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| {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::error::RuntimeError;
|
|||
use crate::event::Event;
|
||||
use crate::filter::Filterer;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GlobsetFilterer {
|
||||
_root: PathBuf,
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in a new issue