diff --git a/crates/cli/src/filterer.rs b/crates/cli/src/filterer.rs index 0bb3f1a3..688a5886 100644 --- a/crates/cli/src/filterer.rs +++ b/crates/cli/src/filterer.rs @@ -1,4 +1,144 @@ -mod common; -mod globset; +use std::{ + ffi::OsString, + path::{Path, PathBuf, MAIN_SEPARATOR}, + sync::Arc, +}; -pub use globset::globset; +use miette::{IntoDiagnostic, Result}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tracing::{info, trace, trace_span}; +use watchexec::{ + error::RuntimeError, + event::{ + filekind::{FileEventKind, ModifyKind}, + Event, Priority, Tag, + }, + filter::Filterer, +}; +use watchexec_filterer_globset::GlobsetFilterer; + +use crate::args::{Args, FsEvent}; + +mod dirs; + +/// A custom filterer that combines the library's Globset filterer and a switch for --no-meta +#[derive(Debug)] +pub struct WatchexecFilterer { + inner: GlobsetFilterer, + fs_events: Vec, +} + +impl Filterer for WatchexecFilterer { + fn check_event(&self, event: &Event, priority: Priority) -> Result { + for tag in &event.tags { + if let Tag::FileEventKind(fek) = tag { + let normalised = match fek { + FileEventKind::Access(_) => FsEvent::Access, + FileEventKind::Modify(ModifyKind::Name(_)) => FsEvent::Rename, + FileEventKind::Modify(ModifyKind::Metadata(_)) => FsEvent::Metadata, + FileEventKind::Modify(_) => FsEvent::Modify, + FileEventKind::Create(_) => FsEvent::Create, + FileEventKind::Remove(_) => FsEvent::Remove, + _ => continue, + }; + + if !self.fs_events.contains(&normalised) { + return Ok(false); + } + } + } + + trace!("check against original event"); + if !self.inner.check_event(event, priority)? { + return Ok(false); + } + + Ok(true) + } +} + +impl WatchexecFilterer { + /// Create a new filterer from the given arguments + pub async fn new(args: &Args) -> Result> { + let (project_origin, workdir) = dirs::dirs(args).await?; + let vcs_types = dirs::vcs_types(&project_origin).await; + let ignore_files = dirs::ignores(args, &vcs_types, &project_origin).await; + + let mut ignores = Vec::new(); + + if !args.no_default_ignore { + ignores.extend([ + (format!("**{MAIN_SEPARATOR}.DS_Store"), None), + (String::from("watchexec.*.log"), None), + (String::from("*.py[co]"), None), + (String::from("#*#"), None), + (String::from(".#*"), None), + (String::from(".*.kate-swp"), None), + (String::from(".*.sw?"), None), + (String::from(".*.sw?x"), None), + (format!("**{MAIN_SEPARATOR}.bzr{MAIN_SEPARATOR}**"), None), + (format!("**{MAIN_SEPARATOR}_darcs{MAIN_SEPARATOR}**"), None), + ( + format!("**{MAIN_SEPARATOR}.fossil-settings{MAIN_SEPARATOR}**"), + None, + ), + (format!("**{MAIN_SEPARATOR}.git{MAIN_SEPARATOR}**"), None), + (format!("**{MAIN_SEPARATOR}.hg{MAIN_SEPARATOR}**"), None), + (format!("**{MAIN_SEPARATOR}.pijul{MAIN_SEPARATOR}**"), None), + (format!("**{MAIN_SEPARATOR}.svn{MAIN_SEPARATOR}**"), None), + ]); + } + + let mut filters = args + .filter_patterns + .iter() + .map(|f| (f.to_owned(), Some(workdir.clone()))) + .collect::>(); + + for filter_file in &args.filter_files { + filters.extend(read_filter_file(filter_file).await?); + } + + ignores.extend( + args.ignore_patterns + .iter() + .map(|f| (f.to_owned(), Some(workdir.clone()))), + ); + + let exts = args + .filter_extensions + .iter() + .map(|e| OsString::from(e.strip_prefix('.').unwrap_or(e))); + + info!("initialising Globset filterer"); + Ok(Arc::new(Self { + inner: GlobsetFilterer::new(project_origin, filters, ignores, ignore_files, exts) + .await + .into_diagnostic()?, + fs_events: args.filter_fs_events.clone(), + })) + } +} + +async fn read_filter_file(path: &Path) -> Result)>> { + let _span = trace_span!("loading filter file", ?path).entered(); + + let file = tokio::fs::File::open(path).await.into_diagnostic()?; + + let mut filters = + Vec::with_capacity(file.metadata().await.map(|m| m.len() as usize).unwrap_or(0) / 20); + + let reader = BufReader::new(file); + let mut lines = reader.lines(); + while let Some(line) = lines.next_line().await.into_diagnostic()? { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + trace!(?line, "adding filter line"); + filters.push((line.to_owned(), Some(path.to_owned()))); + } + + Ok(filters) +} diff --git a/crates/cli/src/filterer/common.rs b/crates/cli/src/filterer/dirs.rs similarity index 100% rename from crates/cli/src/filterer/common.rs rename to crates/cli/src/filterer/dirs.rs diff --git a/crates/cli/src/filterer/globset.rs b/crates/cli/src/filterer/globset.rs deleted file mode 100644 index d235d58b..00000000 --- a/crates/cli/src/filterer/globset.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::{ - ffi::OsString, - path::{Path, PathBuf, MAIN_SEPARATOR}, - sync::Arc, -}; - -use miette::{IntoDiagnostic, Result}; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tracing::{info, trace, trace_span}; -use watchexec::{error::RuntimeError, filter::Filterer}; -use watchexec_events::{ - filekind::{FileEventKind, ModifyKind}, - Event, Priority, Tag, -}; -use watchexec_filterer_globset::GlobsetFilterer; - -use crate::args::{Args, FsEvent}; - -pub async fn globset(args: &Args) -> Result> { - let (project_origin, workdir) = super::common::dirs(args).await?; - - let ignore_files = if args.no_discover_ignore { - Vec::new() - } else { - let vcs_types = super::common::vcs_types(&project_origin).await; - super::common::ignores(args, &vcs_types, &project_origin).await? - }; - - let mut ignores = Vec::new(); - - if !args.no_default_ignore { - ignores.extend([ - (format!("**{MAIN_SEPARATOR}.DS_Store"), None), - (String::from("watchexec.*.log"), None), - (String::from("*.py[co]"), None), - (String::from("#*#"), None), - (String::from(".#*"), None), - (String::from(".*.kate-swp"), None), - (String::from(".*.sw?"), None), - (String::from(".*.sw?x"), None), - (format!("**{MAIN_SEPARATOR}.bzr{MAIN_SEPARATOR}**"), None), - (format!("**{MAIN_SEPARATOR}_darcs{MAIN_SEPARATOR}**"), None), - ( - format!("**{MAIN_SEPARATOR}.fossil-settings{MAIN_SEPARATOR}**"), - None, - ), - (format!("**{MAIN_SEPARATOR}.git{MAIN_SEPARATOR}**"), None), - (format!("**{MAIN_SEPARATOR}.hg{MAIN_SEPARATOR}**"), None), - (format!("**{MAIN_SEPARATOR}.pijul{MAIN_SEPARATOR}**"), None), - (format!("**{MAIN_SEPARATOR}.svn{MAIN_SEPARATOR}**"), None), - ]); - } - - let mut filters = args - .filter_patterns - .iter() - .map(|f| (f.to_owned(), Some(workdir.clone()))) - .collect::>(); - - for filter_file in &args.filter_files { - filters.extend(read_filter_file(filter_file).await?); - } - - ignores.extend( - args.ignore_patterns - .iter() - .map(|f| (f.to_owned(), Some(workdir.clone()))), - ); - - let exts = args - .filter_extensions - .iter() - .map(|e| OsString::from(e.strip_prefix('.').unwrap_or(e))); - - info!("initialising Globset filterer"); - Ok(Arc::new(WatchexecFilterer { - inner: GlobsetFilterer::new(project_origin, filters, ignores, ignore_files, exts) - .await - .into_diagnostic()?, - fs_events: args.filter_fs_events.clone(), - })) -} - -async fn read_filter_file(path: &Path) -> Result)>> { - let _span = trace_span!("loading filter file", ?path).entered(); - - let file = tokio::fs::File::open(path).await.into_diagnostic()?; - - let metadata_len = file - .metadata() - .await - .map(|m| usize::try_from(m.len())) - .unwrap_or(Ok(0)) - .into_diagnostic()?; - let filter_capacity = if metadata_len == 0 { - 0 - } else { - metadata_len / 20 - }; - let mut filters = Vec::with_capacity(filter_capacity); - - let reader = BufReader::new(file); - let mut lines = reader.lines(); - while let Some(line) = lines.next_line().await.into_diagnostic()? { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - - trace!(?line, "adding filter line"); - filters.push((line.to_owned(), Some(path.to_owned()))); - } - - Ok(filters) -} - -/// A custom filterer that combines the library's Globset filterer and a switch for --no-meta -#[derive(Debug)] -pub struct WatchexecFilterer { - inner: GlobsetFilterer, - fs_events: Vec, -} - -impl Filterer for WatchexecFilterer { - fn check_event(&self, event: &Event, priority: Priority) -> Result { - for tag in &event.tags { - if let Tag::FileEventKind(fek) = tag { - let normalised = match fek { - FileEventKind::Access(_) => FsEvent::Access, - FileEventKind::Modify(ModifyKind::Name(_)) => FsEvent::Rename, - FileEventKind::Modify(ModifyKind::Metadata(_)) => FsEvent::Metadata, - FileEventKind::Modify(_) => FsEvent::Modify, - FileEventKind::Create(_) => FsEvent::Create, - FileEventKind::Remove(_) => FsEvent::Remove, - _ => continue, - }; - - if !self.fs_events.contains(&normalised) { - return Ok(false); - } - } - } - - trace!("check against original event"); - if !self.inner.check_event(event, priority)? { - return Ok(false); - } - - Ok(true) - } -} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index beca2109..861bb2e6 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -14,6 +14,8 @@ use tracing::{debug, info, warn}; use watchexec::Watchexec; use watchexec_events::{Event, Priority}; +use crate::filterer::WatchexecFilterer; + pub mod args; mod config; mod emits; @@ -100,8 +102,8 @@ async fn run_watchexec(args: Args) -> Result<()> { info!(version=%env!("CARGO_PKG_VERSION"), "constructing Watchexec from CLI"); let state = state::State::new()?; - let config = config::make_config(&args, &state)?; - config.filterer(filterer::globset(&args).await?); + let mut config = config::make_config(&args, &state)?; + config.filterer(WatchexecFilterer::new(&args).await?); info!("initialising Watchexec runtime"); let wx = Watchexec::with_config(config)?;