diff --git a/Cargo.lock b/Cargo.lock index 7c6c6279..5d98af94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -892,6 +892,24 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.7.0" @@ -2651,6 +2669,7 @@ dependencies = [ "futures", "git2", "globset", + "ignore", "miette", "nom 7.0.0", "notify", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index c1fd412e..fa727f25 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -23,6 +23,7 @@ dunce = "1.0.2" futures = "0.3.16" git2 = "0.13.22" globset = "0.4.8" +ignore = "0.4.18" miette = "3.2.0" nom = "7.0.0" notify = "5.0.0-pre.12" diff --git a/lib/src/filter/tagged.rs b/lib/src/filter/tagged.rs index ad2d2979..3c174da1 100644 --- a/lib/src/filter/tagged.rs +++ b/lib/src/filter/tagged.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use std::sync::Arc; use dunce::canonicalize; +use ignore::gitignore::{Gitignore, GitignoreBuilder}; use tokio::fs::read_to_string; use tracing::{debug, trace, warn}; use unicase::UniCase; @@ -35,6 +36,12 @@ pub struct TaggedFilterer { /// All filters that are applied, in order, by matcher. filters: swaplock::SwapLock>>, + + /// Compiled matcher for Glob filters. + glob_compiled: swaplock::SwapLock>, + + /// Compiled matcher for NotGlob filters. + not_glob_compiled: swaplock::SwapLock>, } impl Filterer for TaggedFilterer { @@ -108,6 +115,8 @@ impl TaggedFilterer { origin: canonicalize(origin.into())?, workdir: canonicalize(workdir.into())?, filters: swaplock::SwapLock::new(HashMap::new()), + glob_compiled: swaplock::SwapLock::new(None), + not_glob_compiled: swaplock::SwapLock::new(None), })) } @@ -141,7 +150,12 @@ impl TaggedFilterer { }; trace!(?resolved, "resolved path to match filter against"); - filter.matches(resolved.to_string_lossy()) + + if matches!(filter.op, Op::Glob | Op::NotGlob) { + todo!("glob match using compiled ignores"); + } else { + filter.matches(resolved.to_string_lossy()) + } } (Tag::FileEventKind(kind), Matcher::FileEventKind) => { filter.matches(format!("{:?}", kind)) @@ -189,7 +203,7 @@ impl TaggedFilterer { } }) .await - .map_err(|err| error::TaggedFiltererError::FilterChange { action: "add", err })?; + .map_err(|err| TaggedFiltererError::FilterChange { action: "add", err })?; if recompile_globs { self.recompile_globs(Op::Glob).await?; @@ -202,13 +216,46 @@ impl TaggedFilterer { Ok(()) } - async fn recompile_globs(&self, op_filter: Op) -> Result<(), error::TaggedFiltererError> { - todo!() + // TODO: globs for non-paths??? - // globs: - // - use ignore's impl - // - after adding some filters, recompile by making a gitignorebuilder and storing the gitignore - // - use two gitignores: one for NotGlob (which is used for gitignores) and one for Glob (invert results from its matches) + async fn recompile_globs(&self, op_filter: Op) -> Result<(), TaggedFiltererError> { + 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) { + // 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::>() + } else { + return target + .replace(None) + .await + .map_err(TaggedFiltererError::GlobsetChange); + } + }; + + let mut builder = GitignoreBuilder::new(&self.origin); + for filter in globs { + if let Pattern::Glob(glob) = filter.pat { + builder + .add_line(filter.in_path, &glob) + .map_err(TaggedFiltererError::GlobParse)?; + } + } + + let compiled = builder.build().map_err(TaggedFiltererError::GlobParse)?; + + target + .replace(Some(compiled)) + .await + .map_err(TaggedFiltererError::GlobsetChange) } pub async fn add_ignore_file(&self, file: &IgnoreFile) -> Result<(), TaggedFiltererError> { diff --git a/lib/src/filter/tagged/error.rs b/lib/src/filter/tagged/error.rs index 87d46aa2..f51f19cb 100644 --- a/lib/src/filter/tagged/error.rs +++ b/lib/src/filter/tagged/error.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; +use ignore::gitignore::Gitignore; use miette::Diagnostic; use thiserror::Error; use tokio::sync::watch::error::SendError; @@ -31,7 +32,7 @@ pub enum TaggedFiltererError { /// Error received when a filter cannot be added or removed from a tagged filter list. #[error("cannot {action} filter: {err:?}")] - #[diagnostic(code(watchexec::filter::tagged::change))] + #[diagnostic(code(watchexec::filter::tagged::filter_change))] FilterChange { action: &'static str, #[source] @@ -41,7 +42,12 @@ pub enum TaggedFiltererError { /// Error received when a glob cannot be parsed. #[error("cannot parse glob: {0}")] #[diagnostic(code(watchexec::filter::tagged::glob_parse))] - GlobParse(#[from] globset::Error), + GlobParse(#[source] ignore::Error), + + /// Error received when a compiled globset cannot be changed. + #[error("cannot change compiled globset: {0:?}")] + #[diagnostic(code(watchexec::filter::tagged::globset_change))] + GlobsetChange(#[source] SendError>), } impl From for RuntimeError {