2021-10-16 06:13:32 +02:00
|
|
|
//! A complex filterer that can match any event tag and supports different matching operators.
|
|
|
|
|
2021-09-27 13:54:33 +02:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2021-09-23 11:59:35 +02:00
|
|
|
use std::path::PathBuf;
|
2021-09-28 11:21:13 +02:00
|
|
|
use std::sync::Arc;
|
2021-09-23 11:59:35 +02:00
|
|
|
|
2021-09-29 17:03:46 +02:00
|
|
|
use dunce::canonicalize;
|
2021-10-13 14:26:15 +02:00
|
|
|
use globset::Glob;
|
2021-10-12 14:49:38 +02:00
|
|
|
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
2021-10-12 17:06:39 +02:00
|
|
|
use ignore::Match;
|
2021-10-27 14:01:35 +02:00
|
|
|
use tracing::{debug, trace, trace_span, warn};
|
2021-09-29 15:34:27 +02:00
|
|
|
use unicase::UniCase;
|
2021-09-27 13:54:33 +02:00
|
|
|
|
2021-09-23 11:59:35 +02:00
|
|
|
use crate::error::RuntimeError;
|
2022-01-16 07:14:31 +01:00
|
|
|
use crate::error::TaggedFiltererError;
|
2022-06-11 08:43:11 +02:00
|
|
|
use crate::event::{Event, FileType, Priority, ProcessEnd, Tag};
|
2021-09-23 11:59:35 +02:00
|
|
|
use crate::filter::Filterer;
|
2022-01-16 02:51:35 +01:00
|
|
|
use crate::ignore::{IgnoreFile, IgnoreFilterer};
|
2021-12-21 06:05:52 +01:00
|
|
|
use crate::signal::process::SubSignal;
|
2021-12-21 04:38:57 +01:00
|
|
|
use crate::signal::source::MainSignal;
|
2022-01-28 11:32:54 +01:00
|
|
|
use crate::swaplock::SwapLock;
|
2021-10-09 07:45:32 +02:00
|
|
|
|
|
|
|
// to make filters
|
|
|
|
pub use regex::Regex;
|
2021-09-23 11:59:35 +02:00
|
|
|
|
2021-12-23 14:20:45 +01:00
|
|
|
pub mod files;
|
2021-09-23 11:59:35 +02:00
|
|
|
mod parse;
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// A filterer implementation that exposes the full capabilities of Watchexec.
|
|
|
|
///
|
2021-12-17 11:19:16 +01:00
|
|
|
/// **Note:** This filterer is experimental, and behaviour may change without semver notice. However,
|
|
|
|
/// types and its API are held to semver. This notice will eventually be removed when it stabilises.
|
|
|
|
///
|
2021-10-16 16:54:48 +02:00
|
|
|
/// Filters match against [event tags][Tag]; can be exact matches, glob matches, regex matches, or
|
|
|
|
/// set matches; can reverse the match (equal/not equal, etc); and can be negated.
|
2021-10-16 12:14:57 +02:00
|
|
|
///
|
|
|
|
/// [Filters][Filter] can be generated from your application and inserted directly, or they can be
|
|
|
|
/// parsed from a textual format:
|
|
|
|
///
|
|
|
|
/// ```text
|
2021-12-18 00:44:59 +01:00
|
|
|
/// [!]{Matcher}{Op}{Value}
|
2021-10-16 12:14:57 +02:00
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// For example:
|
|
|
|
///
|
|
|
|
/// ```text
|
|
|
|
/// path==/foo/bar
|
|
|
|
/// path*=**/bar
|
|
|
|
/// path~=bar$
|
2021-12-18 00:44:59 +01:00
|
|
|
/// !kind=file
|
2021-10-16 12:14:57 +02:00
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// There is a set of [operators][Op]:
|
2021-10-16 17:03:18 +02:00
|
|
|
/// - `==` and `!=`: exact match and exact not match (case insensitive)
|
2021-10-16 12:14:57 +02:00
|
|
|
/// - `~=` and `~!`: regex match and regex not match
|
|
|
|
/// - `*=` and `*!`: glob match and glob not match
|
|
|
|
/// - `:=` and `:!`: set match and set not match
|
|
|
|
///
|
|
|
|
/// Sets are a list of values separated by `,`.
|
|
|
|
///
|
2021-12-18 00:44:59 +01:00
|
|
|
/// In addition to the two-symbol operators, there is the `=` "auto" operator, which maps to the
|
|
|
|
/// most convenient operator for the given _matcher_. The current mapping is:
|
|
|
|
///
|
|
|
|
/// | Matcher | Operator |
|
|
|
|
/// |-------------------------------------------------|---------------|
|
|
|
|
/// | [Tag](Matcher::Tag) | `:=` (in set) |
|
|
|
|
/// | [Path](Matcher::Path) | `*=` (glob) |
|
|
|
|
/// | [FileType](Matcher::FileType) | `:=` (in set) |
|
|
|
|
/// | [FileEventKind](Matcher::FileEventKind) | `*=` (glob) |
|
|
|
|
/// | [Source](Matcher::Source) | `:=` (in set) |
|
|
|
|
/// | [Process](Matcher::Process) | `:=` (in set) |
|
|
|
|
/// | [Signal](Matcher::Signal) | `:=` (in set) |
|
2022-06-11 08:43:11 +02:00
|
|
|
/// | [ProcessCompletion](Matcher::ProcessCompletion) | `*=` (glob) |
|
|
|
|
/// | [Priority](Matcher::Priority) | `:=` (in set) |
|
2021-10-16 12:14:57 +02:00
|
|
|
///
|
|
|
|
/// [Matchers][Matcher] correspond to Tags, but are not one-to-one: the `path` matcher operates on
|
|
|
|
/// the `path` part of the `Path` tag, and the `type` matcher operates on the `file_type`, for
|
|
|
|
/// example.
|
|
|
|
///
|
2021-12-18 00:44:59 +01:00
|
|
|
/// | Matcher | Syntax | Tag |
|
|
|
|
/// |------------------------------------|----------|----------------------------------------------|
|
|
|
|
/// | [Tag](Matcher::Tag) | `tag` | _the presence of a Tag on the event_ |
|
|
|
|
/// | [Path](Matcher::Path) | `path` | [Path](Tag::Path) (`path` field) |
|
|
|
|
/// | [FileType](Matcher::FileType) | `type` | [Path](Tag::Path) (`file_type` field, when Some) |
|
|
|
|
/// | [FileEventKind](Matcher::FileEventKind) | `kind` or `fek` | [FileEventKind](Tag::FileEventKind) |
|
|
|
|
/// | [Source](Matcher::Source) | `source` or `src` | [Source](Tag::Source) |
|
|
|
|
/// | [Process](Matcher::Process) | `process` or `pid` | [Process](Tag::Process) |
|
|
|
|
/// | [Signal](Matcher::Signal) | `signal` | [Signal](Tag::Signal) |
|
|
|
|
/// | [ProcessCompletion](Matcher::ProcessCompletion) | `complete` or `exit` | [ProcessCompletion](Tag::ProcessCompletion) |
|
2022-06-11 08:43:11 +02:00
|
|
|
/// | [Priority](Matcher::Priority) | `priority` | special: event [Priority] |
|
2021-12-18 00:44:59 +01:00
|
|
|
///
|
2021-10-16 12:14:57 +02:00
|
|
|
/// Filters are checked in order, grouped per tag and per matcher. Filter groups may be checked in
|
|
|
|
/// any order, but the filters in the groups are checked in add order. Path glob filters are always
|
|
|
|
/// checked first, for internal reasons.
|
|
|
|
///
|
|
|
|
/// The `negate` boolean field behaves specially: it is not operator negation, but rather the same
|
|
|
|
/// kind of behaviour that is applied to `!`-prefixed globs in gitignore files: if a negated filter
|
|
|
|
/// matches the event, the result of the event checking for that matcher is reverted to `true`, even
|
|
|
|
/// if a previous filter set it to `false`. Unmatched negated filters are ignored.
|
|
|
|
///
|
|
|
|
/// Glob syntax is as supported by the [ignore] crate for Paths, and by [globset] otherwise. (As of
|
|
|
|
/// writing, the ignore crate uses globset internally). Regex syntax is the default syntax of the
|
|
|
|
/// [regex] crate.
|
2021-09-29 17:03:46 +02:00
|
|
|
#[derive(Debug)]
|
2021-09-23 11:59:35 +02:00
|
|
|
pub struct TaggedFilterer {
|
2021-10-09 07:45:32 +02:00
|
|
|
/// The directory the project is in, its origin.
|
2021-09-27 13:54:33 +02:00
|
|
|
///
|
|
|
|
/// This is used to resolve absolute paths without an `in_path` context.
|
2021-10-09 07:45:32 +02:00
|
|
|
origin: PathBuf,
|
2021-09-27 13:54:33 +02:00
|
|
|
|
|
|
|
/// Where the program is running from.
|
|
|
|
///
|
|
|
|
/// This is used to resolve relative paths without an `in_path` context.
|
2021-09-28 11:21:13 +02:00
|
|
|
workdir: PathBuf,
|
2021-09-27 13:54:33 +02:00
|
|
|
|
|
|
|
/// All filters that are applied, in order, by matcher.
|
2022-01-28 11:32:54 +01:00
|
|
|
filters: SwapLock<HashMap<Matcher, Vec<Filter>>>,
|
2021-10-12 14:49:38 +02:00
|
|
|
|
2022-01-16 02:51:35 +01:00
|
|
|
/// Sub-filterer for ignore files.
|
2022-01-28 11:32:54 +01:00
|
|
|
ignore_filterer: SwapLock<IgnoreFilterer>,
|
2022-01-16 02:51:35 +01:00
|
|
|
|
2021-10-12 14:49:38 +02:00
|
|
|
/// Compiled matcher for Glob filters.
|
2022-01-28 11:32:54 +01:00
|
|
|
glob_compiled: SwapLock<Option<Gitignore>>,
|
2021-10-12 14:49:38 +02:00
|
|
|
|
|
|
|
/// Compiled matcher for NotGlob filters.
|
2022-01-28 11:32:54 +01:00
|
|
|
not_glob_compiled: SwapLock<Option<Gitignore>>,
|
2021-09-23 11:59:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Filterer for TaggedFilterer {
|
2022-06-11 08:43:11 +02:00
|
|
|
fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> {
|
|
|
|
self.check(event, priority).map_err(|e| e.into())
|
2021-10-09 07:41:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TaggedFilterer {
|
2022-06-11 08:43:11 +02:00
|
|
|
fn check(&self, event: &Event, priority: Priority) -> Result<bool, TaggedFiltererError> {
|
2021-10-27 14:01:35 +02:00
|
|
|
let _span = trace_span!("filterer_check").entered();
|
2022-06-11 08:43:11 +02:00
|
|
|
trace!(?event, ?priority, "checking event");
|
|
|
|
|
|
|
|
{
|
|
|
|
trace!("checking priority");
|
|
|
|
if let Some(filters) = self.filters.borrow().get(&Matcher::Priority).cloned() {
|
|
|
|
trace!(filters=%filters.len(), "found some filters for priority");
|
|
|
|
//
|
|
|
|
let mut pri_match = true;
|
|
|
|
for filter in &filters {
|
|
|
|
let _span = trace_span!("checking filter against priority", ?filter).entered();
|
|
|
|
let applies = filter.matches(match priority {
|
|
|
|
Priority::Low => "low",
|
|
|
|
Priority::Normal => "normal",
|
|
|
|
Priority::High => "high",
|
|
|
|
Priority::Urgent => unreachable!("urgent by-passes filtering"),
|
|
|
|
})?;
|
|
|
|
if filter.negate {
|
|
|
|
if applies {
|
|
|
|
trace!(prev=%pri_match, now=%true, "negate filter passes, passing this priority");
|
|
|
|
pri_match = true;
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
trace!(prev=%pri_match, now=%pri_match, "negate filter fails, ignoring");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
trace!(prev=%pri_match, this=%applies, now=%(pri_match&applies), "filter applies to priority");
|
|
|
|
pri_match &= applies;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !pri_match {
|
|
|
|
trace!("priority fails check, failing entire event");
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
trace!("no filters for priority, skipping (pass)");
|
|
|
|
}
|
|
|
|
}
|
2021-10-12 17:06:39 +02:00
|
|
|
|
2022-01-16 02:51:35 +01:00
|
|
|
{
|
|
|
|
trace!("checking internal ignore filterer");
|
|
|
|
let igf = self.ignore_filterer.borrow();
|
2022-06-11 08:43:11 +02:00
|
|
|
if !igf
|
|
|
|
.check_event(event, priority)
|
|
|
|
.expect("IgnoreFilterer never errors")
|
|
|
|
{
|
2022-01-16 02:51:35 +01:00
|
|
|
trace!("internal ignore filterer matched (fail)");
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-28 11:21:13 +02:00
|
|
|
if self.filters.borrow().is_empty() {
|
|
|
|
trace!("no filters, skipping entire check (pass)");
|
2021-09-27 13:54:33 +02:00
|
|
|
return Ok(true);
|
|
|
|
}
|
|
|
|
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(tags=%event.tags.len(), "checking all tags on the event");
|
2021-09-27 13:54:33 +02:00
|
|
|
for tag in &event.tags {
|
2021-10-27 14:01:35 +02:00
|
|
|
let _span = trace_span!("check_tag", ?tag).entered();
|
|
|
|
|
|
|
|
trace!("checking tag");
|
2021-10-13 14:26:15 +02:00
|
|
|
for matcher in Matcher::from_tag(tag) {
|
2021-10-27 14:01:35 +02:00
|
|
|
let _span = trace_span!("check_matcher", ?matcher).entered();
|
|
|
|
|
2021-10-13 14:26:15 +02:00
|
|
|
let filters = self.filters.borrow().get(matcher).cloned();
|
|
|
|
if let Some(tag_filters) = filters {
|
|
|
|
if tag_filters.is_empty() {
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!("no filters for this matcher, skipping (pass)");
|
2021-10-13 14:26:15 +02:00
|
|
|
continue;
|
|
|
|
}
|
2021-10-12 17:06:39 +02:00
|
|
|
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!(filters=%tag_filters.len(), "found some filters for this matcher");
|
2021-10-13 14:26:15 +02:00
|
|
|
|
|
|
|
let mut tag_match = true;
|
|
|
|
|
|
|
|
if let (Matcher::Path, Tag::Path { path, file_type }) = (matcher, tag) {
|
2021-10-19 14:18:43 +02:00
|
|
|
let is_dir = file_type.map_or(false, |ft| matches!(ft, FileType::Dir));
|
2021-10-13 14:26:15 +02:00
|
|
|
|
2022-01-16 02:51:11 +01:00
|
|
|
{
|
2022-01-16 02:51:35 +01:00
|
|
|
let gc = self.glob_compiled.borrow();
|
|
|
|
if let Some(igs) = gc.as_ref() {
|
2022-01-16 02:51:11 +01:00
|
|
|
let _span =
|
|
|
|
trace_span!("checking_compiled_filters", compiled=%"Glob")
|
2022-01-16 02:51:35 +01:00
|
|
|
.entered();
|
|
|
|
match if path.strip_prefix(&self.origin).is_ok() {
|
|
|
|
trace!("checking against path or parents");
|
|
|
|
igs.matched_path_or_any_parents(path, is_dir)
|
|
|
|
} else {
|
|
|
|
trace!("checking against path only");
|
|
|
|
igs.matched(path, is_dir)
|
|
|
|
} {
|
|
|
|
Match::None => {
|
|
|
|
trace!("no match (fail)");
|
|
|
|
tag_match &= false;
|
|
|
|
}
|
|
|
|
Match::Ignore(glob) => {
|
2022-01-16 02:51:11 +01:00
|
|
|
if glob
|
|
|
|
.from()
|
|
|
|
.map_or(true, |f| path.strip_prefix(f).is_ok())
|
|
|
|
{
|
2022-01-16 02:51:35 +01:00
|
|
|
trace!(?glob, "positive match (pass)");
|
|
|
|
tag_match &= true;
|
|
|
|
} else {
|
2022-01-16 02:51:11 +01:00
|
|
|
trace!(
|
|
|
|
?glob,
|
|
|
|
"positive match, but not in scope (ignore)"
|
|
|
|
);
|
2022-01-16 02:51:35 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Match::Whitelist(glob) => {
|
|
|
|
trace!(?glob, "negative match (ignore)");
|
2021-12-02 09:33:48 +01:00
|
|
|
}
|
2021-10-13 14:26:15 +02:00
|
|
|
}
|
2021-10-12 17:06:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-16 02:51:11 +01:00
|
|
|
{
|
2022-01-16 02:51:35 +01:00
|
|
|
let ngc = self.not_glob_compiled.borrow();
|
|
|
|
if let Some(ngs) = ngc.as_ref() {
|
|
|
|
let _span =
|
|
|
|
trace_span!("checking_compiled_filters", compiled=%"NotGlob")
|
|
|
|
.entered();
|
|
|
|
match if path.strip_prefix(&self.origin).is_ok() {
|
|
|
|
trace!("checking against path or parents");
|
|
|
|
ngs.matched_path_or_any_parents(path, is_dir)
|
|
|
|
} else {
|
|
|
|
trace!("checking against path only");
|
|
|
|
ngs.matched(path, is_dir)
|
|
|
|
} {
|
|
|
|
Match::None => {
|
|
|
|
trace!("no match (pass)");
|
|
|
|
tag_match &= true;
|
|
|
|
}
|
|
|
|
Match::Ignore(glob) => {
|
2022-01-16 02:51:11 +01:00
|
|
|
if glob
|
|
|
|
.from()
|
|
|
|
.map_or(true, |f| path.strip_prefix(f).is_ok())
|
|
|
|
{
|
2022-01-16 02:51:35 +01:00
|
|
|
trace!(?glob, "positive match (fail)");
|
|
|
|
tag_match &= false;
|
|
|
|
} else {
|
2022-01-16 02:51:11 +01:00
|
|
|
trace!(
|
|
|
|
?glob,
|
|
|
|
"positive match, but not in scope (ignore)"
|
|
|
|
);
|
2022-01-16 02:51:35 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Match::Whitelist(glob) => {
|
|
|
|
trace!(?glob, "negative match (pass)");
|
|
|
|
tag_match = true;
|
2021-12-02 09:33:48 +01:00
|
|
|
}
|
2021-10-13 14:26:15 +02:00
|
|
|
}
|
2021-10-12 17:06:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 14:26:15 +02:00
|
|
|
// those are handled with the compiled ignore filters above
|
|
|
|
let tag_filters = tag_filters
|
|
|
|
.into_iter()
|
|
|
|
.filter(|f| {
|
|
|
|
!matches!(
|
|
|
|
(tag, matcher, f),
|
|
|
|
(
|
|
|
|
Tag::Path { .. },
|
|
|
|
Matcher::Path,
|
|
|
|
Filter {
|
|
|
|
on: Matcher::Path,
|
|
|
|
op: Op::Glob | Op::NotGlob,
|
|
|
|
pat: Pattern::Glob(_),
|
|
|
|
..
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
2021-11-27 14:22:07 +01:00
|
|
|
if tag_filters.is_empty() && tag_match {
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!("no more filters for this matcher, skipping (pass)");
|
2021-10-13 14:26:15 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!(filters=%tag_filters.len(), "got some filters to check still");
|
2021-10-13 14:26:15 +02:00
|
|
|
|
|
|
|
for filter in &tag_filters {
|
2022-01-15 11:46:06 +01:00
|
|
|
let _span = trace_span!("checking filter against tag", ?filter).entered();
|
2021-10-13 14:26:15 +02:00
|
|
|
if let Some(app) = self.match_tag(filter, tag)? {
|
|
|
|
if filter.negate {
|
|
|
|
if app {
|
2021-10-16 12:14:57 +02:00
|
|
|
trace!(prev=%tag_match, now=%true, "negate filter passes, passing this matcher");
|
2021-10-13 14:26:15 +02:00
|
|
|
tag_match = true;
|
2021-10-16 12:14:57 +02:00
|
|
|
break;
|
2021-10-13 14:26:15 +02:00
|
|
|
} else {
|
|
|
|
trace!(prev=%tag_match, now=%tag_match, "negate filter fails, ignoring");
|
|
|
|
}
|
2021-09-28 11:21:13 +02:00
|
|
|
} else {
|
2021-10-13 14:26:15 +02:00
|
|
|
trace!(prev=%tag_match, this=%app, now=%(tag_match&app), "filter applies to this tag");
|
|
|
|
tag_match &= app;
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 14:26:15 +02:00
|
|
|
if !tag_match {
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!("matcher fails check, failing entire event");
|
2021-10-13 14:26:15 +02:00
|
|
|
return Ok(false);
|
|
|
|
}
|
2021-09-28 11:21:13 +02:00
|
|
|
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!("matcher passes check, continuing");
|
2021-10-13 14:26:15 +02:00
|
|
|
} else {
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!("no filters for this matcher, skipping (pass)");
|
2021-10-13 14:26:15 +02:00
|
|
|
}
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!("passing event");
|
2021-09-27 13:54:33 +02:00
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// Initialise a new tagged filterer with no filters.
|
|
|
|
///
|
|
|
|
/// This takes two paths: the project origin, and the current directory. The current directory
|
|
|
|
/// is not obtained from the environment so you can customise it; generally you should use
|
|
|
|
/// [`std::env::current_dir()`] though.
|
|
|
|
///
|
|
|
|
/// The origin is the directory the main project that is being watched is in. This is used to
|
|
|
|
/// resolve absolute paths given in filters without an `in_path` field (e.g. all filters parsed
|
2022-01-16 02:51:35 +01:00
|
|
|
/// from text), and for ignore file based filtering.
|
2021-10-16 12:14:57 +02:00
|
|
|
///
|
|
|
|
/// The workdir is used to resolve relative paths given in filters without an `in_path` field.
|
|
|
|
///
|
|
|
|
/// So, if origin is `/path/to/project` and workdir is `/path/to/project/subtree`:
|
|
|
|
/// - `path=foo.bar` is resolved to `/path/to/project/subtree/foo.bar`
|
|
|
|
/// - `path=/foo.bar` is resolved to `/path/to/project/foo.bar`
|
2021-09-29 17:03:46 +02:00
|
|
|
pub fn new(
|
2021-10-12 14:49:11 +02:00
|
|
|
origin: impl Into<PathBuf>,
|
2021-09-29 17:03:46 +02:00
|
|
|
workdir: impl Into<PathBuf>,
|
2021-10-12 14:49:11 +02:00
|
|
|
) -> Result<Arc<Self>, TaggedFiltererError> {
|
2022-01-16 07:46:52 +01:00
|
|
|
let origin = canonicalize(origin.into()).map_err(|err| TaggedFiltererError::IoError {
|
|
|
|
about: "canonicalise origin on new tagged filterer",
|
|
|
|
err,
|
|
|
|
})?;
|
2021-09-29 17:03:46 +02:00
|
|
|
Ok(Arc::new(Self {
|
2022-01-28 11:32:54 +01:00
|
|
|
filters: SwapLock::new(HashMap::new()),
|
|
|
|
ignore_filterer: SwapLock::new(IgnoreFilterer::empty(&origin)),
|
|
|
|
glob_compiled: SwapLock::new(None),
|
|
|
|
not_glob_compiled: SwapLock::new(None),
|
2022-01-16 07:46:52 +01:00
|
|
|
workdir: canonicalize(workdir.into()).map_err(|err| TaggedFiltererError::IoError {
|
|
|
|
about: "canonicalise workdir on new tagged filterer",
|
|
|
|
err,
|
|
|
|
})?,
|
2022-01-16 02:51:35 +01:00
|
|
|
origin,
|
2021-09-29 17:03:46 +02:00
|
|
|
}))
|
2021-09-28 11:21:51 +02:00
|
|
|
}
|
|
|
|
|
2021-09-29 17:03:46 +02:00
|
|
|
// 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
|
|
|
|
|
|
|
|
// 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, …)
|
2021-10-12 14:49:11 +02:00
|
|
|
fn match_tag(&self, filter: &Filter, tag: &Tag) -> Result<Option<bool>, TaggedFiltererError> {
|
2021-10-27 14:01:35 +02:00
|
|
|
trace!(matcher=?filter.on, "matching filter to tag");
|
2021-09-27 13:54:33 +02:00
|
|
|
match (tag, filter.on) {
|
|
|
|
(tag, Matcher::Tag) => filter.matches(tag.discriminant_name()),
|
2021-10-12 17:06:39 +02:00
|
|
|
(Tag::Path { path, .. }, Matcher::Path) => {
|
2021-09-29 17:03:46 +02:00
|
|
|
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)
|
2021-10-09 07:45:32 +02:00
|
|
|
} else if let Ok(suffix) = path.strip_prefix(&self.origin) {
|
2021-09-29 17:03:46 +02:00
|
|
|
suffix.strip_prefix("/").unwrap_or(suffix)
|
|
|
|
} else {
|
|
|
|
path.strip_prefix("/").unwrap_or(path)
|
|
|
|
};
|
|
|
|
|
|
|
|
trace!(?resolved, "resolved path to match filter against");
|
2021-10-12 14:49:38 +02:00
|
|
|
|
|
|
|
if matches!(filter.op, Op::Glob | Op::NotGlob) {
|
2021-10-13 13:13:48 +02:00
|
|
|
trace!("path glob match with match_tag is already handled");
|
|
|
|
return Ok(None);
|
2021-10-12 14:49:38 +02:00
|
|
|
} else {
|
|
|
|
filter.matches(resolved.to_string_lossy())
|
|
|
|
}
|
2021-09-29 17:03:46 +02:00
|
|
|
}
|
2021-10-12 17:06:55 +02:00
|
|
|
(
|
|
|
|
Tag::Path {
|
|
|
|
file_type: Some(ft),
|
|
|
|
..
|
|
|
|
},
|
|
|
|
Matcher::FileType,
|
2021-10-19 14:18:43 +02:00
|
|
|
) => filter.matches(ft.to_string()),
|
2021-09-28 11:21:13 +02:00
|
|
|
(Tag::FileEventKind(kind), Matcher::FileEventKind) => {
|
|
|
|
filter.matches(format!("{:?}", kind))
|
|
|
|
}
|
2021-09-27 13:54:33 +02:00
|
|
|
(Tag::Source(src), Matcher::Source) => filter.matches(src.to_string()),
|
|
|
|
(Tag::Process(pid), Matcher::Process) => filter.matches(pid.to_string()),
|
2021-12-21 04:38:57 +01:00
|
|
|
(Tag::Signal(sig), Matcher::Signal) => {
|
|
|
|
let (text, int) = match sig {
|
|
|
|
MainSignal::Hangup => ("HUP", 1),
|
|
|
|
MainSignal::Interrupt => ("INT", 2),
|
|
|
|
MainSignal::Quit => ("QUIT", 3),
|
|
|
|
MainSignal::Terminate => ("TERM", 15),
|
|
|
|
MainSignal::User1 => ("USR1", 10),
|
|
|
|
MainSignal::User2 => ("USR2", 12),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(filter.matches(text)?
|
|
|
|
|| filter.matches(format!("SIG{}", text))?
|
|
|
|
|| filter.matches(int.to_string())?)
|
|
|
|
}
|
2021-12-21 05:56:14 +01:00
|
|
|
(Tag::ProcessCompletion(ope), Matcher::ProcessCompletion) => match ope {
|
|
|
|
None => filter.matches("_"),
|
|
|
|
Some(ProcessEnd::Success) => filter.matches("success"),
|
|
|
|
Some(ProcessEnd::ExitError(int)) => filter.matches(format!("error({})", int)),
|
2021-12-21 06:05:52 +01:00
|
|
|
Some(ProcessEnd::ExitSignal(sig)) => {
|
|
|
|
let (text, int) = match sig {
|
|
|
|
SubSignal::Hangup | SubSignal::Custom(1) => ("HUP", 1),
|
|
|
|
SubSignal::ForceStop | SubSignal::Custom(9) => ("KILL", 9),
|
|
|
|
SubSignal::Interrupt | SubSignal::Custom(2) => ("INT", 2),
|
|
|
|
SubSignal::Quit | SubSignal::Custom(3) => ("QUIT", 3),
|
|
|
|
SubSignal::Terminate | SubSignal::Custom(15) => ("TERM", 15),
|
|
|
|
SubSignal::User1 | SubSignal::Custom(10) => ("USR1", 10),
|
|
|
|
SubSignal::User2 | SubSignal::Custom(12) => ("USR2", 12),
|
|
|
|
SubSignal::Custom(n) => ("UNK", *n),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(filter.matches(format!("signal({})", text))?
|
|
|
|
|| filter.matches(format!("signal(SIG{})", text))?
|
|
|
|
|| filter.matches(format!("signal({})", int))?)
|
|
|
|
}
|
2021-12-21 05:56:14 +01:00
|
|
|
Some(ProcessEnd::ExitStop(int)) => filter.matches(format!("stop({})", int)),
|
2021-12-22 12:32:56 +01:00
|
|
|
Some(ProcessEnd::Exception(int)) => filter.matches(format!("exception({:X})", int)),
|
2021-12-21 05:56:14 +01:00
|
|
|
Some(ProcessEnd::Continued) => filter.matches("continued"),
|
|
|
|
},
|
2021-10-27 14:01:35 +02:00
|
|
|
(_, _) => {
|
|
|
|
trace!("no match for tag, skipping");
|
2021-09-28 11:21:13 +02:00
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.map(Some)
|
|
|
|
}
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// Add some filters to the filterer.
|
|
|
|
///
|
|
|
|
/// This is async as it submits the new filters to the live filterer, which may be holding a
|
|
|
|
/// read lock. It takes a slice of filters so it can efficiently add a large number of filters
|
|
|
|
/// with a single write, without needing to acquire the lock repeatedly.
|
|
|
|
///
|
|
|
|
/// If filters with glob operations are added, the filterer's glob matchers are recompiled after
|
2022-01-16 02:51:35 +01:00
|
|
|
/// the new filters are added, in this method. This should not be used for inserting an
|
|
|
|
/// [`IgnoreFile`]: use [`add_ignore_file()`](Self::add_ignore_file) instead.
|
2021-10-12 14:49:11 +02:00
|
|
|
pub async fn add_filters(&self, filters: &[Filter]) -> Result<(), TaggedFiltererError> {
|
2021-10-12 13:48:42 +02:00
|
|
|
debug!(?filters, "adding filters to filterer");
|
2021-09-29 17:03:46 +02:00
|
|
|
|
2021-10-12 13:48:42 +02:00
|
|
|
let mut recompile_globs = false;
|
|
|
|
let mut recompile_not_globs = false;
|
|
|
|
|
|
|
|
let filters = filters
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.inspect(|f| match f.op {
|
|
|
|
Op::Glob => {
|
|
|
|
recompile_globs = true;
|
|
|
|
}
|
|
|
|
Op::NotGlob => {
|
|
|
|
recompile_not_globs = true;
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
})
|
|
|
|
.map(Filter::canonicalised)
|
|
|
|
.collect::<Result<Vec<_>, _>>()?;
|
2021-11-29 14:11:50 +01:00
|
|
|
trace!(?filters, "canonicalised filters");
|
2021-10-12 13:48:42 +02:00
|
|
|
// TODO: use miette's related and issue canonicalisation errors for all of them
|
2021-09-29 17:03:46 +02:00
|
|
|
|
2021-09-28 11:21:51 +02:00
|
|
|
self.filters
|
2021-10-12 13:48:42 +02:00
|
|
|
.change(|fs| {
|
|
|
|
for filter in filters {
|
|
|
|
fs.entry(filter.on).or_default().push(filter);
|
|
|
|
}
|
2021-09-28 11:21:51 +02:00
|
|
|
})
|
|
|
|
.await
|
2021-10-12 14:49:38 +02:00
|
|
|
.map_err(|err| TaggedFiltererError::FilterChange { action: "add", err })?;
|
2021-11-29 14:11:50 +01:00
|
|
|
trace!("inserted filters into swaplock");
|
2021-10-12 13:48:42 +02:00
|
|
|
|
|
|
|
if recompile_globs {
|
|
|
|
self.recompile_globs(Op::Glob).await?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if recompile_not_globs {
|
|
|
|
self.recompile_globs(Op::NotGlob).await?;
|
|
|
|
}
|
|
|
|
|
2021-09-28 11:21:51 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-10-12 14:49:38 +02:00
|
|
|
async fn recompile_globs(&self, op_filter: Op) -> Result<(), TaggedFiltererError> {
|
2021-11-29 14:11:50 +01:00
|
|
|
trace!(?op_filter, "recompiling globs");
|
2021-10-12 14:49:38 +02:00
|
|
|
let target = match op_filter {
|
|
|
|
Op::Glob => &self.glob_compiled,
|
|
|
|
Op::NotGlob => &self.not_glob_compiled,
|
|
|
|
_ => unreachable!("recompile_globs called with invalid op"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let globs = {
|
|
|
|
let filters = self.filters.borrow();
|
|
|
|
if let Some(fs) = filters.get(&Matcher::Path) {
|
2021-11-29 14:11:50 +01:00
|
|
|
trace!(?op_filter, "pulling filters from swaplock");
|
2021-10-12 14:49:38 +02:00
|
|
|
// we want to hold the lock as little as possible, so we clone the filters
|
|
|
|
fs.iter()
|
|
|
|
.cloned()
|
|
|
|
.filter(|f| f.op == op_filter)
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
} else {
|
2021-11-29 14:11:50 +01:00
|
|
|
trace!(?op_filter, "no filters, erasing compiled glob");
|
2021-10-12 14:49:38 +02:00
|
|
|
return target
|
|
|
|
.replace(None)
|
|
|
|
.await
|
|
|
|
.map_err(TaggedFiltererError::GlobsetChange);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut builder = GitignoreBuilder::new(&self.origin);
|
|
|
|
for filter in globs {
|
2021-12-14 16:57:17 +01:00
|
|
|
if let Pattern::Glob(mut glob) = filter.pat {
|
|
|
|
if filter.negate {
|
|
|
|
glob.insert(0, '!');
|
|
|
|
}
|
|
|
|
|
2021-11-29 14:11:50 +01:00
|
|
|
trace!(?op_filter, in_path=?filter.in_path, ?glob, "adding new glob line");
|
2021-10-12 14:49:38 +02:00
|
|
|
builder
|
|
|
|
.add_line(filter.in_path, &glob)
|
|
|
|
.map_err(TaggedFiltererError::GlobParse)?;
|
|
|
|
}
|
|
|
|
}
|
2021-10-12 13:48:42 +02:00
|
|
|
|
2021-11-29 14:11:50 +01:00
|
|
|
trace!(?op_filter, "finalising compiled glob");
|
2021-10-12 14:49:38 +02:00
|
|
|
let compiled = builder.build().map_err(TaggedFiltererError::GlobParse)?;
|
|
|
|
|
2021-11-29 14:11:50 +01:00
|
|
|
trace!(?op_filter, "swapping in new compiled glob");
|
2021-10-12 14:49:38 +02:00
|
|
|
target
|
|
|
|
.replace(Some(compiled))
|
|
|
|
.await
|
|
|
|
.map_err(TaggedFiltererError::GlobsetChange)
|
2021-10-11 12:34:14 +02:00
|
|
|
}
|
2021-09-29 17:03:46 +02:00
|
|
|
|
2022-01-16 02:51:35 +01:00
|
|
|
/// Reads a gitignore-style [`IgnoreFile`] and adds it to the filterer.
|
2021-10-12 14:49:11 +02:00
|
|
|
pub async fn add_ignore_file(&self, file: &IgnoreFile) -> Result<(), TaggedFiltererError> {
|
2022-01-16 02:51:35 +01:00
|
|
|
let mut new = { self.ignore_filterer.borrow().clone() };
|
2021-10-12 13:48:42 +02:00
|
|
|
|
2022-01-16 02:51:35 +01:00
|
|
|
new.add_file(file)
|
|
|
|
.await
|
|
|
|
.map_err(TaggedFiltererError::Ignore)?;
|
|
|
|
self.ignore_filterer
|
|
|
|
.replace(new)
|
|
|
|
.await
|
|
|
|
.map_err(TaggedFiltererError::IgnoreSwap)?;
|
|
|
|
Ok(())
|
2021-09-28 11:21:51 +02:00
|
|
|
}
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// Clears all filters from the filterer.
|
|
|
|
///
|
|
|
|
/// This also recompiles the glob matchers, so essentially it resets the entire filterer state.
|
2021-10-12 14:49:11 +02:00
|
|
|
pub async fn clear_filters(&self) -> Result<(), TaggedFiltererError> {
|
2021-09-28 11:21:51 +02:00
|
|
|
debug!("removing all filters from filterer");
|
|
|
|
self.filters
|
|
|
|
.replace(Default::default())
|
|
|
|
.await
|
2021-10-12 14:49:11 +02:00
|
|
|
.map_err(|err| TaggedFiltererError::FilterChange {
|
2021-09-28 11:21:51 +02:00
|
|
|
action: "clear all",
|
|
|
|
err,
|
|
|
|
})?;
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
self.recompile_globs(Op::Glob).await?;
|
|
|
|
self.recompile_globs(Op::NotGlob).await?;
|
|
|
|
|
2021-09-28 11:21:51 +02:00
|
|
|
Ok(())
|
2021-09-23 11:59:35 +02:00
|
|
|
}
|
2021-10-09 07:45:32 +02:00
|
|
|
}
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// A tagged filter.
|
2021-10-09 07:45:32 +02:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub struct Filter {
|
|
|
|
/// Path the filter applies from.
|
|
|
|
pub in_path: Option<PathBuf>,
|
|
|
|
|
|
|
|
/// Which tag the filter applies to.
|
|
|
|
pub on: Matcher,
|
|
|
|
|
|
|
|
/// The operation to perform on the tag's value.
|
|
|
|
pub op: Op,
|
|
|
|
|
|
|
|
/// The pattern to match against the tag's value.
|
|
|
|
pub pat: Pattern,
|
|
|
|
|
|
|
|
/// If true, a positive match with this filter will override negative matches from previous
|
|
|
|
/// filters on the same tag, and negative matches will be ignored.
|
|
|
|
pub negate: bool,
|
2021-09-23 11:59:35 +02:00
|
|
|
}
|
2021-09-27 13:54:33 +02:00
|
|
|
|
|
|
|
impl Filter {
|
2021-10-16 12:14:57 +02:00
|
|
|
/// Matches the filter against a subject.
|
|
|
|
///
|
|
|
|
/// This is really an internal method to the tagged filterer machinery, exposed so you can build
|
|
|
|
/// your own filterer using the same types or the textual syntax. As such its behaviour is not
|
|
|
|
/// guaranteed to be stable (its signature is, though).
|
2021-10-12 14:49:11 +02:00
|
|
|
pub fn matches(&self, subject: impl AsRef<str>) -> Result<bool, TaggedFiltererError> {
|
2021-09-27 13:54:33 +02:00
|
|
|
let subject = subject.as_ref();
|
|
|
|
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(op=?self.op, pat=?self.pat, ?subject, "performing filter match");
|
|
|
|
Ok(match (self.op, &self.pat) {
|
2021-09-29 15:34:27 +02:00
|
|
|
(Op::Equal, Pattern::Exact(pat)) => UniCase::new(subject) == UniCase::new(pat),
|
|
|
|
(Op::NotEqual, Pattern::Exact(pat)) => UniCase::new(subject) != UniCase::new(pat),
|
2021-09-28 11:21:13 +02:00
|
|
|
(Op::Regex, Pattern::Regex(pat)) => pat.is_match(subject),
|
|
|
|
(Op::NotRegex, Pattern::Regex(pat)) => !pat.is_match(subject),
|
|
|
|
(Op::InSet, Pattern::Set(set)) => set.contains(subject),
|
|
|
|
(Op::InSet, Pattern::Exact(pat)) => subject == pat,
|
|
|
|
(Op::NotInSet, Pattern::Set(set)) => !set.contains(subject),
|
|
|
|
(Op::NotInSet, Pattern::Exact(pat)) => subject != pat,
|
2021-10-13 14:26:15 +02:00
|
|
|
(op @ Op::Glob | op @ Op::NotGlob, Pattern::Glob(glob)) => {
|
|
|
|
// FIXME: someway that isn't this horrible
|
|
|
|
match Glob::new(glob) {
|
|
|
|
Ok(glob) => {
|
|
|
|
let matches = glob.compile_matcher().is_match(subject);
|
|
|
|
match op {
|
|
|
|
Op::Glob => matches,
|
|
|
|
Op::NotGlob => !matches,
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
warn!(
|
|
|
|
"failed to compile glob for non-path match, skipping (pass): {}",
|
|
|
|
err
|
|
|
|
);
|
|
|
|
true
|
|
|
|
}
|
|
|
|
}
|
2021-10-11 12:34:14 +02:00
|
|
|
}
|
2021-09-27 13:54:33 +02:00
|
|
|
(op, pat) => {
|
2021-09-28 11:21:13 +02:00
|
|
|
warn!(
|
|
|
|
"trying to match pattern {:?} with op {:?}, that cannot work",
|
|
|
|
pat, op
|
|
|
|
);
|
|
|
|
false
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
2021-09-28 11:21:13 +02:00
|
|
|
})
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
2021-10-12 13:48:42 +02:00
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// Create a filter from a gitignore-style glob pattern.
|
|
|
|
///
|
|
|
|
/// The optional path is for the `in_path` field of the filter. When parsing gitignore files, it
|
|
|
|
/// should be set to the path of the _directory_ the ignore file is in.
|
|
|
|
///
|
|
|
|
/// The resulting filter matches on [`Path`][Matcher::Path], with the [`NotGlob`][Op::NotGlob]
|
|
|
|
/// op, and a [`Glob`][Pattern::Glob] pattern. If it starts with a `!`, it is negated.
|
2021-10-12 13:48:42 +02:00
|
|
|
pub fn from_glob_ignore(in_path: Option<PathBuf>, glob: &str) -> Self {
|
|
|
|
let (glob, negate) = glob.strip_prefix('!').map_or((glob, false), |g| (g, true));
|
|
|
|
|
|
|
|
Self {
|
|
|
|
in_path,
|
|
|
|
on: Matcher::Path,
|
|
|
|
op: Op::NotGlob,
|
|
|
|
pat: Pattern::Glob(glob.to_string()),
|
|
|
|
negate,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// Returns the filter with its `in_path` canonicalised.
|
|
|
|
pub fn canonicalised(mut self) -> Result<Self, TaggedFiltererError> {
|
2021-10-12 13:48:42 +02:00
|
|
|
if let Some(ctx) = self.in_path {
|
2022-01-16 07:46:52 +01:00
|
|
|
self.in_path =
|
|
|
|
Some(
|
|
|
|
canonicalize(&ctx).map_err(|err| TaggedFiltererError::IoError {
|
|
|
|
about: "canonicalise Filter in_path",
|
|
|
|
err,
|
|
|
|
})?,
|
|
|
|
);
|
2021-10-12 13:48:42 +02:00
|
|
|
trace!(canon=?ctx, "canonicalised in_path");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(self)
|
|
|
|
}
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// What a filter matches on.
|
2021-09-27 13:54:33 +02:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
|
|
|
#[non_exhaustive]
|
|
|
|
pub enum Matcher {
|
2021-10-16 12:14:57 +02:00
|
|
|
/// The presence of a tag on an event.
|
2021-09-27 13:54:33 +02:00
|
|
|
Tag,
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// A path in a filesystem event. Paths are always canonicalised.
|
|
|
|
///
|
|
|
|
/// Note that there may be multiple paths in an event (e.g. both source and destination for renames), and filters
|
|
|
|
/// will be matched on all of them.
|
2021-09-27 13:54:33 +02:00
|
|
|
Path,
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// The file type of an object in a filesystem event.
|
|
|
|
///
|
|
|
|
/// This is not guaranteed to be present for every filesystem event.
|
|
|
|
///
|
2021-10-19 14:18:43 +02:00
|
|
|
/// It can be any of these values: `file`, `dir`, `symlink`, `other`. That last one means
|
|
|
|
/// "not any of the first three."
|
2021-10-12 17:06:55 +02:00
|
|
|
FileType,
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// The [`EventKind`][notify::event::EventKind] of a filesystem event.
|
|
|
|
///
|
|
|
|
/// This is the Debug representation of the event kind. Examples:
|
|
|
|
/// - `Access(Close(Write))`
|
|
|
|
/// - `Modify(Data(Any))`
|
|
|
|
/// - `Modify(Metadata(Permissions))`
|
|
|
|
/// - `Remove(Folder)`
|
|
|
|
///
|
|
|
|
/// You should probably use globs or regexes to match these, ex:
|
|
|
|
/// - `Create(*)`
|
|
|
|
/// - `Modify\(Name\(.+`
|
2021-09-27 13:54:33 +02:00
|
|
|
FileEventKind,
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// The [event source][crate::event::Source] the event came from.
|
|
|
|
///
|
|
|
|
/// These are the lowercase names of the variants.
|
2021-09-27 13:54:33 +02:00
|
|
|
Source,
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// The ID of the process which caused the event.
|
|
|
|
///
|
|
|
|
/// Note that it's rare for events to carry this information.
|
2021-09-27 13:54:33 +02:00
|
|
|
Process,
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// A signal sent to the main process.
|
|
|
|
///
|
|
|
|
/// This can be matched both on the signal number as an integer, and on the signal name as a
|
2022-06-11 08:43:11 +02:00
|
|
|
/// string. On Windows, only `BREAK` is supported; `CTRL_C` parses but won't work. Matching is
|
2021-10-16 12:14:57 +02:00
|
|
|
/// on both uppercase and lowercase forms.
|
2022-06-11 08:43:11 +02:00
|
|
|
///
|
|
|
|
/// Interrupt signals (`TERM` and `INT` on Unix, `CTRL_C` on Windows) are parsed, but these are
|
|
|
|
/// marked Urgent internally to Watchexec, and thus bypass filtering entirely.
|
2021-09-27 13:54:33 +02:00
|
|
|
Signal,
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// The exit status of a subprocess.
|
|
|
|
///
|
|
|
|
/// This is only present for events issued when the subprocess exits. The value is matched on
|
|
|
|
/// both the exit code as an integer, and either `success` or `fail`, whichever succeeds.
|
2021-09-27 13:54:33 +02:00
|
|
|
ProcessCompletion,
|
2022-06-11 08:43:11 +02:00
|
|
|
|
|
|
|
/// The [`Priority`] of the event.
|
|
|
|
///
|
|
|
|
/// This is never `urgent`, as urgent events bypass filtering.
|
|
|
|
Priority,
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
|
2021-10-13 14:26:15 +02:00
|
|
|
impl Matcher {
|
|
|
|
fn from_tag(tag: &Tag) -> &'static [Self] {
|
2021-09-27 13:54:33 +02:00
|
|
|
match tag {
|
2021-10-13 14:26:15 +02:00
|
|
|
Tag::Path {
|
|
|
|
file_type: None, ..
|
|
|
|
} => &[Matcher::Path],
|
|
|
|
Tag::Path { .. } => &[Matcher::Path, Matcher::FileType],
|
|
|
|
Tag::FileEventKind(_) => &[Matcher::FileEventKind],
|
|
|
|
Tag::Source(_) => &[Matcher::Source],
|
|
|
|
Tag::Process(_) => &[Matcher::Process],
|
|
|
|
Tag::Signal(_) => &[Matcher::Signal],
|
|
|
|
Tag::ProcessCompletion(_) => &[Matcher::ProcessCompletion],
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// How a filter value is interpreted.
|
|
|
|
///
|
2021-10-16 17:03:18 +02:00
|
|
|
/// - `==` and `!=` match on the exact value as string equality (case-insensitively),
|
2021-10-16 12:14:57 +02:00
|
|
|
/// - `~=` and `~!` match using a [regex],
|
|
|
|
/// - `*=` and `*!` match using a glob, either via [globset] or [ignore]
|
|
|
|
/// - `:=` and `:!` match via exact string comparisons, but on any of the list of values separated
|
|
|
|
/// by `,`
|
|
|
|
/// - `=`, the "auto" operator, behaves as `*=` if the matcher is `Path`, and as `==` otherwise.
|
2021-09-27 13:54:33 +02:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
#[non_exhaustive]
|
|
|
|
pub enum Op {
|
2021-10-16 12:14:57 +02:00
|
|
|
/// The auto operator, `=`, resolves to `*=` or `==` depending on the matcher.
|
|
|
|
Auto,
|
|
|
|
|
|
|
|
/// The `==` operator, matches on exact string equality.
|
|
|
|
Equal,
|
|
|
|
|
|
|
|
/// The `!=` operator, matches on exact string inequality.
|
|
|
|
NotEqual,
|
|
|
|
|
|
|
|
/// The `~=` operator, matches on a regex.
|
|
|
|
Regex,
|
|
|
|
|
|
|
|
/// The `~!` operator, matches on a regex (matches are fails).
|
|
|
|
NotRegex,
|
|
|
|
|
|
|
|
/// The `*=` operator, matches on a glob.
|
|
|
|
Glob,
|
|
|
|
|
|
|
|
/// The `*!` operator, matches on a glob (matches are fails).
|
|
|
|
NotGlob,
|
|
|
|
|
|
|
|
/// The `:=` operator, matches (with string compares) on a set of values (belongs are passes).
|
|
|
|
InSet,
|
|
|
|
|
|
|
|
/// The `:!` operator, matches on a set of values (belongs are fails).
|
|
|
|
NotInSet,
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
|
2021-10-16 12:14:57 +02:00
|
|
|
/// A filter value (pattern to match with).
|
2021-09-27 13:54:33 +02:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
#[non_exhaustive]
|
|
|
|
pub enum Pattern {
|
2021-10-16 12:14:57 +02:00
|
|
|
/// An exact string.
|
2021-09-27 13:54:33 +02:00
|
|
|
Exact(String),
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// A regex.
|
2021-09-27 13:54:33 +02:00
|
|
|
Regex(Regex),
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// A glob.
|
|
|
|
///
|
|
|
|
/// This is stored as a string as globs are compiled together rather than on a per-filter basis.
|
2021-10-11 12:34:14 +02:00
|
|
|
Glob(String),
|
2021-10-16 12:14:57 +02:00
|
|
|
|
|
|
|
/// A set of exact strings.
|
2021-09-27 13:54:33 +02:00
|
|
|
Set(HashSet<String>),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PartialEq<Self> for Pattern {
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
match (self, other) {
|
2021-10-11 12:34:14 +02:00
|
|
|
(Self::Exact(l), Self::Exact(r)) | (Self::Glob(l), Self::Glob(r)) => l == r,
|
2021-09-27 13:54:33 +02:00
|
|
|
(Self::Regex(l), Self::Regex(r)) => l.as_str() == r.as_str(),
|
|
|
|
(Self::Set(l), Self::Set(r)) => l == r,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Eq for Pattern {}
|