Find and load all ignores for watchexec cli

This commit is contained in:
Félix Saparelli 2021-10-14 00:38:56 +13:00
parent 87b6729ab7
commit ae6af17aea
No known key found for this signature in database
GPG key ID: B948C4BAE44FC474
5 changed files with 140 additions and 32 deletions

2
Cargo.lock generated
View file

@ -2690,11 +2690,13 @@ dependencies = [
"assert_cmd", "assert_cmd",
"clap", "clap",
"console-subscriber", "console-subscriber",
"dunce",
"embed-resource", "embed-resource",
"insta", "insta",
"miette", "miette",
"notify-rust", "notify-rust",
"tokio", "tokio",
"tracing",
"tracing-subscriber", "tracing-subscriber",
"watchexec", "watchexec",
] ]

View file

@ -20,12 +20,14 @@ name = "watchexec"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
watchexec = { path = "../lib" }
miette = { version = "3.2.0", features = ["fancy"] }
console-subscriber = { git = "https://github.com/tokio-rs/console", optional = true } console-subscriber = { git = "https://github.com/tokio-rs/console", optional = true }
dunce = "1.0.2"
miette = { version = "3.2.0", features = ["fancy"] }
notify-rust = "4.5.2" notify-rust = "4.5.2"
tokio = { version = "1.10.0", features = ["full"] } tokio = { version = "1.10.0", features = ["full"] }
tracing = "0.1.26"
tracing-subscriber = "0.2.24" tracing-subscriber = "0.2.24"
watchexec = { path = "../lib" }
[dependencies.clap] [dependencies.clap]
version = "2.33.3" version = "2.33.3"

View file

