Integrate IgnoreFilterer into GlobsetFilterer

This commit is contained in:
Félix Saparelli 2022-01-16 15:18:15 +13:00
parent d6dfb87063
commit b8d9ad728e
No known key found for this signature in database
GPG Key ID: B948C4BAE44FC474
6 changed files with 136 additions and 123 deletions

View File

@ -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<Arc<WatchexecFilterer>> {
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<Arc<WatchexecFilterer
]);
}
for ignore in ignorefiles {
ignores.extend(GlobsetFilterer::list_from_ignore_file(&ignore).await?);
}
let filters = args
.values_of("filter")
.unwrap_or_default()
@ -58,9 +53,8 @@ pub async fn globset(args: &ArgMatches<'static>) -> Result<Arc<WatchexecFilterer
.map(|s| s.split(b','))
.flatten();
debug!(filters=%filters.len(), ignores=%ignores.len(), "vecs lengths");
Ok(Arc::new(WatchexecFilterer {
inner: GlobsetFilterer::new(project_origin, filters, ignores, exts).into_diagnostic()?,
inner: GlobsetFilterer::new(project_origin, filters, ignores, ignore_files, exts).await.into_diagnostic()?,
no_meta: args.is_present("no-meta"),
}))
}

View File

@ -219,12 +219,12 @@ pub enum RuntimeError {
#[diagnostic(code(watchexec::runtime::clearscreen))]
Clearscreen(#[from] clearscreen::Error),
/// Error received when parsing a glob from an [`IgnoreFile`](crate::ignore::files::IgnoreFile) fails.
#[error("cannot parse glob from ignore '{file}': {err}")]
#[diagnostic(code(watchexec::runtime::ignore_file_glob))]
IgnoreFileGlob {
/// Error received when parsing a glob (possibly from an [`IgnoreFile`](crate::ignore::files::IgnoreFile)) fails.
#[error("cannot parse glob from ignore '{file:?}': {err}")]
#[diagnostic(code(watchexec::runtime::ignore_glob))]
GlobsetGlob {
/// The path to the erroring ignore file.
file: PathBuf,
file: Option<PathBuf>,
/// The underlying error.
#[source]

View File

@ -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<OsString>,
}
@ -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<Path>,
filters: impl IntoIterator<Item = (String, Option<PathBuf>)>,
ignores: impl IntoIterator<Item = (String, Option<PathBuf>)>,
ignore_files: impl IntoIterator<Item = IgnoreFile>,
extensions: impl IntoIterator<Item = OsString>,
) -> Result<Self, ignore::Error> {
let mut filters_builder = GitignoreBuilder::new(origin);
let mut ignores_builder = filters_builder.clone();
) -> Result<Self, RuntimeError> {
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<OsString> = extensions.into_iter().collect();
let mut ignore_files = IgnoreFilterer::new(origin, &ignore_files.into_iter().collect::<Vec<_>>()).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<bool, RuntimeError> {
// 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

View File

@ -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,
}
})?;

View File

@ -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");

View File

@ -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 {