diff --git a/cli/src/filterer/globset.rs b/cli/src/filterer/globset.rs index 0a3b367..ccad68a 100644 --- a/cli/src/filterer/globset.rs +++ b/cli/src/filterer/globset.rs @@ -6,7 +6,6 @@ use std::{ use clap::ArgMatches; use miette::{IntoDiagnostic, Result}; -use tracing::debug; use watchexec::{ error::RuntimeError, event::{ @@ -18,7 +17,7 @@ use watchexec::{ pub async fn globset(args: &ArgMatches<'static>) -> Result> { let (project_origin, workdir) = super::common::dirs(args).await?; - let ignorefiles = super::common::ignores(args, &project_origin).await?; + let ignore_files = super::common::ignores(args, &project_origin).await?; let mut ignores = Vec::new(); @@ -37,10 +36,6 @@ pub async fn globset(args: &ArgMatches<'static>) -> Result) -> Result, /// The underlying error. #[source] diff --git a/lib/src/filter/globset.rs b/lib/src/filter/globset.rs index ab21f24..2163a49 100644 --- a/lib/src/filter/globset.rs +++ b/lib/src/filter/globset.rs @@ -10,18 +10,19 @@ use tracing::{debug, trace, trace_span}; use crate::error::RuntimeError; use crate::event::{Event, FileType}; use crate::filter::Filterer; -use crate::ignore::IgnoreFile; +use crate::ignore::{IgnoreFile, IgnoreFilterer}; /// A path-only filterer based on globsets. /// /// This filterer mimics the behavior of the `watchexec` v1 filter, but does not match it exactly, -/// due to differing internals. It is intended to be used as a stopgap until the tagged filter -/// reaches a stable state or becomes the default. As such it does not have an updatable -/// configuration. +/// due to differing internals. It is intended to be used as a stopgap until the tagged filterer +/// or another advanced filterer, reaches a stable state or becomes the default. As such it does not +/// have an updatable configuration. #[derive(Debug)] pub struct GlobsetFilterer { filters: Gitignore, ignores: Gitignore, + ignore_files: IgnoreFilterer, extensions: Vec, } @@ -35,32 +36,45 @@ impl GlobsetFilterer { /// The extensions list is used to filter files by extension. /// /// Non-path events are always passed. - pub fn new( + pub async fn new( origin: impl AsRef, filters: impl IntoIterator)>, ignores: impl IntoIterator)>, + ignore_files: impl IntoIterator, extensions: impl IntoIterator, - ) -> Result { - let mut filters_builder = GitignoreBuilder::new(origin); - let mut ignores_builder = filters_builder.clone(); + ) -> Result { + let origin = origin.as_ref(); + let mut filters_builder = GitignoreBuilder::new(&origin); + let mut ignores_builder = GitignoreBuilder::new(&origin); for (filter, in_path) in filters { trace!(filter=?&filter, "add filter to globset filterer"); - filters_builder.add_line(in_path, &filter)?; + filters_builder.add_line(in_path.clone(), &filter) + .map_err(|err| RuntimeError::GlobsetGlob { file: in_path, err }) + ?; } for (ignore, in_path) in ignores { trace!(ignore=?&ignore, "add ignore to globset filterer"); - ignores_builder.add_line(in_path, &ignore)?; + ignores_builder.add_line(in_path.clone(), &ignore) + .map_err(|err| RuntimeError::GlobsetGlob { file: in_path, err })?; } - let filters = filters_builder.build()?; - let ignores = ignores_builder.build()?; + let filters = filters_builder.build() + .map_err(|err| RuntimeError::GlobsetGlob { file: None, err })?; + let ignores = ignores_builder.build() + .map_err(|err| RuntimeError::GlobsetGlob { file: None, err })?; + let extensions: Vec = extensions.into_iter().collect(); + + let mut ignore_files = IgnoreFilterer::new(origin, &ignore_files.into_iter().collect::>()).await?; + ignore_files.finish(); + debug!( num_filters=%filters.num_ignores(), num_neg_filters=%filters.num_whitelists(), num_ignores=%ignores.num_ignores(), + num_in_ignore_files=?ignore_files.num_ignores(), num_neg_ignores=%ignores.num_whitelists(), num_extensions=%extensions.len(), "globset filterer built"); @@ -68,6 +82,7 @@ impl GlobsetFilterer { Ok(Self { filters, ignores, + ignore_files, extensions, }) } @@ -97,9 +112,16 @@ impl Filterer for GlobsetFilterer { /// /// This implementation never errors. fn check_event(&self, event: &Event) -> Result { - // TODO: integrate ignore::Filter - let _span = trace_span!("filterer_check").entered(); + + { + trace!("checking internal ignore filterer"); + if !self.ignore_files.check_event(event).expect("IgnoreFilterer never errors") { + trace!("internal ignore filterer matched (fail)"); + return Ok(false); + } + } + for (path, file_type) in event.paths() { let _span = trace_span!("path", ?path).entered(); let is_dir = file_type diff --git a/lib/src/ignore/filter.rs b/lib/src/ignore/filter.rs index d654b5a..9f2f8de 100644 --- a/lib/src/ignore/filter.rs +++ b/lib/src/ignore/filter.rs @@ -95,8 +95,8 @@ impl IgnoreFilterer { trace!(?line, "adding ignore line"); builder .add_line(file.applies_in.clone(), line) - .map_err(|err| RuntimeError::IgnoreFileGlob { - file: file.path.clone(), + .map_err(|err| RuntimeError::GlobsetGlob { + file: Some(file.path.clone()), err, })?; } @@ -105,8 +105,8 @@ impl IgnoreFilterer { trace!("compiling globset"); let compiled = builder .build() - .map_err(|err| RuntimeError::IgnoreFileGlob { - file: "set of ignores".into(), + .map_err(|err| RuntimeError::GlobsetGlob { + file: None, err, })?; @@ -124,6 +124,11 @@ impl IgnoreFilterer { }) } + /// Returns the number of ignores and allowlists loaded. + pub fn num_ignores(&self) -> (u64, u64) { + (self.compiled.num_ignores(), self.compiled.num_whitelists()) + } + /// Deletes the internal builder, to save memory. /// /// This makes it impossible to add new ignore files without re-compiling the whole set. @@ -154,8 +159,8 @@ impl IgnoreFilterer { trace!(?line, "adding ignore line"); builder .add_line(file.applies_in.clone(), line) - .map_err(|err| RuntimeError::IgnoreFileGlob { - file: file.path.clone(), + .map_err(|err| RuntimeError::GlobsetGlob { + file: Some(file.path.clone()), err, })?; } @@ -174,7 +179,7 @@ impl IgnoreFilterer { trace!("recompiling globset"); let recompiled = builder .build() - .map_err(|err| RuntimeError::IgnoreFileGlob { file, err })?; + .map_err(|err| RuntimeError::GlobsetGlob { file: Some(file), err })?; trace!( new_ignores=%(recompiled.num_ignores() - pre_ignores), @@ -204,8 +209,8 @@ impl IgnoreFilterer { trace!(?line, "adding ignore line"); builder.add_line(applies_in.clone(), line).map_err(|err| { - RuntimeError::IgnoreFileGlob { - file: "manual glob".into(), + RuntimeError::GlobsetGlob { + file: None, err, } })?; diff --git a/lib/tests/filter_globset.rs b/lib/tests/filter_globset.rs index b8815fe..a1fbd45 100644 --- a/lib/tests/filter_globset.rs +++ b/lib/tests/filter_globset.rs @@ -1,9 +1,9 @@ mod helpers; use helpers::globset::*; -#[test] -fn empty_filter_passes_everything() { - let filterer = filt(&[], &[], &[]); +#[tokio::test] +async fn empty_filter_passes_everything() { + let filterer = filt(&[], &[], &[]).await; filterer.file_does_pass("Cargo.toml"); filterer.file_does_pass("Cargo.json"); @@ -21,9 +21,9 @@ fn empty_filter_passes_everything() { filterer.dir_does_pass("apples/oranges/bananas"); } -#[test] -fn exact_filename() { - let filterer = filt(&["Cargo.toml"], &[], &[]); +#[tokio::test] +async fn exact_filename() { + let filterer = filt(&["Cargo.toml"], &[], &[]).await; filterer.file_does_pass("Cargo.toml"); filterer.file_does_pass("/test/foo/bar/Cargo.toml"); @@ -34,9 +34,9 @@ fn exact_filename() { filterer.dir_does_pass("/test/Cargo.toml"); } -#[test] -fn exact_filenames_multiple() { - let filterer = filt(&["Cargo.toml", "package.json"], &[], &[]); +#[tokio::test] +async fn exact_filenames_multiple() { + let filterer = filt(&["Cargo.toml", "package.json"], &[], &[]).await; filterer.file_does_pass("Cargo.toml"); filterer.file_does_pass("/test/foo/bar/Cargo.toml"); @@ -51,9 +51,9 @@ fn exact_filenames_multiple() { filterer.dir_does_pass("/test/package.json"); } -#[test] -fn glob_single_final_ext_star() { - let filterer = filt(&["Cargo.*"], &[], &[]); +#[tokio::test] +async fn glob_single_final_ext_star() { + let filterer = filt(&["Cargo.*"], &[], &[]).await; filterer.file_does_pass("Cargo.toml"); filterer.file_does_pass("Cargo.json"); @@ -63,9 +63,9 @@ fn glob_single_final_ext_star() { filterer.dir_does_pass("Cargo.toml"); } -#[test] -fn glob_star_trailing_slash() { - let filterer = filt(&["Cargo.*/"], &[], &[]); +#[tokio::test] +async fn glob_star_trailing_slash() { + let filterer = filt(&["Cargo.*/"], &[], &[]).await; filterer.file_doesnt_pass("Cargo.toml"); filterer.file_doesnt_pass("Cargo.json"); @@ -76,9 +76,9 @@ fn glob_star_trailing_slash() { filterer.unk_doesnt_pass("Cargo.toml"); } -#[test] -fn glob_star_leading_slash() { - let filterer = filt(&["/Cargo.*"], &[], &[]); +#[tokio::test] +async fn glob_star_leading_slash() { + let filterer = filt(&["/Cargo.*"], &[], &[]).await; filterer.file_does_pass("Cargo.toml"); filterer.file_does_pass("Cargo.json"); @@ -88,9 +88,9 @@ fn glob_star_leading_slash() { filterer.dir_doesnt_pass("foo/Cargo.toml"); } -#[test] -fn glob_leading_double_star() { - let filterer = filt(&["**/possum"], &[], &[]); +#[tokio::test] +async fn glob_leading_double_star() { + let filterer = filt(&["**/possum"], &[], &[]).await; filterer.file_does_pass("possum"); filterer.file_does_pass("foo/bar/possum"); @@ -103,9 +103,9 @@ fn glob_leading_double_star() { filterer.file_doesnt_pass("/foo/bar/rat"); } -#[test] -fn glob_trailing_double_star() { - let filterer = filt(&["possum/**"], &[], &[]); +#[tokio::test] +async fn glob_trailing_double_star() { + let filterer = filt(&["possum/**"], &[], &[]).await; // these do work by expectation and in v1 filterer.file_does_pass("/test/possum/foo/bar"); @@ -117,9 +117,9 @@ fn glob_trailing_double_star() { filterer.file_doesnt_pass("/foo/bar/rat"); } -#[test] -fn glob_middle_double_star() { - let filterer = filt(&["apples/**/oranges"], &[], &[]); +#[tokio::test] +async fn glob_middle_double_star() { + let filterer = filt(&["apples/**/oranges"], &[], &[]).await; filterer.dir_doesnt_pass("/a/folder"); filterer.file_does_pass("apples/carrots/oranges"); @@ -132,9 +132,9 @@ fn glob_middle_double_star() { filterer.dir_doesnt_pass("apples/oranges/bananas"); } -#[test] -fn glob_double_star_trailing_slash() { - let filterer = filt(&["apples/**/oranges/"], &[], &[]); +#[tokio::test] +async fn glob_double_star_trailing_slash() { + let filterer = filt(&["apples/**/oranges/"], &[], &[]).await; filterer.dir_doesnt_pass("/a/folder"); filterer.file_doesnt_pass("apples/carrots/oranges"); @@ -150,9 +150,9 @@ fn glob_double_star_trailing_slash() { filterer.unk_doesnt_pass("apples/carrots/cauliflowers/artichokes/oranges"); } -#[test] -fn ignore_exact_filename() { - let filterer = filt(&[], &["Cargo.toml"], &[]); +#[tokio::test] +async fn ignore_exact_filename() { + let filterer = filt(&[], &["Cargo.toml"], &[]).await; filterer.file_doesnt_pass("Cargo.toml"); filterer.file_doesnt_pass("/test/foo/bar/Cargo.toml"); @@ -163,9 +163,9 @@ fn ignore_exact_filename() { filterer.dir_doesnt_pass("/test/Cargo.toml"); } -#[test] -fn ignore_exact_filenames_multiple() { - let filterer = filt(&[], &["Cargo.toml", "package.json"], &[]); +#[tokio::test] +async fn ignore_exact_filenames_multiple() { + let filterer = filt(&[], &["Cargo.toml", "package.json"], &[]).await; filterer.file_doesnt_pass("Cargo.toml"); filterer.file_doesnt_pass("/test/foo/bar/Cargo.toml"); @@ -180,9 +180,9 @@ fn ignore_exact_filenames_multiple() { filterer.dir_doesnt_pass("/test/package.json"); } -#[test] -fn ignore_glob_single_final_ext_star() { - let filterer = filt(&[], &["Cargo.*"], &[]); +#[tokio::test] +async fn ignore_glob_single_final_ext_star() { + let filterer = filt(&[], &["Cargo.*"], &[]).await; filterer.file_doesnt_pass("Cargo.toml"); filterer.file_doesnt_pass("Cargo.json"); @@ -192,9 +192,9 @@ fn ignore_glob_single_final_ext_star() { filterer.dir_doesnt_pass("Cargo.toml"); } -#[test] -fn ignore_glob_star_trailing_slash() { - let filterer = filt(&[], &["Cargo.*/"], &[]); +#[tokio::test] +async fn ignore_glob_star_trailing_slash() { + let filterer = filt(&[], &["Cargo.*/"], &[]).await; filterer.file_does_pass("Cargo.toml"); filterer.file_does_pass("Cargo.json"); @@ -205,9 +205,9 @@ fn ignore_glob_star_trailing_slash() { filterer.unk_does_pass("Cargo.toml"); } -#[test] -fn ignore_glob_star_leading_slash() { - let filterer = filt(&[], &["/Cargo.*"], &[]); +#[tokio::test] +async fn ignore_glob_star_leading_slash() { + let filterer = filt(&[], &["/Cargo.*"], &[]).await; filterer.file_doesnt_pass("Cargo.toml"); filterer.file_doesnt_pass("Cargo.json"); @@ -217,9 +217,9 @@ fn ignore_glob_star_leading_slash() { filterer.dir_does_pass("foo/Cargo.toml"); } -#[test] -fn ignore_glob_leading_double_star() { - let filterer = filt(&[], &["**/possum"], &[]); +#[tokio::test] +async fn ignore_glob_leading_double_star() { + let filterer = filt(&[], &["**/possum"], &[]).await; filterer.file_doesnt_pass("possum"); filterer.file_doesnt_pass("foo/bar/possum"); @@ -232,9 +232,9 @@ fn ignore_glob_leading_double_star() { filterer.file_does_pass("/foo/bar/rat"); } -#[test] -fn ignore_glob_trailing_double_star() { - let filterer = filt(&[], &["possum/**"], &[]); +#[tokio::test] +async fn ignore_glob_trailing_double_star() { + let filterer = filt(&[], &["possum/**"], &[]).await; filterer.file_does_pass("possum"); filterer.file_doesnt_pass("possum/foo/bar"); @@ -251,9 +251,9 @@ fn ignore_glob_trailing_double_star() { filterer.file_does_pass("/foo/bar/rat"); } -#[test] -fn ignore_glob_middle_double_star() { - let filterer = filt(&[], &["apples/**/oranges"], &[]); +#[tokio::test] +async fn ignore_glob_middle_double_star() { + let filterer = filt(&[], &["apples/**/oranges"], &[]).await; filterer.dir_does_pass("/a/folder"); filterer.file_doesnt_pass("apples/carrots/oranges"); @@ -266,9 +266,9 @@ fn ignore_glob_middle_double_star() { filterer.dir_does_pass("apples/oranges/bananas"); } -#[test] -fn ignore_glob_double_star_trailing_slash() { - let filterer = filt(&[], &["apples/**/oranges/"], &[]); +#[tokio::test] +async fn ignore_glob_double_star_trailing_slash() { + let filterer = filt(&[], &["apples/**/oranges/"], &[]).await; filterer.dir_does_pass("/a/folder"); filterer.file_does_pass("apples/carrots/oranges"); @@ -284,9 +284,9 @@ fn ignore_glob_double_star_trailing_slash() { filterer.unk_does_pass("apples/carrots/cauliflowers/artichokes/oranges"); } -#[test] -fn ignores_take_precedence() { - let filterer = filt(&["*.docx", "*.toml", "*.json"], &["*.toml", "*.json"], &[]); +#[tokio::test] +async fn ignores_take_precedence() { + let filterer = filt(&["*.docx", "*.toml", "*.json"], &["*.toml", "*.json"], &[]).await; filterer.file_doesnt_pass("Cargo.toml"); filterer.file_doesnt_pass("/test/foo/bar/Cargo.toml"); @@ -299,9 +299,9 @@ fn ignores_take_precedence() { // The following tests replicate the "buggy"/"confusing" watchexec v1 behaviour. -#[test] -fn ignore_folder_incorrectly_with_bare_match() { - let filterer = filt(&[], &["prunes"], &[]); +#[tokio::test] +async fn ignore_folder_incorrectly_with_bare_match() { + let filterer = filt(&[], &["prunes"], &[]).await; filterer.file_does_pass("apples"); filterer.file_does_pass("apples/carrots/cauliflowers/oranges"); @@ -330,9 +330,9 @@ fn ignore_folder_incorrectly_with_bare_match() { filterer.dir_does_pass("prunes/carrots/cauliflowers/artichokes/oranges"); } -#[test] -fn ignore_folder_incorrectly_with_bare_and_leading_slash() { - let filterer = filt(&[], &["/prunes"], &[]); +#[tokio::test] +async fn ignore_folder_incorrectly_with_bare_and_leading_slash() { + let filterer = filt(&[], &["/prunes"], &[]).await; filterer.file_does_pass("apples"); filterer.file_does_pass("apples/carrots/cauliflowers/oranges"); @@ -361,9 +361,9 @@ fn ignore_folder_incorrectly_with_bare_and_leading_slash() { filterer.dir_does_pass("prunes/carrots/cauliflowers/artichokes/oranges"); } -#[test] -fn ignore_folder_incorrectly_with_bare_and_trailing_slash() { - let filterer = filt(&[], &["prunes/"], &[]); +#[tokio::test] +async fn ignore_folder_incorrectly_with_bare_and_trailing_slash() { + let filterer = filt(&[], &["prunes/"], &[]).await; filterer.file_does_pass("apples"); filterer.file_does_pass("apples/carrots/cauliflowers/oranges"); @@ -392,9 +392,9 @@ fn ignore_folder_incorrectly_with_bare_and_trailing_slash() { filterer.dir_does_pass("prunes/carrots/cauliflowers/artichokes/oranges"); } -#[test] -fn ignore_folder_incorrectly_with_only_double_double_glob() { - let filterer = filt(&[], &["**/prunes/**"], &[]); +#[tokio::test] +async fn ignore_folder_incorrectly_with_only_double_double_glob() { + let filterer = filt(&[], &["**/prunes/**"], &[]).await; filterer.file_does_pass("apples"); filterer.file_does_pass("apples/carrots/cauliflowers/oranges"); @@ -423,9 +423,9 @@ fn ignore_folder_incorrectly_with_only_double_double_glob() { filterer.dir_does_pass("prunes"); } -#[test] -fn ignore_folder_correctly_with_double_and_double_double_globs() { - let filterer = filt(&[], &["**/prunes", "**/prunes/**"], &[]); +#[tokio::test] +async fn ignore_folder_correctly_with_double_and_double_double_globs() { + let filterer = filt(&[], &["**/prunes", "**/prunes/**"], &[]).await; filterer.file_does_pass("apples"); filterer.file_does_pass("apples/carrots/cauliflowers/oranges"); diff --git a/lib/tests/helpers.rs b/lib/tests/helpers.rs index dd8a577..a15f1ec 100644 --- a/lib/tests/helpers.rs +++ b/lib/tests/helpers.rs @@ -213,32 +213,24 @@ fn tracing_init() { .ok(); } -pub fn globset_filt(filters: &[&str], ignores: &[&str], extensions: &[&str]) -> GlobsetFilterer { +pub async fn globset_filt(filters: &[&str], ignores: &[&str], extensions: &[&str]) -> GlobsetFilterer { let origin = dunce::canonicalize(".").unwrap(); tracing_init(); GlobsetFilterer::new( origin, filters.iter().map(|s| (s.to_string(), None)), ignores.iter().map(|s| (s.to_string(), None)), + vec![], extensions.iter().map(OsString::from), ) + .await .expect("making filterer") } pub async fn globset_igfilt(origin: &str, ignore_files: &[IgnoreFile]) -> GlobsetFilterer { tracing_init(); - let mut ignores = Vec::new(); - for file in ignore_files { - tracing::info!(?file, "loading ignore file"); - ignores.extend( - GlobsetFilterer::list_from_ignore_file(file) - .await - .expect("adding ignorefile"), - ); - } - let origin = dunce::canonicalize(".").unwrap().join(origin); - GlobsetFilterer::new(origin, vec![], ignores, vec![]).expect("making filterer") + GlobsetFilterer::new(origin, vec![], vec![], ignore_files.iter().cloned(), vec![]).await.expect("making filterer") } pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> IgnoreFilterer {