@ -41,11 +41,16 @@ fn runtime(args: &ArgMatches<'static>) -> Result<(RuntimeConfig, Arc<TaggedFilte
}); });
config.action_throttle(Duration::from_millis( config.action_throttle(Duration::from_millis(
args.value_of("debounce").unwrap_or("100").parse().into_diagnostic()?, args.value_of("debounce")
.unwrap_or("100")
.parse()
.into_diagnostic()?,
)); ));
if let Some(interval) = args.value_of("poll") { if let Some(interval) = args.value_of("poll") {
config.file_watcher(Watcher::Poll(Duration::from_millis(interval.parse().into_diagnostic()?))); config.file_watcher(Watcher::Poll(Duration::from_millis(
interval.parse().into_diagnostic()?,
)));
} }
config.command_shell(if args.is_present("no-shell") { config.command_shell(if args.is_present("no-shell") {
@ -81,7 +86,8 @@ fn runtime(args: &ArgMatches<'static>) -> Result<(RuntimeConfig, Arc<TaggedFilte
let mut signal = args let mut signal = args
.value_of("signal") .value_of("signal")
.map(|s| Signal::from_str(s)) .map(|s| Signal::from_str(s))
.transpose().into_diagnostic()? .transpose()
.into_diagnostic()?
.unwrap_or(Signal::SIGTERM); .unwrap_or(Signal::SIGTERM);
if args.is_present("kill") { if args.is_present("kill") {

View file

@ -1,7 +1,15 @@
use std::{env::var}; use std::{collections::HashSet, env::var, path::PathBuf};
use dunce::canonicalize;
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use watchexec::{Watchexec, event::Event, filter::tagged::{Filter, Matcher, Op, TaggedFilterer}}; use tracing::{debug, warn};
use watchexec::{
event::Event,
filter::tagged::{Filter, Matcher, Op, Pattern, Regex},
ignore_files::{self, IgnoreFile},
project::{self, ProjectType},
Watchexec,
};
mod args; mod args;
mod config; mod config;
@ -17,16 +25,93 @@ async fn main() -> Result<()> {
let args = args::get_args()?; let args = args::get_args()?;
if args.is_present("verbose") { tracing_subscriber::fmt()
tracing_subscriber::fmt() .with_env_filter(match args.occurrences_of("verbose") {
.with_env_filter(match args.occurrences_of("verbose") { 0 => "watchexec-cli=warn",
0 => unreachable!(), 1 => "watchexec=debug,watchexec-cli=debug",
1 => "watchexec=debug", 2 => "watchexec=trace,watchexec-cli=trace",
2 => "watchexec=trace", _ => "trace",
_ => "trace", })
.try_init()
.ok();
let mut origins = HashSet::new();
for path in args.values_of("paths").unwrap_or_default().into_iter() {
let path = canonicalize(path).into_diagnostic()?;
origins.extend(project::origins(&path).await);
}
debug!(?origins, "resolved all project origins");
let project_origin = project::common_prefix(&origins).unwrap_or_else(|| PathBuf::from("."));
debug!(?project_origin, "resolved common/project origin");
let vcs_types = project::types(&project_origin)
.await
.into_iter()
.filter(|pt| pt.is_vcs())
.collect::<Vec<_>>();
debug!(?vcs_types, "resolved vcs types");
let (mut ignores, _errors) = ignore_files::from_origin(&project_origin).await;
// TODO: handle errors
debug!(?ignores, "discovered ignore files from project origin");
let mut skip_git_global_excludes = false;
if !vcs_types.is_empty() {
ignores = ignores
.into_iter()
.filter(|ig| match ig.applies_to {
Some(pt) if pt.is_vcs() => vcs_types.contains(&pt),
_ => true,
}) })
.try_init() .inspect(|ig| {
.ok(); if let IgnoreFile {
applies_to: Some(ProjectType::Git),
applies_in: None,
..
} = ig
{
warn!("project git config overrides the global excludes");
skip_git_global_excludes = true;
}
})
.collect::<Vec<_>>();
debug!(?ignores, "filtered ignores to only those for project vcs");
// TODO: use drain_ignore when that stabilises
}
let (mut global_ignores, _errors) = ignore_files::from_environment().await;
// TODO: handle errors
debug!(?global_ignores, "discovered ignore files from environment");
if skip_git_global_excludes {
global_ignores = global_ignores
.into_iter()
.filter(|gig| {
!matches!(
gig,
IgnoreFile {
applies_to: Some(ProjectType::Git),
applies_in: None,
..
}
)
})
.collect::<Vec<_>>();
debug!(
?global_ignores,
"filtered global ignores to exclude global git ignores"
);
// TODO: use drain_ignore when that stabilises
}
if !vcs_types.is_empty() {
ignores.extend(global_ignores.into_iter().filter(|ig| match ig.applies_to {
Some(pt) if pt.is_vcs() => vcs_types.contains(&pt),
_ => true,
}));
debug!(?ignores, "combined and applied final filter over ignores");
} }
let mut filters = Vec::new(); let mut filters = Vec::new();
@ -36,12 +121,17 @@ async fn main() -> Result<()> {
filters.push(filter.parse()?); filters.push(filter.parse()?);
} }
for ext in args.values_of("extensions").unwrap_or_default().map(|s| s.split(',').map(|s| s.trim())).flatten() { for ext in args
.values_of("extensions")
.unwrap_or_default()
.map(|s| s.split(',').map(|s| s.trim()))
.flatten()
{
filters.push(Filter { filters.push(Filter {
in_path: None, in_path: None,
on: Matcher::Path, on: Matcher::Path,
op: Op::Glob, op: Op::Regex,
pat: TaggedFilterer::glob(&format!("**/*.{}", ext))?, pat: Pattern::Regex(Regex::new(&format!("[.]{}$", ext)).into_diagnostic()?),
negate: false, negate: false,
}); });
} }
@ -49,6 +139,10 @@ async fn main() -> Result<()> {
let (init, runtime, filterer) = config::new(&args)?; let (init, runtime, filterer) = config::new(&args)?;
filterer.add_filters(&filters).await?; filterer.add_filters(&filters).await?;
for ignore in &ignores {
filterer.add_ignore_file(ignore).await?;
}
let wx = Watchexec::new(init, runtime)?; let wx = Watchexec::new(init, runtime)?;
if !args.is_present("postpone") { if !args.is_present("postpone") {

View file

@ -74,8 +74,8 @@ impl ProjectType {
/// present and indicative of the root or origin path of a project. It's entirely possible to have /// present and indicative of the root or origin path of a project. It's entirely possible to have
/// multiple such origins show up: for example, a member of a Cargo workspace will list both the /// multiple such origins show up: for example, a member of a Cargo workspace will list both the
/// member project and the workspace root as origins. /// member project and the workspace root as origins.
pub async fn origins(path: impl AsRef<Path>) -> Vec<PathBuf> { pub async fn origins(path: impl AsRef<Path>) -> HashSet<PathBuf> {
let mut origins = Vec::new(); let mut origins = HashSet::new();
fn check_list(list: DirList) -> bool { fn check_list(list: DirList) -> bool {
if list.is_empty() { if list.is_empty() {
@ -134,13 +134,13 @@ pub async fn origins(path: impl AsRef<Path>) -> Vec<PathBuf> {
let mut current = path.as_ref(); let mut current = path.as_ref();
if check_list(DirList::obtain(current).await) { if check_list(DirList::obtain(current).await) {
origins.push(current.to_owned()); origins.insert(current.to_owned());
} }
while let Some(parent) = current.parent() { while let Some(parent) = current.parent() {
current = parent; current = parent;
if check_list(DirList::obtain(current).await) { if check_list(DirList::obtain(current).await) {
origins.push(current.to_owned()); origins.insert(current.to_owned());
continue; continue;
} }
} }
@ -195,18 +195,22 @@ pub async fn types(path: impl AsRef<Path>) -> HashSet<ProjectType> {
/// This is a utility function which is useful for finding the common root of a set of origins. /// This is a utility function which is useful for finding the common root of a set of origins.
/// ///
/// Returns `None` if zero paths are given or paths share no common prefix. /// Returns `None` if zero paths are given or paths share no common prefix.
pub fn common_prefix(paths: &[PathBuf]) -> Option<PathBuf> { pub fn common_prefix<I, P>(paths: I) -> Option<PathBuf>
match paths.len() { where
0 => return None, I: IntoIterator<Item = P>,
1 => return Some(paths[0].to_owned()), P: AsRef<Path>,
_ => {} {
let mut paths = paths.into_iter();
let first_path = paths.next().map(|p| p.as_ref().to_owned());
let mut longest_path = if let Some(ref p) = first_path {
p.components().collect::<Vec<_>>()
} else {
return None;
}; };
let mut longest_path: Vec<_> = paths[0].components().collect(); for path in paths {
for path in &paths[1..] {
let mut greatest_distance = 0; let mut greatest_distance = 0;
for component_pair in path.components().zip(longest_path.iter()) { for component_pair in path.as_ref().components().zip(longest_path.iter()) {
if component_pair.0 != *component_pair.1 { if component_pair.0 != *component_pair.1 {
break; break;
} }