Clippy fixes (#465)

This commit is contained in:
Félix Saparelli 2023-01-07 02:53:49 +13:00 committed by GitHub
parent 6d65c05e35
commit dc98370492
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 558 additions and 476 deletions

12
.clippy-lints Normal file
View File

@ -0,0 +1,12 @@
-D clippy::all
-W clippy::nursery
-W clippy::pedantic
-A clippy::module-name-repetitions
-A clippy::similar-names
-A clippy::cognitive-complexity
-A clippy::too-many-lines
-A clippy::missing-errors-doc
-A clippy::missing-panics-doc
-A clippy::default-trait-access
-A clippy::enum-glob-use
-A clippy::option-if-let-else

View File

@ -13,9 +13,14 @@ env:
CARGO_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
check:
clippy:
strategy:
fail-fast: false
matrix:
platform:
- ubuntu
@ -29,9 +34,8 @@ jobs:
- uses: actions/checkout@v3
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update nightly
rustup default nightly
rustup component add clippy
rustup toolchain install stable --profile minimal --no-self-update --component clippy
rustup default stable
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
@ -54,5 +58,5 @@ jobs:
${{ runner.os }}-cargo-
- name: Clippy
run: cargo clippy -- -D clippy::all
run: cargo clippy -- $(cat .clippy-lints | tr -d '\r' | xargs)
shell: bash

View File

@ -13,6 +13,10 @@ env:
CARGO_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
strategy:
@ -23,7 +27,7 @@ jobs:
- windows
toolchain:
- stable
- 1.60.0
- 1.61.0
name: Test ${{ matrix.platform }} with Rust ${{ matrix.toolchain }}
runs-on: "${{ matrix.platform }}-latest"
@ -32,7 +36,7 @@ jobs:
- uses: actions/checkout@v3
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update ${{ matrix.toolchain }}
rustup toolchain install ${{ matrix.toolchain }} --profile minimal --no-self-update
rustup default ${{ matrix.toolchain }}
# https://github.com/actions/cache/issues/752
@ -43,20 +47,60 @@ jobs:
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- name: Configure caching
- name: Cargo caching
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.toolchain }}-
${{ runner.os }}-cargo-
- name: Compilation caching
uses: actions/cache@v3
with:
path: target/
key: ${{ runner.os }}-target-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
- name: Run test suite
run: cargo test
- name: Check that CLI runs
run: cargo run -p watchexec-cli -- -1 echo
cross-checks:
name: Checks only against select targets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
sudo apt-get install -y musl-tools
rustup target add x86_64-unknown-linux-musl
- name: Install cross
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Cargo caching
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-stable-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-stable-
${{ runner.os }}-cargo-
- run: cargo check --target x86_64-unknown-linux-musl
- run: cross check --target x86_64-unknown-freebsd
- run: cross check --target x86_64-unknown-netbsd

58
Cargo.lock generated
View File

