2024-04-20 12:06:53 +02:00
|
|
|
use std::{
|
|
|
|
ffi::OsString,
|
|
|
|
path::{Path, PathBuf, MAIN_SEPARATOR},
|
|
|
|
sync::Arc,
|
|
|
|
};
|
2021-12-23 15:37:51 +01:00
|
|
|
|
2024-04-20 12:06:53 +02:00
|
|
|
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(crate) mod parse;
|
|
|
|
mod proglib;
|
|
|
|
mod progs;
|
|
|
|
mod syncval;
|
|
|
|
|
|
|
|
/// 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<FsEvent>,
|
|
|
|
progs: Option<progs::FilterProgs>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Filterer for WatchexecFilterer {
|
|
|
|
#[tracing::instrument(level = "trace", skip(self))]
|
|
|
|
fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> {
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
|
|
|
trace!(allowed=?self.fs_events, this=?normalised, "check against fs event filter");
|
|
|
|
if !self.fs_events.contains(&normalised) {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trace!("check against original event");
|
|
|
|
if !self.inner.check_event(event, priority)? {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(progs) = &self.progs {
|
|
|
|
trace!("check against program filters");
|
|
|
|
if !progs.check(event)? {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl WatchexecFilterer {
|
|
|
|
/// Create a new filterer from the given arguments
|
|
|
|
pub async fn new(args: &Args) -> Result<Arc<Self>> {
|
2024-04-28 08:33:07 +02:00
|
|
|
let project_origin = args.project_origin.clone().unwrap();
|
|
|
|
let workdir = args.workdir.clone().unwrap();
|
2024-04-20 12:06:53 +02:00
|
|
|
|
|
|
|
let ignore_files = if args.no_discover_ignore {
|
|
|
|
Vec::new()
|
|
|
|
} else {
|
2024-04-28 08:33:07 +02:00
|
|
|
let vcs_types = crate::dirs::vcs_types(&project_origin).await;
|
|
|
|
crate::dirs::ignores(args, &vcs_types).await?
|
2024-04-20 12:06:53 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
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::<Vec<_>>();
|
|
|
|
|
|
|
|
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(),
|
|
|
|
progs: if args.filter_programs_parsed.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(progs::FilterProgs::new(args)?)
|
|
|
|
},
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn read_filter_file(path: &Path) -> Result<Vec<(String, Option<PathBuf>)>> {
|
|
|
|
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)
|
|
|
|
}
|