Split tagged and globset filterer in cli
This commit is contained in:
parent
3bfbcaaa2f
commit
ec49185488
|
@ -1,219 +1,5 @@
|
|||
use std::{
|
||||
convert::Infallible, env::current_dir, io::stderr, path::Path, str::FromStr, sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
mod init;
|
||||
mod runtime;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use watchexec::{
|
||||
action::{Action, Outcome},
|
||||
command::Shell,
|
||||
config::{InitConfig, RuntimeConfig},
|
||||
event::ProcessEnd,
|
||||
filter::tagged::TaggedFilterer,
|
||||
fs::Watcher,
|
||||
handler::PrintDisplay,
|
||||
signal::{process::SubSignal, source::MainSignal},
|
||||
};
|
||||
|
||||
pub fn new(args: &ArgMatches<'static>) -> Result<(InitConfig, RuntimeConfig, Arc<TaggedFilterer>)> {
|
||||
let r = runtime(args)?;
|
||||
Ok((init(args)?, r.0, r.1))
|
||||
}
|
||||
|
||||
fn init(_args: &ArgMatches<'static>) -> Result<InitConfig> {
|
||||
let mut config = InitConfig::default();
|
||||
config.on_error(PrintDisplay(stderr()));
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn runtime(args: &ArgMatches<'static>) -> Result<(RuntimeConfig, Arc<TaggedFilterer>)> {
|
||||
let mut config = RuntimeConfig::default();
|
||||
|
||||
config.command(
|
||||
args.values_of_lossy("command")
|
||||
.expect("(clap) Bug: command is not present")
|
||||
.iter(),
|
||||
);
|
||||
|
||||
config.pathset(match args.values_of_os("paths") {
|
||||
Some(paths) => paths.map(|os| Path::new(os).to_owned()).collect(),
|
||||
None => vec![current_dir().into_diagnostic()?],
|
||||
});
|
||||
|
||||
config.action_throttle(Duration::from_millis(
|
||||
args.value_of("debounce")
|
||||
.unwrap_or("100")
|
||||
.parse()
|
||||
.into_diagnostic()?,
|
||||
));
|
||||
|
||||
if let Some(interval) = args.value_of("poll") {
|
||||
config.file_watcher(Watcher::Poll(Duration::from_millis(
|
||||
interval.parse().into_diagnostic()?,
|
||||
)));
|
||||
}
|
||||
|
||||
config.command_shell(if args.is_present("no-shell") {
|
||||
Shell::None
|
||||
} else if let Some(s) = args.value_of("shell") {
|
||||
if s.eq_ignore_ascii_case("powershell") {
|
||||
Shell::Powershell
|
||||
} else if s.eq_ignore_ascii_case("none") {
|
||||
Shell::None
|
||||
} else if s.eq_ignore_ascii_case("cmd") {
|
||||
cmd_shell(s.into())
|
||||
} else {
|
||||
Shell::Unix(s.into())
|
||||
}
|
||||
} else {
|
||||
default_shell()
|
||||
});
|
||||
|
||||
let clear = args.is_present("clear");
|
||||
let mut on_busy = args
|
||||
.value_of("on-busy-update")
|
||||
.unwrap_or("queue")
|
||||
.to_owned();
|
||||
|
||||
if args.is_present("restart") {
|
||||
on_busy = "restart".into();
|
||||
}
|
||||
|
||||
if args.is_present("watch-when-idle") {
|
||||
on_busy = "do-nothing".into();
|
||||
}
|
||||
|
||||
let mut signal = args
|
||||
.value_of("signal")
|
||||
.map(SubSignal::from_str)
|
||||
.transpose()
|
||||
.into_diagnostic()?
|
||||
.unwrap_or(SubSignal::Terminate);
|
||||
|
||||
if args.is_present("kill") {
|
||||
signal = SubSignal::ForceStop;
|
||||
}
|
||||
|
||||
let print_events = args.is_present("print-events");
|
||||
let once = args.is_present("once");
|
||||
|
||||
let filterer = TaggedFilterer::new(".", ".")?;
|
||||
config.filterer(filterer.clone());
|
||||
|
||||
config.on_action(move |action: Action| {
|
||||
let fut = async { Ok::<(), Infallible>(()) };
|
||||
|
||||
if print_events {
|
||||
for (n, event) in action.events.iter().enumerate() {
|
||||
eprintln!("[EVENT {}] {}", n, event);
|
||||
}
|
||||
}
|
||||
|
||||
if once {
|
||||
action.outcome(Outcome::both(Outcome::Start, Outcome::wait(Outcome::Exit)));
|
||||
return fut;
|
||||
}
|
||||
|
||||
let signals: Vec<MainSignal> = action.events.iter().flat_map(|e| e.signals()).collect();
|
||||
let has_paths = action
|
||||
.events
|
||||
.iter()
|
||||
.flat_map(|e| e.paths())
|
||||
.next()
|
||||
.is_some();
|
||||
|
||||
if signals.contains(&MainSignal::Terminate) {
|
||||
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
|
||||
return fut;
|
||||
}
|
||||
|
||||
if signals.contains(&MainSignal::Interrupt) {
|
||||
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
|
||||
return fut;
|
||||
}
|
||||
|
||||
if !has_paths {
|
||||
if !signals.is_empty() {
|
||||
let mut out = Outcome::DoNothing;
|
||||
for sig in signals {
|
||||
out = Outcome::both(out, Outcome::Signal(sig.into()));
|
||||
}
|
||||
|
||||
action.outcome(out);
|
||||
return fut;
|
||||
}
|
||||
|
||||
let completion = action.events.iter().flat_map(|e| e.completions()).next();
|
||||
if let Some(status) = completion {
|
||||
match status {
|
||||
Some(ProcessEnd::ExitError(code)) => {
|
||||
eprintln!("[Command exited with {}]", code);
|
||||
}
|
||||
Some(ProcessEnd::ExitSignal(sig)) => {
|
||||
eprintln!("[Command killed by {:?}]", sig);
|
||||
}
|
||||
Some(ProcessEnd::ExitStop(sig)) => {
|
||||
eprintln!("[Command stopped by {:?}]", sig);
|
||||
}
|
||||
Some(ProcessEnd::Continued) => {
|
||||
eprintln!("[Command continued]");
|
||||
}
|
||||
Some(ProcessEnd::Exception(ex)) => {
|
||||
eprintln!("[Command ended by exception {:#x}]", ex);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
action.outcome(Outcome::DoNothing);
|
||||
return fut;
|
||||
}
|
||||
}
|
||||
|
||||
let when_running = match (clear, on_busy.as_str()) {
|
||||
(_, "do-nothing") => Outcome::DoNothing,
|
||||
(true, "restart") => {
|
||||
Outcome::both(Outcome::Stop, Outcome::both(Outcome::Clear, Outcome::Start))
|
||||
}
|
||||
(false, "restart") => Outcome::both(Outcome::Stop, Outcome::Start),
|
||||
(_, "signal") => Outcome::Signal(signal),
|
||||
(true, "queue") => Outcome::wait(Outcome::both(Outcome::Clear, Outcome::Start)),
|
||||
(false, "queue") => Outcome::wait(Outcome::Start),
|
||||
_ => Outcome::DoNothing,
|
||||
};
|
||||
|
||||
let when_idle = if clear {
|
||||
Outcome::both(Outcome::Clear, Outcome::Start)
|
||||
} else {
|
||||
Outcome::Start
|
||||
};
|
||||
|
||||
action.outcome(Outcome::if_running(when_running, when_idle));
|
||||
|
||||
fut
|
||||
});
|
||||
|
||||
Ok((config, filterer))
|
||||
}
|
||||
|
||||
// until 2.0, then Powershell
|
||||
#[cfg(windows)]
|
||||
fn default_shell() -> Shell {
|
||||
Shell::Cmd
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn default_shell() -> Shell {
|
||||
Shell::default()
|
||||
}
|
||||
|
||||
// because Shell::Cmd is only on windows
|
||||
#[cfg(windows)]
|
||||
fn cmd_shell(_: String) -> Shell {
|
||||
Shell::Cmd
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn cmd_shell(s: String) -> Shell {
|
||||
Shell::Unix(s)
|
||||
}
|
||||
pub use init::init;
|
||||
pub use runtime::runtime;
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
use std::io::stderr;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use miette::Result;
|
||||
use watchexec::{config::InitConfig, handler::PrintDisplay};
|
||||
|
||||
pub fn init(_args: &ArgMatches<'static>) -> Result<InitConfig> {
|
||||
let mut config = InitConfig::default();
|
||||
config.on_error(PrintDisplay(stderr()));
|
||||
Ok(config)
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
use std::{convert::Infallible, env::current_dir, path::Path, str::FromStr, time::Duration};
|
||||
|
||||
use clap::ArgMatches;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use watchexec::{
|
||||
action::{Action, Outcome},
|
||||
command::Shell,
|
||||
config::RuntimeConfig,
|
||||
event::ProcessEnd,
|
||||
fs::Watcher,
|
||||
signal::{process::SubSignal, source::MainSignal},
|
||||
};
|
||||
|
||||
pub fn runtime(args: &ArgMatches<'static>) -> Result<RuntimeConfig> {
|
||||
let mut config = RuntimeConfig::default();
|
||||
|
||||
config.command(
|
||||
args.values_of_lossy("command")
|
||||
.expect("(clap) Bug: command is not present")
|
||||
.iter(),
|
||||
);
|
||||
|
||||
config.pathset(match args.values_of_os("paths") {
|
||||
Some(paths) => paths.map(|os| Path::new(os).to_owned()).collect(),
|
||||
None => vec![current_dir().into_diagnostic()?],
|
||||
});
|
||||
|
||||
config.action_throttle(Duration::from_millis(
|
||||
args.value_of("debounce")
|
||||
.unwrap_or("100")
|
||||
.parse()
|
||||
.into_diagnostic()?,
|
||||
));
|
||||
|
||||
if let Some(interval) = args.value_of("poll") {
|
||||
config.file_watcher(Watcher::Poll(Duration::from_millis(
|
||||
interval.parse().into_diagnostic()?,
|
||||
)));
|
||||
}
|
||||
|
||||
config.command_shell(if args.is_present("no-shell") {
|
||||
Shell::None
|
||||
} else if let Some(s) = args.value_of("shell") {
|
||||
if s.eq_ignore_ascii_case("powershell") {
|
||||
Shell::Powershell
|
||||
} else if s.eq_ignore_ascii_case("none") {
|
||||
Shell::None
|
||||
} else if s.eq_ignore_ascii_case("cmd") {
|
||||
cmd_shell(s.into())
|
||||
} else {
|
||||
Shell::Unix(s.into())
|
||||
}
|
||||
} else {
|
||||
default_shell()
|
||||
});
|
||||
|
||||
let clear = args.is_present("clear");
|
||||
let mut on_busy = args
|
||||
.value_of("on-busy-update")
|
||||
.unwrap_or("queue")
|
||||
.to_owned();
|
||||
|
||||
if args.is_present("restart") {
|
||||
on_busy = "restart".into();
|
||||
}
|
||||
|
||||
if args.is_present("watch-when-idle") {
|
||||
on_busy = "do-nothing".into();
|
||||
}
|
||||
|
||||
let mut signal = args
|
||||
.value_of("signal")
|
||||
.map(SubSignal::from_str)
|
||||
.transpose()
|
||||
.into_diagnostic()?
|
||||
.unwrap_or(SubSignal::Terminate);
|
||||
|
||||
if args.is_present("kill") {
|
||||
signal = SubSignal::ForceStop;
|
||||
}
|
||||
|
||||
let print_events = args.is_present("print-events");
|
||||
let once = args.is_present("once");
|
||||
|
||||
config.on_action(move |action: Action| {
|
||||
let fut = async { Ok::<(), Infallible>(()) };
|
||||
|
||||
if print_events {
|
||||
for (n, event) in action.events.iter().enumerate() {
|
||||
eprintln!("[EVENT {}] {}", n, event);
|
||||
}
|
||||
}
|
||||
|
||||
if once {
|
||||
action.outcome(Outcome::both(Outcome::Start, Outcome::wait(Outcome::Exit)));
|
||||
return fut;
|
||||
}
|
||||
|
||||
let signals: Vec<MainSignal> = action.events.iter().flat_map(|e| e.signals()).collect();
|
||||
let has_paths = action
|
||||
.events
|
||||
.iter()
|
||||
.flat_map(|e| e.paths())
|
||||
.next()
|
||||
.is_some();
|
||||
|
||||
if signals.contains(&MainSignal::Terminate) {
|
||||
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
|
||||
return fut;
|
||||
}
|
||||
|
||||
if signals.contains(&MainSignal::Interrupt) {
|
||||
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
|
||||
return fut;
|
||||
}
|
||||
|
||||
if !has_paths {
|
||||
if !signals.is_empty() {
|
||||
let mut out = Outcome::DoNothing;
|
||||
for sig in signals {
|
||||
out = Outcome::both(out, Outcome::Signal(sig.into()));
|
||||
}
|
||||
|
||||
action.outcome(out);
|
||||
return fut;
|
||||
}
|
||||
|
||||
let completion = action.events.iter().flat_map(|e| e.completions()).next();
|
||||
if let Some(status) = completion {
|
||||
match status {
|
||||
Some(ProcessEnd::ExitError(code)) => {
|
||||
eprintln!("[Command exited with {}]", code);
|
||||
}
|
||||
Some(ProcessEnd::ExitSignal(sig)) => {
|
||||
eprintln!("[Command killed by {:?}]", sig);
|
||||
}
|
||||
Some(ProcessEnd::ExitStop(sig)) => {
|
||||
eprintln!("[Command stopped by {:?}]", sig);
|
||||
}
|
||||
Some(ProcessEnd::Continued) => {
|
||||
eprintln!("[Command continued]");
|
||||
}
|
||||
Some(ProcessEnd::Exception(ex)) => {
|
||||
eprintln!("[Command ended by exception {:#x}]", ex);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
action.outcome(Outcome::DoNothing);
|
||||
return fut;
|
||||
}
|
||||
}
|
||||
|
||||
let when_running = match (clear, on_busy.as_str()) {
|
||||
(_, "do-nothing") => Outcome::DoNothing,
|
||||
(true, "restart") => {
|
||||
Outcome::both(Outcome::Stop, Outcome::both(Outcome::Clear, Outcome::Start))
|
||||
}
|
||||
(false, "restart") => Outcome::both(Outcome::Stop, Outcome::Start),
|
||||
(_, "signal") => Outcome::Signal(signal),
|
||||
(true, "queue") => Outcome::wait(Outcome::both(Outcome::Clear, Outcome::Start)),
|
||||
(false, "queue") => Outcome::wait(Outcome::Start),
|
||||
_ => Outcome::DoNothing,
|
||||
};
|
||||
|
||||
let when_idle = if clear {
|
||||
Outcome::both(Outcome::Clear, Outcome::Start)
|
||||
} else {
|
||||
Outcome::Start
|
||||
};
|
||||
|
||||
action.outcome(Outcome::if_running(when_running, when_idle));
|
||||
|
||||
fut
|
||||
});
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// until 2.0, then Powershell
|
||||
#[cfg(windows)]
|
||||
fn default_shell() -> Shell {
|
||||
Shell::Cmd
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn default_shell() -> Shell {
|
||||
Shell::default()
|
||||
}
|
||||
|
||||
// because Shell::Cmd is only on windows
|
||||
#[cfg(windows)]
|
||||
fn cmd_shell(_: String) -> Shell {
|
||||
Shell::Cmd
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn cmd_shell(s: String) -> Shell {
|
||||
Shell::Unix(s)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
mod common;
|
||||
mod globset;
|
||||
mod tagged;
|
||||
|
||||
pub use globset::globset;
|
||||
pub use tagged::tagged;
|
|
@ -0,0 +1,109 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::ArgMatches;
|
||||
use dunce::canonicalize;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use tracing::{debug, warn};
|
||||
use watchexec::{
|
||||
filter::tagged::{Filter, Matcher, Op, Pattern, Regex, TaggedFilterer},
|
||||
ignore_files::{self, IgnoreFile},
|
||||
paths::common_prefix,
|
||||
project::{self, ProjectType},
|
||||
};
|
||||
|
||||
pub async fn dirs(args: &ArgMatches<'static>) -> Result<(PathBuf, PathBuf)> {
|
||||
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 = common_prefix(&origins).unwrap_or_else(|| PathBuf::from("."));
|
||||
debug!(?project_origin, "resolved common/project origin");
|
||||
|
||||
let workdir = env::current_dir()
|
||||
.and_then(|wd| wd.canonicalize())
|
||||
.into_diagnostic()?;
|
||||
debug!(?workdir, "resolved working directory");
|
||||
|
||||
Ok((project_origin, workdir))
|
||||
}
|
||||
|
||||
pub async fn ignores(args: &ArgMatches<'static>, origin: &Path) -> Result<Vec<IgnoreFile>> {
|
||||
let vcs_types = project::types(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(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,
|
||||
})
|
||||
.inspect(|ig| {
|
||||
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");
|
||||
}
|
||||
|
||||
Ok(ignores)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
use std::{ffi::OsString, sync::Arc};
|
||||
|
||||
use clap::ArgMatches;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use watchexec::filter::globset::GlobsetFilterer;
|
||||
|
||||
pub async fn globset(args: &ArgMatches<'static>) -> Result<Arc<GlobsetFilterer>> {
|
||||
let (project_origin, workdir) = super::common::dirs(args).await?;
|
||||
let ignores = super::common::ignores(args, &project_origin).await?;
|
||||
// TODO: load ignorefiles
|
||||
|
||||
let filters = args
|
||||
.values_of("filter")
|
||||
.unwrap_or_default()
|
||||
.map(|f| (f.to_owned(), None));
|
||||
// TODO: scope to workdir?
|
||||
|
||||
// TODO: load ignores from args
|
||||
|
||||
let exts = args
|
||||
.values_of("extensions")
|
||||
.unwrap_or_default()
|
||||
.map(|s| s.split(',').map(|s| OsString::from(s.trim())))
|
||||
.flatten();
|
||||
// TODO: get osstrings directly
|
||||
|
||||
Ok(Arc::new(
|
||||
GlobsetFilterer::new(project_origin, filters, vec![], exts).into_diagnostic()?,
|
||||
))
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use tracing::debug;
|
||||
use watchexec::filter::tagged::{Filter, Matcher, Op, Pattern, Regex, TaggedFilterer};
|
||||
|
||||
pub async fn tagged(args: &ArgMatches<'static>) -> Result<Arc<TaggedFilterer>> {
|
||||
let (project_origin, workdir) = super::common::dirs(args).await?;
|
||||
let ignores = super::common::ignores(args, &project_origin).await?;
|
||||
|
||||
let mut filters = Vec::new();
|
||||
|
||||
for filter in args.values_of("filter").unwrap_or_default() {
|
||||
let mut filter: Filter = filter.parse()?;
|
||||
filter.in_path = Some(workdir.clone());
|
||||
filters.push(filter);
|
||||
}
|
||||
|
||||
debug!(?filters, "parsed filters");
|
||||
|
||||
let filterer = TaggedFilterer::new(project_origin, workdir)?;
|
||||
|
||||
filterer.add_filters(&filters).await?;
|
||||
|
||||
for ignore in &ignores {
|
||||
filterer.add_ignore_file(ignore).await?;
|
||||
}
|
||||
|
||||
// TODO: load global/env filter files
|
||||
// TODO: load -F filter files
|
||||
|
||||
Ok(filterer)
|
||||
}
|
147
cli/src/main.rs
147
cli/src/main.rs
|
@ -1,23 +1,11 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
env::{self, var},
|
||||
path::PathBuf,
|
||||
};
|
||||
use std::env::var;
|
||||
|
||||
use dunce::canonicalize;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use tracing::{debug, warn};
|
||||
use watchexec::{
|
||||
event::Event,
|
||||
filter::tagged::{Filter, Matcher, Op, Pattern, Regex},
|
||||
ignore_files::{self, IgnoreFile},
|
||||
paths::common_prefix,
|
||||
project::{self, ProjectType},
|
||||
Watchexec,
|
||||
};
|
||||
use watchexec::{event::Event, Watchexec};
|
||||
|
||||
mod args;
|
||||
mod config;
|
||||
mod filterer;
|
||||
|
||||
#[cfg(target_env = "musl")]
|
||||
#[global_allocator]
|
||||
|
@ -44,122 +32,19 @@ async fn main() -> Result<()> {
|
|||
.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 = 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,
|
||||
})
|
||||
.inspect(|ig| {
|
||||
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();
|
||||
|
||||
// TODO: move into config
|
||||
let workdir = env::current_dir()
|
||||
.and_then(|wd| wd.canonicalize())
|
||||
.into_diagnostic()?;
|
||||
for filter in args.values_of("filter").unwrap_or_default() {
|
||||
// TODO: use globset
|
||||
let mut filter: Filter = filter.parse()?;
|
||||
filter.in_path = Some(workdir.clone());
|
||||
filters.push(filter);
|
||||
}
|
||||
|
||||
for ext in args
|
||||
.values_of("extensions")
|
||||
.unwrap_or_default()
|
||||
.map(|s| s.split(',').map(|s| s.trim()))
|
||||
.flatten()
|
||||
{
|
||||
// TODO: use globset
|
||||
filters.push(Filter {
|
||||
in_path: None,
|
||||
on: Matcher::Path,
|
||||
op: Op::Regex,
|
||||
pat: Pattern::Regex(Regex::new(&format!("[.]{}$", ext)).into_diagnostic()?),
|
||||
negate: false,
|
||||
});
|
||||
}
|
||||
|
||||
debug!(?filters, "parsed filters and extensions");
|
||||
|
||||
let (init, runtime, filterer) = config::new(&args)?;
|
||||
filterer.add_filters(&filters).await?;
|
||||
|
||||
for ignore in &ignores {
|
||||
filterer.add_ignore_file(ignore).await?;
|
||||
}
|
||||
let init = config::init(&args)?;
|
||||
let mut runtime = config::runtime(&args)?;
|
||||
runtime.filterer(
|
||||
if var("WATCHEXEC_FILTERER")
|
||||
.map(|v| v == "tagged")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
eprintln!("!!! EXPERIMENTAL: using tagged filterer !!!");
|
||||
filterer::tagged(&args).await?
|
||||
} else {
|
||||
filterer::globset(&args).await?
|
||||
},
|
||||
);
|
||||
|
||||
let wx = Watchexec::new(init, runtime)?;
|
||||
|
||||
|
|
Loading…
Reference in New Issue