@ -542,12 +542,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "dunce"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541"
[[package]]
name = "either"
version = "1.8.0"
@ -642,9 +636,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
dependencies = [
"futures-channel",
"futures-core",
@ -657,9 +651,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
dependencies = [
"futures-core",
"futures-sink",
@ -667,15 +661,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
[[package]]
name = "futures-executor"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [
"futures-core",
"futures-task",
@ -684,9 +678,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
@ -705,9 +699,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [
"proc-macro2",
"quote",
@ -716,21 +710,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
[[package]]
name = "futures-task"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
[[package]]
name = "futures-util"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [
"futures-channel",
"futures-core",
@ -1139,7 +1133,6 @@ dependencies = [
name = "ignore-files"
version = "1.0.1"
dependencies = [
"dunce",
"futures",
"git-config",
"ignore",
@ -1461,6 +1454,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "normalize-path"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf22e319b2e3cb517350572e3b70c6822e0a520abfb5c78f690e829a73e8d9f2"
[[package]]
name = "notify"
version = "5.0.0"
@ -1736,7 +1735,6 @@ dependencies = [
name = "project-origins"
version = "1.1.1"
dependencies = [
"dunce",
"futures",
"miette",
"tokio",
@ -2658,11 +2656,10 @@ dependencies = [
"atomic-take",
"clearscreen",
"command-group",
"dunce",
"futures",
"ignore-files",
"libc",
"miette",
"normalize-path",
"notify",
"once_cell",
"project-origins",
@ -2679,7 +2676,6 @@ dependencies = [
"clap",
"console-subscriber",
"dirs 4.0.0",
"dunce",
"embed-resource",
"futures",
"ignore-files",
@ -2699,7 +2695,6 @@ dependencies = [
name = "watchexec-filterer-globset"
version = "1.0.1"
dependencies = [
"dunce",
"ignore",
"ignore-files",
"project-origins",
@ -2714,7 +2709,6 @@ dependencies = [
name = "watchexec-filterer-ignore"
version = "1.0.0"
dependencies = [
"dunce",
"ignore",
"ignore-files",
"project-origins",
@ -2728,7 +2722,7 @@ dependencies = [
name = "watchexec-filterer-tagged"
version = "0.1.1"
dependencies = [
"dunce",
"futures",
"globset",
"ignore",
"ignore-files",

View File

@ -22,7 +22,6 @@ path = "src/main.rs"
[dependencies]
console-subscriber = { version = "0.1.0", optional = true }
dirs = "4.0.0"
dunce = "1.0.2"
futures = "0.3.17"
miette = { version = "5.3.0", features = ["fancy"] }
notify-rust = "4.5.2"

View File

@ -275,7 +275,7 @@ pub fn get_args(tagged_filterer: bool) -> Result<ArgMatches> {
let arg_file = BufReader::new(
File::open(arg_path)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to open argument file {:?}", arg_path))?,
.wrap_err_with(|| format!("Failed to open argument file {arg_path:?}"))?,
);
let mut more_args: Vec<OsString> = arg_file

View File

@ -1,7 +1,7 @@
use std::convert::Infallible;
use clap::ArgMatches;
use miette::{Report, Result};
use miette::Report;
use tracing::error;
use watchexec::{
config::InitConfig,
@ -10,7 +10,7 @@ use watchexec::{
ErrorHook,
};
pub fn init(_args: &ArgMatches) -> Result<InitConfig> {
pub fn init(_args: &ArgMatches) -> InitConfig {
let mut config = InitConfig::default();
config.on_error(SyncFnHandler::from(
|err: ErrorHook| -> std::result::Result<(), Infallible> {
@ -47,5 +47,5 @@ pub fn init(_args: &ArgMatches) -> Result<InitConfig> {
},
));
Ok(config)
config
}

View File

@ -1,6 +1,6 @@
use std::{
collections::HashMap, convert::Infallible, env::current_dir, ffi::OsString, path::Path,
str::FromStr, time::Duration,
str::FromStr, string::ToString, time::Duration,
};
use clap::ArgMatches;
@ -12,7 +12,7 @@ use watchexec::{
command::{Command, Shell},
config::RuntimeConfig,
error::RuntimeError,
event::{ProcessEnd, Tag},
event::{Event, ProcessEnd, Tag},
fs::Watcher,
handler::SyncFnHandler,
keyboard::Keyboard,
@ -52,28 +52,23 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
let clear = args.is_present("clear");
let notif = args.is_present("notif");
let mut on_busy = args
.value_of("on-busy-update")
.unwrap_or("queue")
.to_owned();
if args.is_present("restart") {
on_busy = "restart".into();
let on_busy = if args.is_present("restart") {
"restart"
} else if args.is_present("watch-when-idle") {
"do-nothing"
} else {
args.value_of("on-busy-update").unwrap_or("queue")
}
.to_owned();
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()?;
if args.is_present("kill") {
signal = Some(SubSignal::ForceStop);
}
let signal = if args.is_present("kill") {
Some(SubSignal::ForceStop)
} else {
args.value_of("signal")
.map(SubSignal::from_str)
.transpose()
.into_diagnostic()?
};
let print_events = args.is_present("print-events");
let once = args.is_present("once");
@ -89,7 +84,7 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
if print_events {
for (n, event) in action.events.iter().enumerate() {
eprintln!("[EVENT {}] {}", n, event);
eprintln!("[EVENT {n}] {event}");
}
}
@ -105,13 +100,8 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
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();
let signals: Vec<MainSignal> = action.events.iter().flat_map(Event::signals).collect();
let has_paths = action.events.iter().flat_map(Event::paths).next().is_some();
if signals.contains(&MainSignal::Terminate) {
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
@ -144,28 +134,28 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
return fut;
}
let completion = action.events.iter().flat_map(|e| e.completions()).next();
let completion = action.events.iter().flat_map(Event::completions).next();
if let Some(status) = completion {
let (msg, printit) = match status {
Some(ProcessEnd::ExitError(code)) => {
(format!("Command exited with {}", code), true)
(format!("Command exited with {code}"), true)
}
Some(ProcessEnd::ExitSignal(sig)) => {
(format!("Command killed by {:?}", sig), true)
(format!("Command killed by {sig:?}"), true)
}
Some(ProcessEnd::ExitStop(sig)) => {
(format!("Command stopped by {:?}", sig), true)
(format!("Command stopped by {sig:?}"), true)
}
Some(ProcessEnd::Continued) => ("Command continued".to_string(), true),
Some(ProcessEnd::Exception(ex)) => {
(format!("Command ended by exception {:#x}", ex), true)
(format!("Command ended by exception {ex:#x}"), true)
}
Some(ProcessEnd::Success) => ("Command was successful".to_string(), false),
None => ("Command completed".to_string(), false),
};
if printit {
eprintln!("[[{}]]", msg);
eprintln!("[[{msg}]]");
}
if notif {
@ -173,10 +163,12 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
.summary("Watchexec: command ended")
.body(&msg)
.show()
.map(drop)
.unwrap_or_else(|err| {
eprintln!("[[Failed to send desktop notification: {}]]", err);
});
.map_or_else(
|err| {
eprintln!("[[Failed to send desktop notification: {err}]]");
},
drop,
);
}
action.outcome(Outcome::DoNothing);
@ -198,7 +190,6 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
let when_idle = start.clone();
let when_running = match on_busy.as_str() {
"do-nothing" => Outcome::DoNothing,
"restart" => Outcome::both(
if let Some(sig) = signal {
Outcome::both(
@ -212,6 +203,7 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
),
"signal" => Outcome::Signal(signal.unwrap_or(SubSignal::Terminate)),
"queue" => Outcome::wait(start),
// "do-nothing" => Outcome::DoNothing,
_ => Outcome::DoNothing,
};
@ -250,7 +242,7 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
envs.extend(
summarise_events_to_env(prespawn.events.iter())
.into_iter()
.map(|(k, v)| (format!("WATCHEXEC_{}_PATH", k), v)),
.map(|(k, v)| (format!("WATCHEXEC_{k}_PATH"), v)),
);
}
@ -276,10 +268,12 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
.summary("Watchexec: change detected")
.body(&format!("Running {}", postspawn.command))
.show()
.map(drop)
.unwrap_or_else(|err| {
eprintln!("[[Failed to send desktop notification: {}]]", err);
});
.map_or_else(
|err| {
eprintln!("[[Failed to send desktop notification: {err}]]");
},
drop,
);
}
Ok::<(), Infallible>(())
@ -292,7 +286,7 @@ fn interpret_command_args(args: &ArgMatches) -> Result<Command> {
let mut cmd = args
.values_of("command")
.expect("(clap) Bug: command is not present")
.map(|s| s.to_string())
.map(ToString::to_string)
.collect::<Vec<_>>();
Ok(if args.is_present("no-shell") {
@ -321,8 +315,8 @@ fn interpret_command_args(args: &ArgMatches) -> Result<Command> {
let (shprog, shopts) = sh.split_first().unwrap();
(
Shell::Unix(shprog.to_string()),
shopts.iter().map(|s| s.to_string()).collect(),
Shell::Unix((*shprog).to_string()),
shopts.iter().map(|s| (*s).to_string()).collect(),
)
}
} else {

View File

@ -5,32 +5,31 @@ use std::{
};
use clap::ArgMatches;
use dunce::canonicalize;
use ignore_files::IgnoreFile;
use miette::{miette, IntoDiagnostic, Result};
use project_origins::ProjectType;
use tokio::fs::canonicalize;
use tracing::{debug, info, warn};
use watchexec::paths::common_prefix;
pub async fn dirs(args: &ArgMatches) -> Result<(PathBuf, PathBuf)> {
let curdir = env::current_dir()
.and_then(canonicalize)
.into_diagnostic()?;
let curdir = env::current_dir().into_diagnostic()?;
let curdir = canonicalize(curdir).await.into_diagnostic()?;
debug!(?curdir, "current directory");
let project_origin = if let Some(origin) = args.value_of_os("project-origin") {
debug!(?origin, "project origin override");
canonicalize(origin).into_diagnostic()?
canonicalize(origin).await.into_diagnostic()?
} else {
let homedir = dirs::home_dir()
.map(canonicalize)
.transpose()
.into_diagnostic()?;
let homedir = match dirs::home_dir() {
None => None,
Some(dir) => Some(canonicalize(dir).await.into_diagnostic()?),
};
debug!(?homedir, "home directory");
let mut paths = HashSet::new();
for path in args.values_of_os("paths").unwrap_or_default() {
paths.insert(canonicalize(path).into_diagnostic()?);
paths.insert(canonicalize(path).await.into_diagnostic()?);
}
let homedir_requested = homedir.as_ref().map_or(false, |home| paths.contains(home));
@ -71,6 +70,7 @@ pub async fn dirs(args: &ArgMatches) -> Result<(PathBuf, PathBuf)> {
common_prefix(&origins)
.ok_or_else(|| miette!("no common prefix, but this should never fail"))?,
)
.await
.into_diagnostic()?
};
info!(?project_origin, "resolved common/project origin");

View File

@ -26,23 +26,23 @@ pub async fn globset(args: &ArgMatches) -> Result<Arc<WatchexecFilterer>> {
if !args.is_present("no-default-ignore") {
ignores.extend([
(format!("**{s}.DS_Store", s = MAIN_SEPARATOR), None),
(format!("**{MAIN_SEPARATOR}.DS_Store"), 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!("**{s}.bzr{s}**", s = MAIN_SEPARATOR), None),
(format!("**{s}_darcs{s}**", s = MAIN_SEPARATOR), None),
(format!("**{MAIN_SEPARATOR}.bzr{MAIN_SEPARATOR}**"), None),
(format!("**{MAIN_SEPARATOR}_darcs{MAIN_SEPARATOR}**"), None),
(
format!("**{s}.fossil-settings{s}**", s = MAIN_SEPARATOR),
format!("**{MAIN_SEPARATOR}.fossil-settings{MAIN_SEPARATOR}**"),
None,
),
(format!("**{s}.git{s}**", s = MAIN_SEPARATOR), None),
(format!("**{s}.hg{s}**", s = MAIN_SEPARATOR), None),
(format!("**{s}.pijul{s}**", s = MAIN_SEPARATOR), None),
(format!("**{s}.svn{s}**", s = 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),
]);
}

View File

@ -14,7 +14,7 @@ pub async fn tagged(args: &ArgMatches) -> Result<Arc<TaggedFilterer>> {
let vcs_types = super::common::vcs_types(&project_origin).await;
let ignores = super::common::ignores(args, &vcs_types, &project_origin).await;
let filterer = TaggedFilterer::new(project_origin, workdir.clone())?;
let filterer = TaggedFilterer::new(project_origin, workdir.clone()).await?;
for ignore in &ignores {
filterer.add_ignore_file(ignore).await?;
@ -25,7 +25,7 @@ pub async fn tagged(args: &ArgMatches) -> Result<Arc<TaggedFilterer>> {
let file = FilterFile(IgnoreFile {
applies_in: None,
applies_to: None,
path: dunce::canonicalize(path).into_diagnostic()?,
path: tokio::fs::canonicalize(path).await.into_diagnostic()?,
});
filter_files.push(file);
}

View File

@ -1,4 +1,5 @@
#![deny(rust_2018_idioms)]
#![allow(clippy::missing_const_for_fn, clippy::future_not_send)]
use std::{env::var, fs::File, sync::Mutex};
@ -82,7 +83,7 @@ pub async fn run() -> Result<()> {
info!(version=%env!("CARGO_PKG_VERSION"), "constructing Watchexec from CLI");
debug!(?args, "arguments");
let init = config::init(&args)?;
let init = config::init(&args);
let mut runtime = config::runtime(&args)?;
runtime.filterer(if tagged_filterer {
eprintln!("!!! EXPERIMENTAL: using tagged filterer !!!");

View File

@ -12,7 +12,7 @@ homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.60.0"
rust-version = "1.61.0"
edition = "2021"
[dependencies]
@ -32,7 +32,6 @@ version = "1.0.0"
path = "../ignore"
[dev-dependencies]
dunce = "1.0.2"
tracing-subscriber = "0.3.6"
[dev-dependencies.project-origins]

View File

@ -9,7 +9,7 @@ _The default filterer implementation for Watchexec._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.60.0 (incurs a minor semver bump).
- Minimum Supported Rust Version: 1.61.0 (incurs a minor semver bump).
- Status: maintained.
[docs]: https://docs.rs/watchexec-filterer-globset

View File

@ -48,6 +48,7 @@ impl GlobsetFilterer {
/// The extensions list is used to filter files by extension.
///
/// Non-path events are always passed.
#[allow(clippy::future_not_send)]
pub async fn new(
origin: impl AsRef<Path>,
filters: impl IntoIterator<Item = (String, Option<PathBuf>)>,
@ -133,9 +134,7 @@ impl Filterer for GlobsetFilterer {
} else {
Ok(paths.any(|(path, file_type)| {
let _span = trace_span!("path", ?path).entered();
let is_dir = file_type
.map(|t| matches!(t, FileType::Dir))
.unwrap_or(false);
let is_dir = file_type.map_or(false, |t| matches!(t, FileType::Dir));
if self.ignores.matched(path, is_dir).is_ignore() {
trace!("ignored by globset ignore");

View File

@ -380,7 +380,7 @@ async fn multipath_allow_on_any_one_pass() {
};
let filterer = filt(&[], &[], &["py"]).await;
let origin = dunce::canonicalize(".").unwrap();
let origin = tokio::fs::canonicalize(".").await.unwrap();
let event = Event {
tags: vec![

View File

@ -35,7 +35,7 @@ pub trait PathHarness: Filterer {
}
fn path_pass(&self, path: &str, file_type: Option<FileType>, pass: bool) {
let origin = dunce::canonicalize(".").unwrap();
let origin = std::fs::canonicalize(".").unwrap();
let full_path = if let Some(suf) = path.strip_prefix("/test/") {
origin.join(suf)
} else if Path::new(path).has_root() {
@ -110,12 +110,12 @@ pub async fn globset_filt(
ignores: &[&str],
extensions: &[&str],
) -> GlobsetFilterer {
let origin = dunce::canonicalize(".").unwrap();
let origin = tokio::fs::canonicalize(".").await.unwrap();
tracing_init();
GlobsetFilterer::new(
origin,
filters.iter().map(|s| (s.to_string(), None)),
ignores.iter().map(|s| (s.to_string(), None)),
filters.iter().map(|s| ((*s).to_string(), None)),
ignores.iter().map(|s| ((*s).to_string(), None)),
vec![],
extensions.iter().map(OsString::from),
)
@ -130,7 +130,7 @@ pub trait Applies {
impl Applies for IgnoreFile {
fn applies_in(mut self, origin: &str) -> Self {
let origin = dunce::canonicalize(".").unwrap().join(origin);
let origin = std::fs::canonicalize(".").unwrap().join(origin);
self.applies_in = Some(origin);
self
}

View File

@ -12,7 +12,7 @@ homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.60.0"
rust-version = "1.61.0"
edition = "2021"
[dependencies]
@ -28,7 +28,6 @@ version = "2.0.2"
path = "../../lib"
[dev-dependencies]
dunce = "1.0.2"
tracing-subscriber = "0.3.6"
[dev-dependencies.project-origins]

View File

@ -9,7 +9,7 @@ _(Sub)filterer implementation for ignore files._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.60.0 (incurs a minor semver bump).
- Minimum Supported Rust Version: 1.61.0 (incurs a minor semver bump).
- Status: maintained.
This is mostly a thin layer above the [ignore-files](../../ignore-files) crate, and is meant to be

View File

@ -36,9 +36,7 @@ impl Filterer for IgnoreFilterer {
for (path, file_type) in event.paths() {
let _span = trace_span!("checking_against_compiled", ?path, ?file_type).entered();
let is_dir = file_type
.map(|t| matches!(t, FileType::Dir))
.unwrap_or(false);
let is_dir = file_type.map_or(false, |t| matches!(t, FileType::Dir));
match self.0.match_path(path, is_dir) {
Match::None => {

View File

@ -37,11 +37,11 @@ fn folders_suite(filterer: &IgnoreFilterer, name: &str) {
filterer.dir_does_pass("apples/carrots/cauliflowers/oranges");
filterer.dir_does_pass("apples/carrots/cauliflowers/artichokes/oranges");
filterer.file_does_pass(&format!("raw-{}", name));
filterer.dir_does_pass(&format!("raw-{}", name));
filterer.file_does_pass(&format!("raw-{}/carrots/cauliflowers/oranges", name));
filterer.file_does_pass(&format!("raw-{}/oranges/bananas", name));
filterer.dir_does_pass(&format!("raw-{}/carrots/cauliflowers/oranges", name));
filterer.file_does_pass(&format!("raw-{name}"));
filterer.dir_does_pass(&format!("raw-{name}"));
filterer.file_does_pass(&format!("raw-{name}/carrots/cauliflowers/oranges"));
filterer.file_does_pass(&format!("raw-{name}/oranges/bananas"));
filterer.dir_does_pass(&format!("raw-{name}/carrots/cauliflowers/oranges"));
filterer.file_does_pass(&format!(
"raw-{}/carrots/cauliflowers/artichokes/oranges",
name
@ -51,11 +51,11 @@ fn folders_suite(filterer: &IgnoreFilterer, name: &str) {
name
));
filterer.dir_doesnt_pass(&format!("{}/carrots/cauliflowers/oranges", name));
filterer.dir_doesnt_pass(&format!("{}/carrots/cauliflowers/artichokes/oranges", name));
filterer.file_doesnt_pass(&format!("{}/carrots/cauliflowers/oranges", name));
filterer.file_doesnt_pass(&format!("{}/carrots/cauliflowers/artichokes/oranges", name));
filterer.file_doesnt_pass(&format!("{}/oranges/bananas", name));
filterer.dir_doesnt_pass(&format!("{name}/carrots/cauliflowers/oranges"));
filterer.dir_doesnt_pass(&format!("{name}/carrots/cauliflowers/artichokes/oranges"));
filterer.file_doesnt_pass(&format!("{name}/carrots/cauliflowers/oranges"));
filterer.file_doesnt_pass(&format!("{name}/carrots/cauliflowers/artichokes/oranges"));
filterer.file_doesnt_pass(&format!("{name}/oranges/bananas"));
}
#[tokio::test]

View File

@ -34,7 +34,7 @@ pub trait PathHarness: Filterer {
}
fn path_pass(&self, path: &str, file_type: Option<FileType>, pass: bool) {
let origin = dunce::canonicalize(".").unwrap();
let origin = std::fs::canonicalize(".").unwrap();
let full_path = if let Some(suf) = path.strip_prefix("/test/") {
origin.join(suf)
} else if Path::new(path).has_root() {
@ -180,7 +180,7 @@ fn tracing_init() {
pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> IgnoreFilterer {
tracing_init();
let origin = dunce::canonicalize(".").unwrap().join(origin);
let origin = tokio::fs::canonicalize(".").await.unwrap().join(origin);
IgnoreFilterer(
IgnoreFilter::new(origin, ignore_files)
.await
@ -189,7 +189,7 @@ pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> IgnoreFil
}
pub fn ig_file(name: &str) -> IgnoreFile {
let path = dunce::canonicalize(".")
let path = std::fs::canonicalize(".")
.unwrap()
.join("tests")
.join("ignores")
@ -208,7 +208,7 @@ pub trait Applies {
impl Applies for IgnoreFile {
fn applies_in(mut self, origin: &str) -> Self {
let origin = dunce::canonicalize(".").unwrap().join(origin);
let origin = std::fs::canonicalize(".").unwrap().join(origin);
self.applies_in = Some(origin);
self
}

View File

@ -12,18 +12,18 @@ homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.60.0"
rust-version = "1.61.0"
edition = "2021"
[dependencies]
miette = "5.3.0"
thiserror = "1.0.26"
tracing = "0.1.26"
dunce = "1.0.2"
futures = "0.3.25"
globset = "0.4.8"
ignore = "0.4.18"
miette = "5.3.0"
nom = "7.0.0"
regex = "1.5.4"
thiserror = "1.0.26"
tracing = "0.1.26"
unicase = "2.6.0"
[dependencies.ignore-files]

View File

@ -9,7 +9,7 @@ _Experimental filterer using tagged filters._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.60.0 (incurs a minor semver bump).
- Minimum Supported Rust Version: 1.61.0 (incurs a minor semver bump).
- Status: maintained.
[docs]: https://docs.rs/watchexec-filterer-tagged

View File

@ -9,7 +9,7 @@ use watchexec_filterer_ignore::IgnoreFilterer;
use crate::{Filter, Matcher};
/// Errors emitted by the TaggedFilterer.
/// Errors emitted by the `TaggedFilterer`.
#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]
#[diagnostic(url(docsrs))]

View File

@ -1,9 +1,9 @@
use std::collections::HashSet;
use std::path::PathBuf;
use dunce::canonicalize;
use globset::Glob;
use regex::Regex;
use tokio::fs::canonicalize;
use tracing::{trace, warn};
use unicase::UniCase;
use watchexec::event::Tag;
@ -49,7 +49,7 @@ impl Filter {
(Op::InSet, Pattern::Exact(pat)) => subject == pat,
(Op::NotInSet, Pattern::Set(set)) => !set.contains(subject),
(Op::NotInSet, Pattern::Exact(pat)) => subject != pat,
(op @ Op::Glob | op @ Op::NotGlob, Pattern::Glob(glob)) => {
(op @ (Op::Glob | Op::NotGlob), Pattern::Glob(glob)) => {
// FIXME: someway that isn't this horrible
match Glob::new(glob) {
Ok(glob) => {
@ -86,6 +86,7 @@ impl Filter {
///
/// The resulting filter matches on [`Path`][Matcher::Path], with the [`NotGlob`][Op::NotGlob]
/// op, and a [`Glob`][Pattern::Glob] pattern. If it starts with a `!`, it is negated.
#[must_use]
pub fn from_glob_ignore(in_path: Option<PathBuf>, glob: &str) -> Self {
let (glob, negate) = glob.strip_prefix('!').map_or((glob, false), |g| (g, true));
@ -99,14 +100,16 @@ impl Filter {
}
/// Returns the filter with its `in_path` canonicalised.
pub fn canonicalised(mut self) -> Result<Self, TaggedFiltererError> {
pub async fn canonicalised(mut self) -> Result<Self, TaggedFiltererError> {
if let Some(ctx) = self.in_path {
self.in_path =
Some(
canonicalize(&ctx).map_err(|err| TaggedFiltererError::IoError {
about: "canonicalise Filter in_path",
err,
})?,
canonicalize(&ctx)
.await
.map_err(|err| TaggedFiltererError::IoError {
about: "canonicalise Filter in_path",
err,
})?,
);
trace!(canon=?ctx, "canonicalised in_path");
}
@ -186,13 +189,13 @@ impl Matcher {
match tag {
Tag::Path {
file_type: None, ..
} => &[Matcher::Path],
Tag::Path { .. } => &[Matcher::Path, Matcher::FileType],
Tag::FileEventKind(_) => &[Matcher::FileEventKind],
Tag::Source(_) => &[Matcher::Source],
Tag::Process(_) => &[Matcher::Process],
Tag::Signal(_) => &[Matcher::Signal],
Tag::ProcessCompletion(_) => &[Matcher::ProcessCompletion],
} => &[Self::Path],
Tag::Path { .. } => &[Self::Path, Self::FileType],
Tag::FileEventKind(_) => &[Self::FileEventKind],
Tag::Source(_) => &[Self::Source],
Tag::Process(_) => &[Self::Process],
Tag::Signal(_) => &[Self::Signal],
Tag::ProcessCompletion(_) => &[Self::ProcessCompletion],
_ => {
warn!("unhandled tag: {:?}", tag);
&[]

View File

@ -1,13 +1,14 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::{collections::HashMap, convert::Into};
use dunce::canonicalize;
use futures::{stream::FuturesOrdered, TryStreamExt};
use ignore::{
gitignore::{Gitignore, GitignoreBuilder},
Match,
};
use ignore_files::{IgnoreFile, IgnoreFilter};
use tokio::fs::canonicalize;
use tracing::{debug, trace, trace_span};
use watchexec::{
error::RuntimeError,
@ -49,7 +50,7 @@ pub struct TaggedFilterer {
impl Filterer for TaggedFilterer {
fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> {
self.check(event, priority).map_err(|e| e.into())
self.check(event, priority).map_err(Into::into)
}
}
@ -77,9 +78,9 @@ impl TaggedFilterer {
trace!(prev=%pri_match, now=%true, "negate filter passes, passing this priority");
pri_match = true;
break;
} else {
trace!(prev=%pri_match, now=%pri_match, "negate filter fails, ignoring");
}
trace!(prev=%pri_match, now=%pri_match, "negate filter fails, ignoring");
} else {
trace!(prev=%pri_match, this=%applies, now=%(pri_match&applies), "filter applies to priority");
pri_match &= applies;
@ -246,9 +247,9 @@ impl TaggedFilterer {
trace!(prev=%tag_match, now=%true, "negate filter passes, passing this matcher");
tag_match = true;
break;
} else {
trace!(prev=%tag_match, now=%tag_match, "negate filter fails, ignoring");
}
trace!(prev=%tag_match, now=%tag_match, "negate filter fails, ignoring");
} else {
trace!(prev=%tag_match, this=%app, now=%(tag_match&app), "filter applies to this tag");
tag_match &= app;
@ -287,23 +288,24 @@ impl TaggedFilterer {
/// So, if origin is `/path/to/project` and workdir is `/path/to/project/subtree`:
/// - `path=foo.bar` is resolved to `/path/to/project/subtree/foo.bar`
/// - `path=/foo.bar` is resolved to `/path/to/project/foo.bar`
pub fn new(
origin: impl Into<PathBuf>,
workdir: impl Into<PathBuf>,
) -> Result<Arc<Self>, TaggedFiltererError> {
let origin = canonicalize(origin.into()).map_err(|err| TaggedFiltererError::IoError {
about: "canonicalise origin on new tagged filterer",
err,
})?;
pub async fn new(origin: PathBuf, workdir: PathBuf) -> Result<Arc<Self>, TaggedFiltererError> {
let origin = canonicalize(origin)
.await
.map_err(|err| TaggedFiltererError::IoError {
about: "canonicalise origin on new tagged filterer",
err,
})?;
Ok(Arc::new(Self {
filters: SwapLock::new(HashMap::new()),
ignore_filterer: SwapLock::new(IgnoreFilterer(IgnoreFilter::empty(&origin))),
glob_compiled: SwapLock::new(None),
not_glob_compiled: SwapLock::new(None),
workdir: canonicalize(workdir.into()).map_err(|err| TaggedFiltererError::IoError {
about: "canonicalise workdir on new tagged filterer",
err,
})?,
workdir: canonicalize(workdir)
.await
.map_err(|err| TaggedFiltererError::IoError {
about: "canonicalise workdir on new tagged filterer",
err,
})?,
origin,
}))
}
@ -341,9 +343,9 @@ impl TaggedFilterer {
if matches!(filter.op, Op::Glob | Op::NotGlob) {
trace!("path glob match with match_tag is already handled");
return Ok(None);
} else {
filter.matches(resolved.to_string_lossy())
}
filter.matches(resolved.to_string_lossy())
}
(
Tag::Path {
@ -353,7 +355,7 @@ impl TaggedFilterer {
Matcher::FileType,
) => filter.matches(ft.to_string()),
(Tag::FileEventKind(kind), Matcher::FileEventKind) => {
filter.matches(format!("{:?}", kind))
filter.matches(format!("{kind:?}"))
}
(Tag::Source(src), Matcher::Source) => filter.matches(src.to_string()),
(Tag::Process(pid), Matcher::Process) => filter.matches(pid.to_string()),
@ -368,13 +370,13 @@ impl TaggedFilterer {
};
Ok(filter.matches(text)?
|| filter.matches(format!("SIG{}", text))?
|| filter.matches(format!("SIG{text}"))?
|| filter.matches(int.to_string())?)
}
(Tag::ProcessCompletion(ope), Matcher::ProcessCompletion) => match ope {
None => filter.matches("_"),
Some(ProcessEnd::Success) => filter.matches("success"),
Some(ProcessEnd::ExitError(int)) => filter.matches(format!("error({})", int)),
Some(ProcessEnd::ExitError(int)) => filter.matches(format!("error({int})")),
Some(ProcessEnd::ExitSignal(sig)) => {
let (text, int) = match sig {
SubSignal::Hangup | SubSignal::Custom(1) => ("HUP", 1),
@ -387,12 +389,12 @@ impl TaggedFilterer {
SubSignal::Custom(n) => ("UNK", *n),
};
Ok(filter.matches(format!("signal({})", text))?
|| filter.matches(format!("signal(SIG{})", text))?
|| filter.matches(format!("signal({})", int))?)
Ok(filter.matches(format!("signal({text})"))?
|| filter.matches(format!("signal(SIG{text})"))?
|| filter.matches(format!("signal({int})"))?)
}
Some(ProcessEnd::ExitStop(int)) => filter.matches(format!("stop({})", int)),
Some(ProcessEnd::Exception(int)) => filter.matches(format!("exception({:X})", int)),
Some(ProcessEnd::ExitStop(int)) => filter.matches(format!("stop({int})")),
Some(ProcessEnd::Exception(int)) => filter.matches(format!("exception({int:X})")),
Some(ProcessEnd::Continued) => filter.matches("continued"),
},
(_, _) => {
@ -418,20 +420,24 @@ impl TaggedFilterer {
let mut recompile_globs = false;
let mut recompile_not_globs = false;
let filters = filters
.iter()
.cloned()
.inspect(|f| match f.op {
Op::Glob => {
recompile_globs = true;
}
Op::NotGlob => {
recompile_not_globs = true;
}
_ => {}
})
.map(Filter::canonicalised)
.collect::<Result<Vec<_>, _>>()?;
#[allow(clippy::from_iter_instead_of_collect)]
let filters = FuturesOrdered::from_iter(
filters
.iter()
.cloned()
.inspect(|f| match f.op {
Op::Glob => {
recompile_globs = true;
}
Op::NotGlob => {
recompile_not_globs = true;
}
_ => {}
})
.map(Filter::canonicalised),
)
.try_collect::<Vec<_>>()
.await?;
trace!(?filters, "canonicalised filters");
// TODO: use miette's related and issue canonicalisation errors for all of them
@ -441,22 +447,21 @@ impl TaggedFilterer {
fs.entry(filter.on).or_default().push(filter);
}
})
.await
.map_err(|err| TaggedFiltererError::FilterChange { action: "add", err })?;
trace!("inserted filters into swaplock");
if recompile_globs {
self.recompile_globs(Op::Glob).await?;
self.recompile_globs(Op::Glob)?;
}
if recompile_not_globs {
self.recompile_globs(Op::NotGlob).await?;
self.recompile_globs(Op::NotGlob)?;
}
Ok(())
}
async fn recompile_globs(&self, op_filter: Op) -> Result<(), TaggedFiltererError> {
fn recompile_globs(&self, op_filter: Op) -> Result<(), TaggedFiltererError> {
trace!(?op_filter, "recompiling globs");
let target = match op_filter {
Op::Glob => &self.glob_compiled,
@ -477,7 +482,6 @@ impl TaggedFilterer {
trace!(?op_filter, "no filters, erasing compiled glob");
return target
.replace(None)
.await
.map_err(TaggedFiltererError::GlobsetChange);
}
};
@ -502,7 +506,6 @@ impl TaggedFilterer {
trace!(?op_filter, "swapping in new compiled glob");
target
.replace(Some(compiled))
.await
.map_err(TaggedFiltererError::GlobsetChange)
}
@ -516,7 +519,6 @@ impl TaggedFilterer {
.map_err(TaggedFiltererError::Ignore)?;
self.ignore_filterer
.replace(new)
.await
.map_err(TaggedFiltererError::IgnoreSwap)?;
Ok(())
}
@ -524,18 +526,17 @@ impl TaggedFilterer {
/// Clears all filters from the filterer.
///
/// This also recompiles the glob matchers, so essentially it resets the entire filterer state.
pub async fn clear_filters(&self) -> Result<(), TaggedFiltererError> {
pub fn clear_filters(&self) -> Result<(), TaggedFiltererError> {
debug!("removing all filters from filterer");
self.filters
.replace(Default::default())
.await
.map_err(|err| TaggedFiltererError::FilterChange {
self.filters.replace(Default::default()).map_err(|err| {
TaggedFiltererError::FilterChange {
action: "clear all",
err,
})?;
}
})?;
self.recompile_globs(Op::Glob).await?;
self.recompile_globs(Op::NotGlob).await?;
self.recompile_globs(Op::Glob)?;
self.recompile_globs(Op::NotGlob)?;
Ok(())
}

View File

@ -45,7 +45,7 @@ impl FromStr for Filter {
"process" | "pid" => Ok(Matcher::Process),
"signal" | "sig" => Ok(Matcher::Signal),
"complete" | "exit" => Ok(Matcher::ProcessCompletion),
m => Err(format!("unknown matcher: {}", m)),
m => Err(format!("unknown matcher: {m}")),
},
)(i)
}
@ -73,7 +73,7 @@ impl FromStr for Filter {
":=" => Ok(Op::InSet),
":!" => Ok(Op::NotInSet),
"=" => Ok(Op::Auto),
o => Err(format!("unknown op: `{}`", o)),
o => Err(format!("unknown op: `{o}`")),
},
)(i)
}

View File

@ -33,7 +33,7 @@ where
///
/// This obtains a clone of the value, and then calls the closure with a mutable reference to
/// it. Once the closure returns, the value is swapped in.
pub async fn change(&self, f: impl FnOnce(&mut T)) -> Result<(), SendError<T>> {
pub fn change(&self, f: impl FnOnce(&mut T)) -> Result<(), SendError<T>> {
let mut new = { self.r.borrow().clone() };
f(&mut new);
@ -41,7 +41,7 @@ where
}
/// Replace the value with a new one.
pub async fn replace(&self, new: T) -> Result<(), SendError<T>> {
pub fn replace(&self, new: T) -> Result<(), SendError<T>> {
self.s.send(new)
}
}

View File

@ -8,7 +8,7 @@ use helpers::tagged_ff::*;
#[tokio::test]
async fn empty_filter_passes_everything() {
let filterer = filt("", &[], &[file("empty.wef")]).await;
let filterer = filt("", &[], &[file("empty.wef").await]).await;
filterer.file_does_pass("Cargo.toml");
filterer.file_does_pass("Cargo.json");
@ -35,7 +35,7 @@ async fn empty_filter_passes_everything() {
#[tokio::test]
async fn folder() {
let filterer = filt("", &[], &[file("folder.wef")]).await;
let filterer = filt("", &[], &[file("folder.wef").await]).await;
filterer.file_doesnt_pass("apples");
filterer.file_doesnt_pass("apples/oranges/bananas");
@ -54,7 +54,7 @@ async fn folder() {
#[tokio::test]
async fn patterns() {
let filterer = filt("", &[], &[file("path-patterns.wef")]).await;
let filterer = filt("", &[], &[file("path-patterns.wef").await]).await;
// Unmatched
filterer.file_does_pass("FINAL-FINAL.docx");
@ -94,7 +94,7 @@ async fn patterns() {
#[tokio::test]
async fn negate() {
let filterer = filt("", &[], &[file("negate.wef")]).await;
let filterer = filt("", &[], &[file("negate.wef").await]).await;
filterer.file_doesnt_pass("yeah");
filterer.file_does_pass("nah");
@ -103,7 +103,7 @@ async fn negate() {
#[tokio::test]
async fn ignores_and_filters() {
let filterer = filt("", &[file("globs").0], &[file("folder.wef")]).await;
let filterer = filt("", &[file("globs").await.0], &[file("folder.wef").await]).await;
// ignored
filterer.dir_doesnt_pass("test-helper");

View File

@ -8,6 +8,7 @@ use std::{
use ignore_files::{IgnoreFile, IgnoreFilter};
use project_origins::ProjectType;
use tokio::fs::canonicalize;
use watchexec::{
error::RuntimeError,
event::{filekind::FileEventKind, Event, FileType, Priority, ProcessEnd, Source, Tag},
@ -49,7 +50,7 @@ pub trait PathHarness: Filterer {
}
fn path_pass(&self, path: &str, file_type: Option<FileType>, pass: bool) {
let origin = dunce::canonicalize(".").unwrap();
let origin = std::fs::canonicalize(".").unwrap();
let full_path = if let Some(suf) = path.strip_prefix("/test/") {
origin.join(suf)
} else if Path::new(path).has_root() {
@ -207,24 +208,28 @@ fn tracing_init() {
pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> IgnoreFilter {
tracing_init();
let origin = dunce::canonicalize(".").unwrap().join(origin);
let origin = canonicalize(".").await.unwrap().join(origin);
IgnoreFilter::new(origin, ignore_files)
.await
.expect("making filterer")
}
pub async fn tagged_filt(filters: &[Filter]) -> Arc<TaggedFilterer> {
let origin = dunce::canonicalize(".").unwrap();
let origin = canonicalize(".").await.unwrap();
tracing_init();
let filterer = TaggedFilterer::new(origin.clone(), origin).expect("creating filterer");
let filterer = TaggedFilterer::new(origin.clone(), origin)
.await
.expect("creating filterer");
filterer.add_filters(filters).await.expect("adding filters");
filterer
}
pub async fn tagged_igfilt(origin: &str, ignore_files: &[IgnoreFile]) -> Arc<TaggedFilterer> {
let origin = dunce::canonicalize(".").unwrap().join(origin);
let origin = canonicalize(".").await.unwrap().join(origin);
tracing_init();
let filterer = TaggedFilterer::new(origin.clone(), origin).expect("creating filterer");
let filterer = TaggedFilterer::new(origin.clone(), origin)
.await
.expect("creating filterer");
for file in ignore_files {
tracing::info!(?file, "loading ignore file");
filterer
@ -255,8 +260,9 @@ pub async fn tagged_fffilt(
filterer
}
pub fn ig_file(name: &str) -> IgnoreFile {
let path = dunce::canonicalize(".")
pub async fn ig_file(name: &str) -> IgnoreFile {
let path = canonicalize(".")
.await
.unwrap()
.join("tests")
.join("ignores")
@ -268,8 +274,8 @@ pub fn ig_file(name: &str) -> IgnoreFile {
}
}
pub fn ff_file(name: &str) -> FilterFile {
FilterFile(ig_file(name))
pub async fn ff_file(name: &str) -> FilterFile {
FilterFile(ig_file(name).await)
}
pub trait Applies {
@ -279,7 +285,7 @@ pub trait Applies {
impl Applies for IgnoreFile {
fn applies_in(mut self, origin: &str) -> Self {
let origin = dunce::canonicalize(".").unwrap().join(origin);
let origin = std::fs::canonicalize(".").unwrap().join(origin);
self.applies_in = Some(origin);
self
}
@ -337,7 +343,7 @@ pub trait FilterExt {
impl FilterExt for Filter {
fn in_subpath(mut self, sub: impl AsRef<Path>) -> Self {
let origin = dunce::canonicalize(".").unwrap();
let origin = std::fs::canonicalize(".").unwrap();
self.in_path = Some(origin.join(sub));
self
}

View File

@ -16,13 +16,12 @@ edition = "2021"
[dependencies]
futures = "0.3.21"
ignore = "0.4.18"
git-config = "0.12.0"
tokio = { version = "1.19.2", default-features = false, features = ["fs"] }
tracing = "0.1.35"
dunce = "1.0.2"
ignore = "0.4.18"
miette = "5.3.0"
thiserror = "1.0.31"
tokio = { version = "1.19.2", default-features = false, features = ["fs"] }
tracing = "0.1.35"
[dependencies.project-origins]
version = "1.1.1"

View File

@ -7,7 +7,7 @@ use std::{
use git_config::{path::interpolate::Context as InterpolateContext, File, Path as GitPath};
use project_origins::ProjectType;
use tokio::fs::{metadata, read_dir};
use tokio::fs::{canonicalize, metadata, read_dir};
use tracing::{trace, trace_span};
use crate::{IgnoreFile, IgnoreFilter};
@ -45,7 +45,12 @@ const PATH_SEPARATOR: &str = ";";
/// return an `IgnoreFile { path: path/to/that/file, applies_in: None, applies_to: Some(ProjectType::Git) }`.
/// This is the only case in which the `applies_in` field is None from this function. When such is
/// received the global Git ignore files found by [`from_environment()`] **should be ignored**.
pub async fn from_origin(path: impl AsRef<Path>) -> (Vec<IgnoreFile>, Vec<Error>) {
///
/// ## Async
///
/// This future is not `Send` due to [`git_config`] internals.
#[allow(clippy::future_not_send)]
pub async fn from_origin(path: impl AsRef<Path> + Send) -> (Vec<IgnoreFile>, Vec<Error>) {
let base = path.as_ref().to_owned();
let mut files = Vec::new();
let mut errors = Vec::new();
@ -60,7 +65,8 @@ pub async fn from_origin(path: impl AsRef<Path>) -> (Vec<IgnoreFile>, Vec<Error>
)),
Some(Err(err)) => errors.push(Error::new(ErrorKind::Other, err)),
Some(Ok(config)) => {
if let Ok(excludes) = config.value::<GitPath<'_>>("core", None, "excludesFile") {
let config_excludes = config.value::<GitPath<'_>>("core", None, "excludesFile");
if let Ok(excludes) = config_excludes {
match excludes.interpolate(InterpolateContext {
home_dir: env::var("HOME").ok().map(PathBuf::from).as_deref(),
..Default::default()
@ -190,6 +196,11 @@ pub async fn from_origin(path: impl AsRef<Path>) -> (Vec<IgnoreFile>, Vec<Error>
/// All errors (permissions, etc) are collected and returned alongside the ignore files: you may
/// want to show them to the user while still using whatever ignores were successfully found. Errors
/// from files not being found are silently ignored (the files are just not returned).
///
/// ## Async
///
/// This future is not `Send` due to [`git_config`] internals.
#[allow(clippy::future_not_send)]
pub async fn from_environment(appname: Option<&str>) -> (Vec<IgnoreFile>, Vec<Error>) {
let mut files = Vec::new();
let mut errors = Vec::new();
@ -213,7 +224,8 @@ pub async fn from_environment(appname: Option<&str>) -> (Vec<IgnoreFile>, Vec<Er
Err(err) => errors.push(Error::new(ErrorKind::Other, err)),
Ok(Err(err)) => errors.push(Error::new(ErrorKind::Other, err)),
Ok(Ok(config)) => {
if let Ok(excludes) = config.value::<GitPath<'_>>("core", None, "excludesFile") {
let config_excludes = config.value::<GitPath<'_>>("core", None, "excludesFile");
if let Ok(excludes) = config_excludes {
match excludes.interpolate(InterpolateContext {
home_dir: env::var("HOME").ok().map(PathBuf::from).as_deref(),
..Default::default()
@ -314,6 +326,8 @@ pub async fn from_environment(appname: Option<&str>) -> (Vec<IgnoreFile>, Vec<Er
/// Utility function to handle looking for an ignore file and adding it to a list if found.
///
/// This is mostly an internal function, but it is exposed for other filterers to use.
#[allow(clippy::future_not_send)]
#[tracing::instrument(skip(files, errors), level = "trace")]
#[inline]
pub async fn discover_file(
files: &mut Vec<IgnoreFile>,
@ -322,7 +336,6 @@ pub async fn discover_file(
applies_to: Option<ProjectType>,
path: PathBuf,
) -> bool {
let _span = trace_span!("discover_file", ?path, ?applies_in, ?applies_to).entered();
match find_file(path).await {
Err(err) => {
trace!(?err, "found an error");
@ -372,7 +385,7 @@ enum Visit {
impl DirTourist {
pub async fn new(base: &Path, files: &[IgnoreFile]) -> Result<Self, Error> {
let base = dunce::canonicalize(base)?;
let base = canonicalize(base).await?;
trace!("create IgnoreFilterer for visiting directories");
let mut filter = IgnoreFilter::new(&base, files)
.await
@ -389,9 +402,8 @@ impl DirTourist {
"/.svn",
"/.pijul",
],
Some(base.clone()),
Some(&base),
)
.await
.map_err(|err| Error::new(ErrorKind::Other, err))?;
Ok(Self {
@ -403,74 +415,80 @@ impl DirTourist {
})
}
#[allow(clippy::future_not_send)]
pub async fn next(&mut self) -> Visit {
if let Some(path) = self.to_visit.pop() {
let _span = trace_span!("visit_path", ?path).entered();
if self.must_skip(&path) {
trace!("in skip list");
return Visit::Skip;
}
if !self.filter.check_dir(&path) {
trace!("path is ignored, adding to skip list");
self.skip(path);
return Visit::Skip;
}
let mut dir = match read_dir(&path).await {
Ok(dir) => dir,
Err(err) => {
trace!("failed to read dir: {}", err);
self.errors.push(err);
return Visit::Skip;
}
};
while let Some(entry) = match dir.next_entry().await {
Ok(entry) => entry,
Err(err) => {
trace!("failed to read dir entries: {}", err);
self.errors.push(err);
return Visit::Skip;
}
} {
let path = entry.path();
let _span = trace_span!("dir_entry", ?path).entered();
if self.must_skip(&path) {
trace!("in skip list");
continue;
}
match entry.file_type().await {
Ok(ft) => {
if ft.is_dir() {
if !self.filter.check_dir(&path) {
trace!("path is ignored, adding to skip list");
self.skip(path);
continue;
}
trace!("found a dir, adding to list");
self.to_visit.push(path);
} else {
trace!("not a dir");
}
}
Err(err) => {
trace!("failed to read filetype, adding to skip list: {}", err);
self.errors.push(err);
self.skip(path);
}
}
}
Visit::Find(path)
self.visit_path(path).await
} else {
Visit::Done
}
}
#[allow(clippy::future_not_send)]
#[tracing::instrument(skip(self), level = "trace")]
async fn visit_path(&mut self, path: PathBuf) -> Visit {
if self.must_skip(&path) {
trace!("in skip list");
return Visit::Skip;
}
if !self.filter.check_dir(&path) {
trace!("path is ignored, adding to skip list");
self.skip(path);
return Visit::Skip;
}
let mut dir = match read_dir(&path).await {
Ok(dir) => dir,
Err(err) => {
trace!("failed to read dir: {}", err);
self.errors.push(err);
return Visit::Skip;
}
};
while let Some(entry) = match dir.next_entry().await {
Ok(entry) => entry,
Err(err) => {
trace!("failed to read dir entries: {}", err);
self.errors.push(err);
return Visit::Skip;
}
} {
let path = entry.path();
let _span = trace_span!("dir_entry", ?path).entered();
if self.must_skip(&path) {
trace!("in skip list");
continue;
}
match entry.file_type().await {
Ok(ft) => {
if ft.is_dir() {
if !self.filter.check_dir(&path) {
trace!("path is ignored, adding to skip list");
self.skip(path);
continue;
}
trace!("found a dir, adding to list");
self.to_visit.push(path);
} else {
trace!("not a dir");
}
}
Err(err) => {
trace!("failed to read filetype, adding to skip list: {}", err);
self.errors.push(err);
self.skip(path);
}
}
}
Visit::Find(path)
}
pub fn skip(&mut self, path: PathBuf) {
let check_path = path.as_path();
self.to_visit.retain(|p| !p.starts_with(check_path));

View File

@ -39,7 +39,7 @@ impl IgnoreFilter {
///
/// Use [`empty()`](IgnoreFilterer::empty()) if you want an empty filterer,
/// or to construct one outside an async environment.
pub async fn new(origin: impl AsRef<Path>, files: &[IgnoreFile]) -> Result<Self, Error> {
pub async fn new(origin: impl AsRef<Path> + Send, files: &[IgnoreFile]) -> Result<Self, Error> {
let origin = origin.as_ref();
let _span = trace_span!("build_filterer", ?origin);
@ -113,6 +113,7 @@ impl IgnoreFilter {
}
/// Returns the number of ignores and allowlists loaded.
#[must_use]
pub fn num_ignores(&self) -> (u64, u64) {
(self.compiled.num_ignores(), self.compiled.num_whitelists())
}
@ -183,11 +184,7 @@ impl IgnoreFilter {
/// Adds some globs manually, if the builder is available.
///
/// Does nothing silently otherwise.
pub async fn add_globs(
&mut self,
globs: &[&str],
applies_in: Option<PathBuf>,
) -> Result<(), Error> {
pub fn add_globs(&mut self, globs: &[&str], applies_in: Option<&PathBuf>) -> Result<(), Error> {
if let Some(ref mut builder) = self.builder {
let _span = trace_span!("loading ignore globs", ?globs).entered();
for line in globs {
@ -197,7 +194,7 @@ impl IgnoreFilter {
trace!(?line, "adding ignore line");
builder
.add_line(applies_in.clone(), line)
.add_line(applies_in.cloned(), line)
.map_err(|err| Error::Glob { file: None, err })?;
}

View File

@ -12,7 +12,7 @@ homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.60.0"
rust-version = "1.61.0"
edition = "2021"
[dependencies]
@ -20,11 +20,11 @@ async-priority-channel = "0.1.0"
async-recursion = "1.0.0"
atomic-take = "1.0.0"
clearscreen = "1.0.9"
dunce = "1.0.2"
futures = "0.3.16"
miette = "5.3.0"
once_cell = "1.8.0"
thiserror = "1.0.26"
normalize-path = "0.2.0"
[dependencies.command-group]
version = "1.0.8"
@ -57,8 +57,5 @@ features = [
version = "0.1.26"
features = ["log"]
[target.'cfg(unix)'.dependencies]
libc = "0.2.104"
[dev-dependencies]
tracing-subscriber = "0.3.6"

View File

@ -1,7 +1,7 @@
[![Crates.io page](https://badgen.net/crates/v/watchexec)](https://crates.io/crates/watchexec)
[![API Docs](https://docs.rs/watchexec/badge.svg)][docs]
[![Crate license: Apache 2.0](https://badgen.net/badge/license/Apache%202.0)][license]
![MSRV: 1.60.0 (minor)](https://badgen.net/badge/MSRV/1.60.0%20%28minor%29/0b7261)
![MSRV: 1.61.0 (minor)](https://badgen.net/badge/MSRV/1.61.0%20%28minor%29/0b7261)
[![CI status](https://github.com/watchexec/watchexec/actions/workflows/check.yml/badge.svg)](https://github.com/watchexec/watchexec/actions/workflows/check.yml)
# Watchexec library
@ -10,7 +10,7 @@ _The library which powers [Watchexec CLI](https://watchexec.github.io) and other
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.60.0 (incurs a minor semver bump).
- Minimum Supported Rust Version: 1.61.0 (incurs a minor semver bump).
- Status: maintained.
[docs]: https://docs.rs/watchexec

View File

@ -6,6 +6,7 @@ use watchexec::{
command::Command,
config::{InitConfig, RuntimeConfig},
error::ReconfigError,
event::Event,
fs::Watcher,
signal::source::MainSignal,
ErrorHook, Watchexec,
@ -37,12 +38,12 @@ async fn main() -> Result<()> {
let mut config = config.clone();
let w = w.clone();
async move {
eprintln!("Watchexec Action: {:?}", action);
eprintln!("Watchexec Action: {action:?}");
let sigs = action
.events
.iter()
.flat_map(|event| event.signals())
.flat_map(Event::signals)
.collect::<Vec<_>>();
if sigs.iter().any(|sig| sig == &MainSignal::Interrupt) {

View File

@ -66,21 +66,25 @@ impl Default for Outcome {
impl Outcome {
/// Convenience function to create an outcome conditional on the state of the subprocess.
pub fn if_running(then: Outcome, otherwise: Outcome) -> Self {
#[must_use]
pub fn if_running(then: Self, otherwise: Self) -> Self {
Self::IfRunning(Box::new(then), Box::new(otherwise))
}
/// Convenience function to create a sequence of outcomes.
pub fn both(one: Outcome, two: Outcome) -> Self {
#[must_use]
pub fn both(one: Self, two: Self) -> Self {
Self::Both(Box::new(one), Box::new(two))
}
/// Convenience function to wait for the subprocess to complete before executing the outcome.
pub fn wait(and_then: Outcome) -> Self {
Self::Both(Box::new(Outcome::Wait), Box::new(and_then))
#[must_use]
pub fn wait(and_then: Self) -> Self {
Self::Both(Box::new(Self::Wait), Box::new(and_then))
}
/// Resolves the outcome given the current state of the subprocess.
#[must_use]
pub fn resolve(self, is_running: bool) -> Self {
match (is_running, self) {
(true, Self::IfRunning(then, _)) => then.resolve(true),

View File

@ -79,7 +79,7 @@ impl OutcomeWorker {
});
}
async fn check_gen<O>(&self, f: impl Future<Output = O>) -> Option<O> {
async fn check_gen<O>(&self, f: impl Future<Output = O> + Send) -> Option<O> {
// TODO: use a select and a notifier of some kind so it cancels tasks
if self.gencheck.load(Ordering::SeqCst) != self.gen {
warn!(when=%"pre", gen=%self.gen, "outcome worker was cycled, aborting");
@ -113,9 +113,7 @@ impl OutcomeWorker {
notry!(self.process.wait())?;
notry!(self.process.drop_inner());
}
(false, o @ Outcome::Stop)
| (false, o @ Outcome::Wait)
| (false, o @ Outcome::Signal(_)) => {
(false, o @ (Outcome::Stop | Outcome::Wait | Outcome::Signal(_))) => {
debug!(outcome=?o, "meaningless without a process, not doing anything");
}
(_, Outcome::Start) => {

View File

@ -13,8 +13,7 @@ impl ProcessHolder {
.read()
.await
.as_ref()
.map(|p| p.is_running())
.unwrap_or(false)
.map_or(false, Supervisor::is_running)
}
pub async fn is_some(&self) -> bool {

View File

@ -55,9 +55,9 @@ pub async fn worker(
trace!("out of throttle but nothing to do, resetting");
last = Instant::now();
continue;
} else {
trace!("out of throttle on recycle");
}
trace!("out of throttle on recycle");
} else {
trace!(?maxtime, "waiting for event");
let maybe_event = timeout(maxtime, events.recv()).await;
@ -116,6 +116,7 @@ pub async fn worker(
trace!("out of throttle, starting action process");
last = Instant::now();
#[allow(clippy::iter_with_drain)]
let events = Arc::from(set.drain(..).collect::<Vec<_>>().into_boxed_slice());
let action = Action::new(Arc::clone(&events));
debug!(?action, "action constructed");
@ -130,7 +131,7 @@ pub async fn worker(
let err = action_handler
.call(action)
.await
.map_err(|e| rte("action worker", e));
.map_err(|e| rte("action worker", e.as_ref()));
if let Err(err) = err {
errors.send(err).await?;
debug!("action handler errored, skipping");

View File

@ -88,13 +88,13 @@ impl Command {
trace!(cmd=?self, "constructing command");
match self {
Command::Exec { prog, args } => {
Self::Exec { prog, args } => {
let mut c = TokioCommand::new(prog);
c.args(args);
Ok(c)
}
Command::Shell {
Self::Shell {
shell,
args,
command,
@ -133,16 +133,16 @@ impl Command {
impl fmt::Display for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Command::Exec { prog, args } => {
write!(f, "{}", prog)?;
Self::Exec { prog, args } => {
write!(f, "{prog}")?;
for arg in args {
write!(f, " {}", arg)?;
write!(f, " {arg}")?;
}
Ok(())
}
Command::Shell { command, .. } => {
write!(f, "{}", command)
Self::Shell { command, .. } => {
write!(f, "{command}")
}
}
}

View File

@ -25,7 +25,7 @@ pub enum Process {
impl Default for Process {
/// Returns [`Process::None`].
fn default() -> Self {
Process::None
Self::None
}
}

View File

@ -160,7 +160,7 @@ impl Supervisor {
let event = Event {
tags: vec![
Tag::Source(Source::Internal),
Tag::ProcessCompletion(status.map(|s| s.into())),
Tag::ProcessCompletion(status.map(Into::into)),
],
metadata: Default::default(),
};
@ -214,7 +214,7 @@ impl Supervisor {
/// when the signal has been delivered.
pub async fn signal(&self, signal: SubSignal) {
if cfg!(windows) {
if let SubSignal::ForceStop = signal {
if signal == SubSignal::ForceStop {
self.intervene.send(Intervention::Kill).await.ok();
}
// else: https://github.com/watchexec/watchexec/issues/219
@ -293,7 +293,7 @@ async fn spawn_process(
pre_spawn_handler
.call(pre_spawn)
.await
.map_err(|e| rte("action pre-spawn", e))?;
.map_err(|e| rte("action pre-spawn", e.as_ref()))?;
let (proc, id, post_spawn) = span.in_scope::<_, Result<_, RuntimeError>>(|| {
let mut spawnable = Arc::try_unwrap(spawnable)
@ -337,7 +337,7 @@ async fn spawn_process(
post_spawn_handler
.call(post_spawn)
.await
.map_err(|e| rte("action post-spawn", e))?;
.map_err(|e| rte("action post-spawn", e.as_ref()))?;
Ok((proc, id))
}

View File

@ -9,7 +9,7 @@ async fn unix_shell_none() -> Result<(), std::io::Error> {
args: vec!["hi".into()]
}
.to_spawnable()
.unwrap()
.expect("echo directly")
.group_status()
.await?
.success());
@ -25,7 +25,7 @@ async fn unix_shell_sh() -> Result<(), std::io::Error> {
command: "echo hi".into()
}
.to_spawnable()
.unwrap()
.expect("echo with sh")
.group_status()
.await?
.success());
@ -41,7 +41,7 @@ async fn unix_shell_alternate() -> Result<(), std::io::Error> {
command: "echo hi".into()
}
.to_spawnable()
.unwrap()
.expect("echo with bash")
.group_status()
.await?
.success());
@ -57,7 +57,7 @@ async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
command: "echo hi".into()
}
.to_spawnable()
.unwrap()
.expect("echo with shopts")
.group_status()
.await?
.success());

View File

@ -25,8 +25,8 @@ use crate::{
/// Another advantage of using the convenience methods is that each one contains a call to the
/// [`debug!`] macro, providing insight into what config your application sets for "free".
///
/// You should see the detailed documentation on [fs::WorkingData][crate::fs::WorkingData] and
/// [action::WorkingData][crate::action::WorkingData] for important information and particulars
/// You should see the detailed documentation on [`fs::WorkingData`][crate::fs::WorkingData] and
/// [`action::WorkingData`][crate::action::WorkingData] for important information and particulars
/// about each field, especially the handlers.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]

View File

@ -71,15 +71,16 @@ pub enum Tag {
impl Tag {
/// The name of the variant.
#[must_use]
pub const fn discriminant_name(&self) -> &'static str {
match self {
Tag::Path { .. } => "Path",
Tag::FileEventKind(_) => "FileEventKind",
Tag::Source(_) => "Source",
Tag::Keyboard(_) => "Keyboard",
Tag::Process(_) => "Process",
Tag::Signal(_) => "Signal",
Tag::ProcessCompletion(_) => "ProcessCompletion",
Self::Path { .. } => "Path",
Self::FileEventKind(_) => "FileEventKind",
Self::Source(_) => "Source",
Self::Keyboard(_) => "Keyboard",
Self::Process(_) => "Process",
Self::Signal(_) => "Signal",
Self::ProcessCompletion(_) => "ProcessCompletion",
}
}
}
@ -164,39 +165,24 @@ pub enum ProcessEnd {
}
impl From<ExitStatus> for ProcessEnd {
#[cfg(target_os = "fuchsia")]
fn from(es: ExitStatus) -> Self {
// Once https://github.com/rust-lang/rust/pull/88300 (unix_process_wait_more) lands, use
// that API instead of doing the transmute, and clean up the forbid condition at crate root.
let raw: i64 = unsafe { std::mem::transmute(es) };
NonZeroI64::try_from(raw)
.map(Self::ExitError)
.unwrap_or(Self::Success)
}
#[cfg(all(unix, not(target_os = "fuchsia")))]
#[cfg(unix)]
fn from(es: ExitStatus) -> Self {
use std::os::unix::process::ExitStatusExt;
match (es.code(), es.signal()) {
(Some(_), Some(_)) => {
match (es.code(), es.signal(), es.stopped_signal()) {
(Some(_), Some(_), _) => {
unreachable!("exitstatus cannot both be code and signal?!")
}
(Some(code), None) => match NonZeroI64::try_from(i64::from(code)) {
Ok(code) => Self::ExitError(code),
Err(_) => Self::Success,
},
// TODO: once unix_process_wait_more lands, use stopped_signal() instead and clear the libc dep
(None, Some(signal)) if libc::WIFSTOPPED(-signal) => {
match NonZeroI32::try_from(libc::WSTOPSIG(-signal)) {
Ok(signal) => Self::ExitStop(signal),
Err(_) => Self::Success,
}
(Some(code), None, _) => {
NonZeroI64::try_from(i64::from(code)).map_or(Self::Success, Self::ExitError)
}
(None, Some(_), Some(stopsig)) => {
NonZeroI32::try_from(stopsig).map_or(Self::Success, Self::ExitStop)
}
// TODO: once unix_process_wait_more lands, use continued() instead and clear the libc dep
#[cfg(not(target_os = "vxworks"))]
(None, Some(signal)) if libc::WIFCONTINUED(-signal) => Self::Continued,
(None, Some(signal)) => Self::ExitSignal(signal.into()),
(None, None) => Self::Success,
(None, Some(_), _) if es.continued() => Self::Continued,
(None, Some(signal), _) => Self::ExitSignal(signal.into()),
(None, None, _) => Self::Success,
}
}
@ -302,6 +288,7 @@ impl Default for Priority {
impl Event {
/// Returns true if the event has an Internal source tag.
#[must_use]
pub fn is_internal(&self) -> bool {
self.tags
.iter()
@ -309,6 +296,7 @@ impl Event {
}
/// Returns true if the event has no tags.
#[must_use]
pub fn is_empty(&self) -> bool {
self.tags.is_empty()
}
@ -346,16 +334,16 @@ impl fmt::Display for Event {
Tag::Path { path, file_type } => {
write!(f, " path={}", path.display())?;
if let Some(ft) = file_type {
write!(f, " filetype={}", ft)?;
write!(f, " filetype={ft}")?;
}
}
Tag::FileEventKind(kind) => write!(f, " kind={:?}", kind)?,
Tag::Source(s) => write!(f, " source={:?}", s)?,
Tag::Keyboard(k) => write!(f, " keyboard={:?}", k)?,
Tag::Process(p) => write!(f, " process={}", p)?,
Tag::Signal(s) => write!(f, " signal={:?}", s)?,
Tag::FileEventKind(kind) => write!(f, " kind={kind:?}")?,
Tag::Source(s) => write!(f, " source={s:?}")?,
Tag::Keyboard(k) => write!(f, " keyboard={k:?}")?,
Tag::Process(p) => write!(f, " process={p}")?,
Tag::Signal(s) => write!(f, " signal={s:?}")?,
Tag::ProcessCompletion(None) => write!(f, " command-completed")?,
Tag::ProcessCompletion(Some(c)) => write!(f, " command-completed({:?})", c)?,
Tag::ProcessCompletion(Some(c)) => write!(f, " command-completed({c:?})")?,
}
}

View File

@ -30,6 +30,6 @@ impl Filterer for () {
impl<T: Filterer> Filterer for Arc<T> {
fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> {
Arc::as_ref(self).check_event(event, priority)
Self::as_ref(self).check_event(event, priority)
}
}

View File

@ -9,9 +9,10 @@ use std::{
};
use async_priority_channel as priority;
use normalize_path::NormalizePath;
use notify::{Config, Watcher as _};
use tokio::sync::{mpsc, watch};
use tracing::{debug, error, trace, warn};
use tracing::{debug, error, trace};
use crate::{
error::{CriticalError, FsWatcherError, RuntimeError},
@ -131,8 +132,8 @@ impl AsRef<Path> for WatchedPath {
/// _not_ to drop the watch sender: this will cause the worker to stop gracefully, which may not be
/// what was expected.
///
/// Note that the paths emitted by the watcher are canonicalised. No guarantee is made about the
/// implementation or output of that canonicalisation (i.e. it might not be `std`'s).
/// Note that the paths emitted by the watcher are normalised. No guarantee is made about the
/// implementation or output of that normalisation (it may change without notice).
///
/// # Examples
///
@ -188,13 +189,13 @@ pub async fn worker(
} else {
let mut to_watch = Vec::with_capacity(data.pathset.len());
let mut to_drop = Vec::with_capacity(pathset.len());
for path in data.pathset.iter() {
for path in &data.pathset {
if !pathset.contains(path) {
to_watch.push(path.clone());
}
}
for path in pathset.iter() {
for path in &pathset {
if !data.pathset.contains(path) {
to_drop.push(path.clone());
}
@ -210,7 +211,7 @@ pub async fn worker(
let n_events = events.clone();
match kind.create(move |nev: Result<notify::Event, notify::Error>| {
trace!(event = ?nev, "receiving possible event from watcher");
if let Err(e) = process_event(nev, kind, n_events.clone()) {
if let Err(e) = process_event(nev, kind, &n_events) {
n_errors.try_send(e).ok();
}
}) {
@ -296,7 +297,7 @@ fn notify_multi_path_errors(
fn process_event(
nev: Result<notify::Event, notify::Error>,
kind: Watcher,
n_events: priority::Sender<Event, Priority>,
n_events: &priority::Sender<Event, Priority>,
) -> Result<(), RuntimeError> {
let nev = nev.map_err(|err| RuntimeError::FsWatcher {
kind,
@ -311,10 +312,7 @@ fn process_event(
// possibly pull file_type from whatever notify (or the native driver) returns?
tags.push(Tag::Path {
file_type: metadata(&path).ok().map(|m| m.file_type().into()),
path: dunce::canonicalize(&path).unwrap_or_else(|err| {
warn!(?err, ?path, "failed to canonicalise event path");
path
}),
path: path.normalize(),
});
}

View File

@ -100,8 +100,12 @@ pub trait Handler<T> {
///
/// Internally this is a Tokio [`Mutex`].
pub struct HandlerLock<T>(Arc<Mutex<Box<dyn Handler<T> + Send>>>);
impl<T> HandlerLock<T> {
impl<T> HandlerLock<T>
where
T: Send,
{
/// Wrap a [`Handler`] into a lock.
#[must_use]
pub fn new(handler: Box<dyn Handler<T> + Send>) -> Self {
Self(Arc::new(Mutex::new(handler)))
}
@ -125,13 +129,16 @@ impl<T> Clone for HandlerLock<T> {
}
}
impl<T> Default for HandlerLock<T> {
impl<T> Default for HandlerLock<T>
where
T: Send,
{
fn default() -> Self {
Self::new(Box::new(()))
}
}
pub(crate) fn rte(ctx: &'static str, err: Box<dyn Error>) -> RuntimeError {
pub(crate) fn rte(ctx: &'static str, err: &dyn Error) -> RuntimeError {
RuntimeError::Handler {
ctx,
err: err.to_string(),
@ -239,7 +246,7 @@ where
W: Write,
{
fn handle(&mut self, data: T) -> Result<(), Box<dyn Error>> {
writeln!(self.0, "{:?}", data).map_err(|e| Box::new(e) as _)
writeln!(self.0, "{data:?}").map_err(|e| Box::new(e) as _)
}
}
@ -252,6 +259,6 @@ where
W: Write,
{
fn handle(&mut self, data: T) -> Result<(), Box<dyn Error>> {
writeln!(self.0, "{}", data).map_err(|e| Box::new(e) as _)
writeln!(self.0, "{data}").map_err(|e| Box::new(e) as _)
}
}

View File

@ -96,8 +96,7 @@
#![doc(html_logo_url = "https://watchexec.github.io/logo:watchexec.svg")]
#![warn(clippy::unwrap_used, missing_docs)]
#![deny(rust_2018_idioms)]
#![cfg_attr(not(target_os = "fuchsia"), forbid(unsafe_code))]
// see event::ProcessEnd for why this is disabled on fuchsia
#![forbid(unsafe_code)]
// the toolkit to make your own
pub mod action;

View File

@ -129,15 +129,14 @@ pub fn summarise_events_to_env<'events>(
_ => "OTHERWISE_CHANGED",
})
.or_insert_with(HashSet::new)
.extend(paths.into_iter().map(|p| {
if let Some(suffix) = common_path
.extend(paths.into_iter().map(|ref p| {
common_path
.as_ref()
.and_then(|prefix| p.strip_prefix(prefix).ok())
{
suffix.as_os_str().to_owned()
} else {
p.into_os_string()
}
.map_or_else(
|| p.clone().into_os_string(),
|suffix| suffix.as_os_str().to_owned(),
)
}));
}

View File

@ -114,6 +114,7 @@ impl SubSignal {
/// This will return `None` if the signal is not supported on the current platform (only for
/// [`Custom`][SubSignal::Custom], as the first-class ones are always supported).
#[cfg(unix)]
#[must_use]
pub fn to_nix(self) -> Option<NixSignal> {
match self {
Self::Hangup => Some(NixSignal::SIGHUP),
@ -129,6 +130,8 @@ impl SubSignal {
/// Converts from a [`nix::Signal`][command_group::Signal].
#[cfg(unix)]
#[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn from_nix(sig: NixSignal) -> Self {
match sig {
NixSignal::SIGHUP => Self::Hangup,

View File

@ -204,7 +204,7 @@ async fn error_hook(
let crit = hook.critical.clone();
if let Err(err) = handler.handle(hook) {
error!(%err, "error while handling error");
let rehook = ErrorHook::new(rte("error hook", err));
let rehook = ErrorHook::new(rte("error hook", err.as_ref()));
let recrit = rehook.critical.clone();
handler.handle(rehook).unwrap_or_else(|err| {
error!(%err, "error while handling error of handling error");
@ -248,17 +248,16 @@ impl ErrorHook {
) -> Result<(), CriticalError> {
match Arc::try_unwrap(crit) {
Err(err) => {
error!(?err, "{} hook has an outstanding ref", name);
error!(?err, "{name} hook has an outstanding ref");
Ok(())
}
Ok(crit) => {
if let Some(crit) = crit.into_inner() {
debug!(%crit, "{} output a critical error", name);
Ok(crit) => crit.into_inner().map_or_else(
|| Ok(()),
|crit| {
debug!(%crit, "{name} output a critical error");
Err(crit)
} else {
Ok(())
}
}
},
),
}
}

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, ffi::OsString};
use std::{collections::HashMap, ffi::OsString, path::MAIN_SEPARATOR};
use notify::event::CreateKind;
use watchexec::{
@ -12,7 +12,7 @@ const ENV_SEP: &str = ":";
const ENV_SEP: &str = ";";
fn ospath(path: &str) -> OsString {
let root = dunce::canonicalize(".").unwrap();
let root = std::fs::canonicalize(".").unwrap();
if path.is_empty() {
root
} else {
@ -171,11 +171,13 @@ fn single_type_multipath() {
(
"CREATED",
OsString::from(
"".to_string()
+ "deeper/sub/folder.txt"
+ ENV_SEP + "dom/folder.txt"
+ ENV_SEP + "root.txt" + ENV_SEP
+ "sub/folder.txt"
[
format!("deeper{MAIN_SEPARATOR}sub{MAIN_SEPARATOR}folder.txt"),
format!("dom{MAIN_SEPARATOR}folder.txt"),
"root.txt".to_string(),
format!("sub{MAIN_SEPARATOR}folder.txt"),
]
.join(ENV_SEP)
)
),
("COMMON", ospath("")),
@ -194,7 +196,13 @@ fn single_type_divergent_paths() {
HashMap::from([
(
"CREATED",
OsString::from("".to_string() + "dom/folder.txt" + ENV_SEP + "sub/folder.txt")
OsString::from(
[
format!("dom{MAIN_SEPARATOR}folder.txt"),
format!("sub{MAIN_SEPARATOR}folder.txt"),
]
.join(ENV_SEP)
)
),
("COMMON", ospath("")),
])
@ -218,11 +226,22 @@ fn multitype_multipath() {
HashMap::from([
(
"CREATED",
OsString::from("".to_string() + "root.txt" + ENV_SEP + "sibling.txt"),
OsString::from(["root.txt", "sibling.txt"].join(ENV_SEP)),
),
(
"META_CHANGED",
OsString::from(format!("sub{MAIN_SEPARATOR}folder.txt"))
),
(
"REMOVED",
OsString::from(format!("dom{MAIN_SEPARATOR}folder.txt"))
),
(
"OTHERWISE_CHANGED",
OsString::from(format!(
"deeper{MAIN_SEPARATOR}sub{MAIN_SEPARATOR}folder.txt"
))
),
("META_CHANGED", OsString::from("sub/folder.txt"),),
("REMOVED", OsString::from("dom/folder.txt"),),
("OTHERWISE_CHANGED", OsString::from("deeper/sub/folder.txt"),),
("COMMON", ospath("")),
])
);
@ -249,7 +268,7 @@ fn multiple_paths_in_one_event() {
HashMap::from([
(
"OTHERWISE_CHANGED",
OsString::from("".to_string() + "one.txt" + ENV_SEP + "two.txt")
OsString::from(String::new() + "one.txt" + ENV_SEP + "two.txt")
),
("COMMON", ospath("")),
])
@ -275,7 +294,7 @@ fn mixed_non_paths_events() {
HashMap::from([
(
"OTHERWISE_CHANGED",
OsString::from("".to_string() + "one.txt" + ENV_SEP + "two.txt")
OsString::from(String::new() + "one.txt" + ENV_SEP + "two.txt")
),
("COMMON", ospath("")),
])
@ -312,7 +331,7 @@ fn multipath_is_sorted() {
(
"OTHERWISE_CHANGED",
OsString::from(
"".to_string()
String::new()
+ "0123.txt" + ENV_SEP + "a.txt"
+ ENV_SEP + "b.txt" + ENV_SEP
+ "c.txt" + ENV_SEP + "ᄁ.txt"
@ -342,7 +361,7 @@ fn multipath_is_deduped() {
(
"OTHERWISE_CHANGED",
OsString::from(
"".to_string()
String::new()
+ "0123.txt" + ENV_SEP + "a.txt"
+ ENV_SEP + "b.txt" + ENV_SEP
+ "c.txt" + ENV_SEP + "ᄁ.txt"

View File

@ -20,6 +20,5 @@ tokio = { version = "1.19.2", features = ["fs"] }
tokio-stream = { version = "0.1.9", features = ["fs"] }
[dev-dependencies]
dunce = "1.0.2"
miette = "5.3.0"
tracing-subscriber = "0.3.11"

View File

@ -9,7 +9,7 @@ async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let first_arg = args().nth(1).unwrap_or_else(|| ".".to_string());
let path = dunce::canonicalize(first_arg).into_diagnostic()?;
let path = tokio::fs::canonicalize(first_arg).await.into_diagnostic()?;
for origin in origins(&path).await {
println!("{}", origin.display());

View File

@ -152,7 +152,8 @@ pub enum ProjectType {
impl ProjectType {
/// Returns true if the project type is a VCS.
pub fn is_vcs(self) -> bool {
#[must_use]
pub const fn is_vcs(self) -> bool {
matches!(
self,
Self::Bazaar
@ -163,7 +164,8 @@ impl ProjectType {
}
/// Returns true if the project type is a software suite.
pub fn is_soft(self) -> bool {
#[must_use]
pub const fn is_soft(self) -> bool {
matches!(
self,
Self::Bundler
@ -187,10 +189,8 @@ impl ProjectType {
///
/// This looks at a wider variety of files than the [`types`] function does: something can be
/// detected as an origin but not be able to match to any particular [`ProjectType`].
pub async fn origins(path: impl AsRef<Path>) -> HashSet<PathBuf> {
let mut origins = HashSet::new();
fn check_list(list: DirList) -> bool {
pub async fn origins(path: impl AsRef<Path> + Send) -> HashSet<PathBuf> {
fn check_list(list: &DirList) -> bool {
if list.is_empty() {
return false;
}
@ -252,14 +252,17 @@ pub async fn origins(path: impl AsRef<Path>) -> HashSet<PathBuf> {
.any(|f| f)
}
let mut current = path.as_ref();
if check_list(DirList::obtain(current).await) {
let mut origins = HashSet::new();
let path = path.as_ref();
let mut current = path;
if check_list(&DirList::obtain(current).await) {
origins.insert(current.to_owned());
}
while let Some(parent) = current.parent() {
current = parent;
if check_list(DirList::obtain(current).await) {
if check_list(&DirList::obtain(current).await) {
origins.insert(current.to_owned());
continue;
}
@ -277,8 +280,9 @@ pub async fn origins(path: impl AsRef<Path>) -> HashSet<PathBuf> {
///
/// Note that this only detects project types listed in the [`ProjectType`] enum, and may not detect
/// anything for some paths returned by [`origins()`].
pub async fn types(path: impl AsRef<Path>) -> HashSet<ProjectType> {
let list = DirList::obtain(path.as_ref()).await;
pub async fn types(path: impl AsRef<Path> + Send) -> HashSet<ProjectType> {
let path = path.as_ref();
let list = DirList::obtain(path).await;
[
list.if_has_dir("_darcs", ProjectType::Darcs),
list.if_has_dir(".bzr", ProjectType::Bazaar),
@ -353,13 +357,13 @@ impl DirList {
#[inline]
fn has_file(&self, name: impl AsRef<Path>) -> bool {
let name = name.as_ref();
self.0.get(name).map(|x| x.is_file()).unwrap_or(false)
self.0.get(name).map_or(false, std::fs::FileType::is_file)
}
#[inline]
fn has_dir(&self, name: impl AsRef<Path>) -> bool {
let name = name.as_ref();
self.0.get(name).map(|x| x.is_dir()).unwrap_or(false)
self.0.get(name).map_or(false, std::fs::FileType::is_dir)
}
#[inline]