feat(cli): add -W for non-recursive watches

This commit is contained in:
Félix Saparelli 2024-04-27 23:49:46 +12:00
parent 0504c6c24f
commit 9c04011cde
No known key found for this signature in database
5 changed files with 115 additions and 80 deletions

View File

@ -1,6 +1,9 @@
use std::{
collections::BTreeSet,
ffi::{OsStr, OsString},
path::PathBuf,
fs::canonicalize,
mem::take,
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};
@ -11,7 +14,7 @@ use clap::{
};
use miette::{IntoDiagnostic, Result};
use tokio::{fs::File, io::AsyncReadExt};
use watchexec::paths::PATH_SEPARATOR;
use watchexec::{paths::PATH_SEPARATOR, sources::fs::WatchedPath};
use watchexec_signals::Signal;
use crate::filterer::parse::parse_filter_program;
@ -128,7 +131,25 @@ pub struct Args {
value_hint = ValueHint::AnyPath,
value_name = "PATH",
)]
pub paths: Vec<PathBuf>,
pub recursive_paths: Vec<PathBuf>,
/// Watch a specific directory, non-recursively
///
/// Unlike '-w', folders watched with this option are not recursed into.
///
/// This option can be specified multiple times to watch multiple directories non-recursively.
#[arg(
short = 'W',
long = "watch-non-recursive",
help_heading = OPTSET_FILTERING,
value_hint = ValueHint::AnyPath,
value_name = "PATH",
)]
pub non_recursive_paths: Vec<PathBuf>,
#[doc(hidden)]
#[arg(skip)]
pub paths: Vec<WatchedPath>,
/// Clear screen before running command
///
@ -1221,6 +1242,61 @@ pub async fn get_args() -> Result<Args> {
.exit();
}
args.workdir = Some(if let Some(w) = take(&mut args.workdir) {
w
} else {
let curdir = std::env::current_dir().into_diagnostic()?;
canonicalize(curdir).into_diagnostic()?
});
debug!(workdir=?args.workdir, "current directory");
let project_origin = if let Some(p) = take(&mut args.project_origin) {
p
} else {
crate::dirs::project_origin(&args).await?
};
args.paths = take(&mut args.recursive_paths)
.into_iter()
.map(|path| {
{
if path.is_absolute() {
Ok(path)
} else {
canonicalize(project_origin.join(path)).into_diagnostic()
}
}
.map(WatchedPath::non_recursive)
})
.chain(take(&mut args.non_recursive_paths).into_iter().map(|path| {
{
if path.is_absolute() {
Ok(path)
} else {
canonicalize(project_origin.join(path)).into_diagnostic()
}
}
.map(WatchedPath::non_recursive)
}))
.collect::<Result<BTreeSet<_>>>()?
.into_iter()
.collect();
if args.paths.len() == 1
&& args
.paths
.first()
.map_or(false, |p| p.as_ref() == Path::new("/dev/null"))
{
debug!("only path is /dev/null, not watching anything");
args.paths = Vec::new();
} else if args.paths.is_empty() {
debug!("no paths, using current directory");
args.paths.push(args.workdir.clone().unwrap().into());
}
debug!(paths=?args.paths, "resolved all watched paths");
for (n, prog) in args.filter_programs.iter_mut().enumerate() {
if let Some(progpath) = prog.strip_prefix('@') {
trace!(?n, path=?progpath, "reading filter program from file");
@ -1233,7 +1309,7 @@ pub async fn get_args() -> Result<Args> {
}
}
args.filter_programs_parsed = std::mem::take(&mut args.filter_programs)
args.filter_programs_parsed = take(&mut args.filter_programs)
.into_iter()
.enumerate()
.map(parse_filter_program)

View File

@ -1,11 +1,10 @@
use std::{
borrow::Cow,
collections::HashMap,
env::{current_dir, var},
env::var,
ffi::{OsStr, OsString},
fs::File,
io::{IsTerminal, Write},
path::Path,
process::Stdio,
sync::{
atomic::{AtomicBool, AtomicU8, Ordering},
@ -68,19 +67,7 @@ pub fn make_config(args: &Args, state: &State) -> Result<Config> {
eprintln!("[[Error (not fatal)]]\n{}", Report::new(err.error));
});
config.pathset(if args.paths.is_empty() {
vec![current_dir().into_diagnostic()?]
} else if args.paths.len() == 1
&& args
.paths
.first()
.map_or(false, |p| p == Path::new("/dev/null"))
{
// special case: /dev/null means "don't start the fs event source"
Vec::new()
} else {
args.paths.clone()
});
config.pathset(args.paths.clone());
config.throttle(args.debounce.0);
config.keyboard_events(args.stdin_quit);

View File

@ -1,7 +1,5 @@
use std::{
borrow::Cow,
collections::HashSet,
env,
path::{Path, PathBuf},
};
@ -14,16 +12,7 @@ use watchexec::paths::common_prefix;
use crate::args::Args;
type ProjectOriginPath = PathBuf;
type WorkDirPath = PathBuf;
/// Extract relevant directories (in particular the project origin and work directory)
/// given the command line arguments that were provided
pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> {
let curdir = env::current_dir().into_diagnostic()?;
let curdir = canonicalize(curdir).await.into_diagnostic()?;
debug!(?curdir, "current directory");
pub async fn project_origin(args: &Args) -> Result<PathBuf> {
let project_origin = if let Some(origin) = &args.project_origin {
debug!(?origin, "project origin override");
canonicalize(origin).await.into_diagnostic()?
@ -34,27 +23,19 @@ pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> {
};
debug!(?homedir, "home directory");
let mut paths = HashSet::new();
for path in &args.paths {
paths.insert(canonicalize(path).await.into_diagnostic()?);
}
let homedir_requested = homedir.as_ref().map_or(false, |home| paths.contains(home));
let homedir_requested = homedir.as_ref().map_or(false, |home| {
args.paths
.binary_search_by_key(home, |w| PathBuf::from(w.clone()))
.is_ok()
});
debug!(
?homedir_requested,
"resolved whether the homedir is explicitly requested"
);
if paths.is_empty() {
debug!("no paths, using current directory");
paths.insert(curdir.clone());
}
debug!(?paths, "resolved all watched paths");
let mut origins = HashSet::new();
for path in paths {
origins.extend(project_origins::origins(&path).await);
for path in &args.paths {
origins.extend(project_origins::origins(path).await);
}
match (homedir, homedir_requested) {
@ -67,7 +48,7 @@ pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> {
if origins.is_empty() {
debug!("no origins, using current directory");
origins.insert(curdir.clone());
origins.insert(args.workdir.clone().unwrap());
}
debug!(?origins, "resolved all project origins");
@ -82,10 +63,7 @@ pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> {
};
info!(?project_origin, "resolved common/project origin");
let workdir = curdir;
info!(?workdir, "resolved working directory");
Ok((project_origin, workdir))
Ok(project_origin)
}
pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
@ -98,37 +76,30 @@ pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
vcs_types
}
pub async fn ignores(
args: &Args,
vcs_types: &[ProjectType],
origin: &Path,
) -> Result<Vec<IgnoreFile>> {
fn higher_make_absolute_if_needed<'a>(
origin: &'a Path,
) -> impl 'a + Fn(&'a PathBuf) -> Cow<'a, Path> {
|path| {
if path.is_absolute() {
Cow::Borrowed(path)
} else {
Cow::Owned(origin.join(path))
}
}
}
pub async fn ignores(args: &Args, vcs_types: &[ProjectType]) -> Result<Vec<IgnoreFile>> {
let origin = args.project_origin.clone().unwrap();
let mut skip_git_global_excludes = false;
let mut ignores = if args.no_project_ignore {
Vec::new()
} else {
let make_absolute_if_needed = higher_make_absolute_if_needed(origin);
let include_paths = args.paths.iter().map(&make_absolute_if_needed);
let ignore_files = args.ignore_files.iter().map(&make_absolute_if_needed);
let ignore_files = args.ignore_files.iter().map(|path| {
if path.is_absolute() {
path.into()
} else {
origin.join(path)
}
});
let (mut ignores, errors) = ignore_files::from_origin(
IgnoreFilesFromOriginArgs::new_unchecked(origin, include_paths, ignore_files)
.canonicalise()
.await
.into_diagnostic()?,
IgnoreFilesFromOriginArgs::new_unchecked(
&origin,
args.paths.iter().map(PathBuf::from),
ignore_files,
)
.canonicalise()
.await
.into_diagnostic()?,
)
.await;
@ -221,7 +192,7 @@ pub async fn ignores(
.filter(|ig| {
!ig.applies_in
.as_ref()
.map_or(false, |p| p.starts_with(origin))
.map_or(false, |p| p.starts_with(&origin))
})
.collect::<Vec<_>>();
debug!(

View File

@ -16,7 +16,6 @@ use watchexec_filterer_globset::GlobsetFilterer;
use crate::args::{Args, FsEvent};
mod dirs;
pub(crate) mod parse;
mod proglib;
mod progs;
@ -71,13 +70,14 @@ impl Filterer for WatchexecFilterer {
impl WatchexecFilterer {
/// Create a new filterer from the given arguments
pub async fn new(args: &Args) -> Result<Arc<Self>> {
let (project_origin, workdir) = dirs::dirs(args).await?;
let project_origin = args.project_origin.clone().unwrap();
let workdir = args.workdir.clone().unwrap();
let ignore_files = if args.no_discover_ignore {
Vec::new()
} else {
let vcs_types = dirs::vcs_types(&project_origin).await;
dirs::ignores(args, &vcs_types, &project_origin).await?
let vcs_types = crate::dirs::vcs_types(&project_origin).await;
crate::dirs::ignores(args, &vcs_types).await?
};
let mut ignores = Vec::new();

View File

@ -18,6 +18,7 @@ use crate::filterer::WatchexecFilterer;
pub mod args;
mod config;
mod dirs;
mod emits;
mod filterer;
mod state;