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_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true" CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
check: clippy:
strategy: strategy:
fail-fast: false
matrix: matrix:
platform: platform:
- ubuntu - ubuntu
@ -29,9 +34,8 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Configure toolchain - name: Configure toolchain
run: | run: |
rustup toolchain install --profile minimal --no-self-update nightly rustup toolchain install stable --profile minimal --no-self-update --component clippy
rustup default nightly rustup default stable
rustup component add clippy
# https://github.com/actions/cache/issues/752 # https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }} - if: ${{ runner.os == 'Windows' }}
@ -54,5 +58,5 @@ jobs:
${{ runner.os }}-cargo- ${{ runner.os }}-cargo-
- name: Clippy - 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_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true" CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
test: test:
strategy: strategy:
@ -23,7 +27,7 @@ jobs:
- windows - windows
toolchain: toolchain:
- stable - stable
- 1.60.0 - 1.61.0
name: Test ${{ matrix.platform }} with Rust ${{ matrix.toolchain }} name: Test ${{ matrix.platform }} with Rust ${{ matrix.toolchain }}
runs-on: "${{ matrix.platform }}-latest" runs-on: "${{ matrix.platform }}-latest"
@ -32,7 +36,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Configure toolchain - name: Configure toolchain
run: | 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 }} rustup default ${{ matrix.toolchain }}
# https://github.com/actions/cache/issues/752 # https://github.com/actions/cache/issues/752
@ -43,20 +47,60 @@ jobs:
echo "Adding GNU tar to PATH" echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- name: Configure caching - name: Cargo caching
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: | path: |
~/.cargo/registry/index/ ~/.cargo/registry/index/
~/.cargo/registry/cache/ ~/.cargo/registry/cache/
~/.cargo/git/db/ ~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-cargo-${{ matrix.toolchain }}- ${{ runner.os }}-cargo-${{ matrix.toolchain }}-
${{ runner.os }}-cargo- ${{ 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 - name: Run test suite
run: cargo test run: cargo test
- name: Check that CLI runs - name: Check that CLI runs
run: cargo run -p watchexec-cli -- -1 echo 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", "winapi",
] ]
[[package]]
name = "dunce"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541"
[[package]] [[package]]
name = "either" name = "either"
version = "1.8.0" version = "1.8.0"
@ -642,9 +636,9 @@ dependencies = [
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -657,9 +651,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -667,15 +661,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -684,9 +678,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
@ -705,9 +699,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -716,21 +710,21 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.24" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -1139,7 +1133,6 @@ dependencies = [
name = "ignore-files" name = "ignore-files"
version = "1.0.1" version = "1.0.1"
dependencies = [ dependencies = [
"dunce",
"futures", "futures",
"git-config", "git-config",
"ignore", "ignore",
@ -1461,6 +1454,12 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "normalize-path"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf22e319b2e3cb517350572e3b70c6822e0a520abfb5c78f690e829a73e8d9f2"
[[package]] [[package]]
name = "notify" name = "notify"
version = "5.0.0" version = "5.0.0"
@ -1736,7 +1735,6 @@ dependencies = [
name = "project-origins" name = "project-origins"
version = "1.1.1" version = "1.1.1"
dependencies = [ dependencies = [
"dunce",
"futures", "futures",
"miette", "miette",
"tokio", "tokio",
@ -2658,11 +2656,10 @@ dependencies = [
"atomic-take", "atomic-take",
"clearscreen", "clearscreen",
"command-group", "command-group",
"dunce",
"futures", "futures",
"ignore-files", "ignore-files",
"libc",
"miette", "miette",
"normalize-path",
"notify", "notify",
"once_cell", "once_cell",
"project-origins", "project-origins",
@ -2679,7 +2676,6 @@ dependencies = [
"clap", "clap",
"console-subscriber", "console-subscriber",
"dirs 4.0.0", "dirs 4.0.0",
"dunce",
"embed-resource", "embed-resource",
"futures", "futures",
"ignore-files", "ignore-files",
@ -2699,7 +2695,6 @@ dependencies = [
name = "watchexec-filterer-globset" name = "watchexec-filterer-globset"
version = "1.0.1" version = "1.0.1"
dependencies = [ dependencies = [
"dunce",
"ignore", "ignore",
"ignore-files", "ignore-files",
"project-origins", "project-origins",
@ -2714,7 +2709,6 @@ dependencies = [
name = "watchexec-filterer-ignore" name = "watchexec-filterer-ignore"
version = "1.0.0" version = "1.0.0"
dependencies = [ dependencies = [
"dunce",
"ignore", "ignore",
"ignore-files", "ignore-files",
"project-origins", "project-origins",
@ -2728,7 +2722,7 @@ dependencies = [
name = "watchexec-filterer-tagged" name = "watchexec-filterer-tagged"
version = "0.1.1" version = "0.1.1"
dependencies = [ dependencies = [
"dunce", "futures",
"globset", "globset",
"ignore", "ignore",
"ignore-files", "ignore-files",

View File

@ -22,7 +22,6 @@ path = "src/main.rs"
[dependencies] [dependencies]
console-subscriber = { version = "0.1.0", optional = true } console-subscriber = { version = "0.1.0", optional = true }
dirs = "4.0.0" dirs = "4.0.0"
dunce = "1.0.2"
futures = "0.3.17" futures = "0.3.17"
miette = { version = "5.3.0", features = ["fancy"] } miette = { version = "5.3.0", features = ["fancy"] }
notify-rust = "4.5.2" 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( let arg_file = BufReader::new(
File::open(arg_path) File::open(arg_path)
.into_diagnostic() .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 let mut more_args: Vec<OsString> = arg_file

View File

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

View File

@ -5,32 +5,31 @@ use std::{
}; };
use clap::ArgMatches; use clap::ArgMatches;
use dunce::canonicalize;
use ignore_files::IgnoreFile; use ignore_files::IgnoreFile;
use miette::{miette, IntoDiagnostic, Result}; use miette::{miette, IntoDiagnostic, Result};
use project_origins::ProjectType; use project_origins::ProjectType;
use tokio::fs::canonicalize;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use watchexec::paths::common_prefix; use watchexec::paths::common_prefix;
pub async fn dirs(args: &ArgMatches) -> Result<(PathBuf, PathBuf)> { pub async fn dirs(args: &ArgMatches) -> Result<(PathBuf, PathBuf)> {
let curdir = env::current_dir() let curdir = env::current_dir().into_diagnostic()?;
.and_then(canonicalize) let curdir = canonicalize(curdir).await.into_diagnostic()?;
.into_diagnostic()?;
debug!(?curdir, "current directory"); debug!(?curdir, "current directory");
let project_origin = if let Some(origin) = args.value_of_os("project-origin") { let project_origin = if let Some(origin) = args.value_of_os("project-origin") {
debug!(?origin, "project origin override"); debug!(?origin, "project origin override");
canonicalize(origin).into_diagnostic()? canonicalize(origin).await.into_diagnostic()?
} else { } else {
let homedir = dirs::home_dir() let homedir = match dirs::home_dir() {
.map(canonicalize) None => None,
.transpose() Some(dir) => Some(canonicalize(dir).await.into_diagnostic()?),
.into_diagnostic()?; };
debug!(?homedir, "home directory"); debug!(?homedir, "home directory");
let mut paths = HashSet::new(); let mut paths = HashSet::new();
for path in args.values_of_os("paths").unwrap_or_default() { 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)); 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) common_prefix(&origins)
.ok_or_else(|| miette!("no common prefix, but this should never fail"))?, .ok_or_else(|| miette!("no common prefix, but this should never fail"))?,
) )
.await
.into_diagnostic()? .into_diagnostic()?
}; };
info!(?project_origin, "resolved common/project origin"); 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") { if !args.is_present("no-default-ignore") {
ignores.extend([ ignores.extend([
(format!("**{s}.DS_Store", s = MAIN_SEPARATOR), None), (format!("**{MAIN_SEPARATOR}.DS_Store"), None),
(String::from("*.py[co]"), None), (String::from("*.py[co]"), None),
(String::from("#*#"), None), (String::from("#*#"), None),
(String::from(".#*"), None), (String::from(".#*"), None),
(String::from(".*.kate-swp"), None), (String::from(".*.kate-swp"), None),
(String::from(".*.sw?"), None), (String::from(".*.sw?"), None),
(String::from(".*.sw?x"), None), (String::from(".*.sw?x"), None),
(format!("**{s}.bzr{s}**", s = MAIN_SEPARATOR), None), (format!("**{MAIN_SEPARATOR}.bzr{MAIN_SEPARATOR}**"), None),
(format!("**{s}_darcs{s}**", s = 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, None,
), ),
(format!("**{s}.git{s}**", s = MAIN_SEPARATOR), None), (format!("**{MAIN_SEPARATOR}.git{MAIN_SEPARATOR}**"), None),
(format!("**{s}.hg{s}**", s = MAIN_SEPARATOR), None), (format!("**{MAIN_SEPARATOR}.hg{MAIN_SEPARATOR}**"), None),
(format!("**{s}.pijul{s}**", s = MAIN_SEPARATOR), None), (format!("**{MAIN_SEPARATOR}.pijul{MAIN_SEPARATOR}**"), None),
(format!("**{s}.svn{s}**", s = 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 vcs_types = super::common::vcs_types(&project_origin).await;
let ignores = super::common::ignores(args, &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 { for ignore in &ignores {
filterer.add_ignore_file(ignore).await?; filterer.add_ignore_file(ignore).await?;
@ -25,7 +25,7 @@ pub async fn tagged(args: &ArgMatches) -> Result<Arc<TaggedFilterer>> {
let file = FilterFile(IgnoreFile { let file = FilterFile(IgnoreFile {
applies_in: None, applies_in: None,
applies_to: None, applies_to: None,
path: dunce::canonicalize(path).into_diagnostic()?, path: tokio::fs::canonicalize(path).await.into_diagnostic()?,
}); });
filter_files.push(file); filter_files.push(file);
} }

View File

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

View File

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

View File

@ -9,7 +9,7 @@ _The default filterer implementation for Watchexec._
- **[API documentation][docs]**. - **[API documentation][docs]**.
- Licensed under [Apache 2.0][license]. - 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. - Status: maintained.
[docs]: https://docs.rs/watchexec-filterer-globset [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. /// The extensions list is used to filter files by extension.
/// ///
/// Non-path events are always passed. /// Non-path events are always passed.
#[allow(clippy::future_not_send)]
pub async fn new( pub async fn new(
origin: impl AsRef<Path>, origin: impl AsRef<Path>,
filters: impl IntoIterator<Item = (String, Option<PathBuf>)>, filters: impl IntoIterator<Item = (String, Option<PathBuf>)>,
@ -133,9 +134,7 @@ impl Filterer for GlobsetFilterer {
} else { } else {
Ok(paths.any(|(path, file_type)| { Ok(paths.any(|(path, file_type)| {
let _span = trace_span!("path", ?path).entered(); let _span = trace_span!("path", ?path).entered();
let is_dir = file_type let is_dir = file_type.map_or(false, |t| matches!(t, FileType::Dir));
.map(|t| matches!(t, FileType::Dir))
.unwrap_or(false);
if self.ignores.matched(path, is_dir).is_ignore() { if self.ignores.matched(path, is_dir).is_ignore() {
trace!("ignored by globset 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 filterer = filt(&[], &[], &["py"]).await;
let origin = dunce::canonicalize(".").unwrap(); let origin = tokio::fs::canonicalize(".").await.unwrap();
let event = Event { let event = Event {
tags: vec![ tags: vec![

View File

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

View File

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

View File

@ -9,7 +9,7 @@ _(Sub)filterer implementation for ignore files._
- **[API documentation][docs]**. - **[API documentation][docs]**.
- Licensed under [Apache 2.0][license]. - 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. - Status: maintained.
This is mostly a thin layer above the [ignore-files](../../ignore-files) crate, and is meant to be 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() { for (path, file_type) in event.paths() {
let _span = trace_span!("checking_against_compiled", ?path, ?file_type).entered(); let _span = trace_span!("checking_against_compiled", ?path, ?file_type).entered();
let is_dir = file_type let is_dir = file_type.map_or(false, |t| matches!(t, FileType::Dir));
.map(|t| matches!(t, FileType::Dir))
.unwrap_or(false);
match self.0.match_path(path, is_dir) { match self.0.match_path(path, is_dir) {
Match::None => { 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/oranges");
filterer.dir_does_pass("apples/carrots/cauliflowers/artichokes/oranges"); filterer.dir_does_pass("apples/carrots/cauliflowers/artichokes/oranges");
filterer.file_does_pass(&format!("raw-{}", name)); filterer.file_does_pass(&format!("raw-{name}"));
filterer.dir_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-{name}/carrots/cauliflowers/oranges"));
filterer.file_does_pass(&format!("raw-{}/oranges/bananas", name)); filterer.file_does_pass(&format!("raw-{name}/oranges/bananas"));
filterer.dir_does_pass(&format!("raw-{}/carrots/cauliflowers/oranges", name)); filterer.dir_does_pass(&format!("raw-{name}/carrots/cauliflowers/oranges"));
filterer.file_does_pass(&format!( filterer.file_does_pass(&format!(
"raw-{}/carrots/cauliflowers/artichokes/oranges", "raw-{}/carrots/cauliflowers/artichokes/oranges",
name name
@ -51,11 +51,11 @@ fn folders_suite(filterer: &IgnoreFilterer, name: &str) {
name name
)); ));
filterer.dir_doesnt_pass(&format!("{}/carrots/cauliflowers/oranges", name)); filterer.dir_doesnt_pass(&format!("{name}/carrots/cauliflowers/oranges"));
filterer.dir_doesnt_pass(&format!("{}/carrots/cauliflowers/artichokes/oranges", name)); filterer.dir_doesnt_pass(&format!("{name}/carrots/cauliflowers/artichokes/oranges"));
filterer.file_doesnt_pass(&format!("{}/carrots/cauliflowers/oranges", name)); filterer.file_doesnt_pass(&format!("{name}/carrots/cauliflowers/oranges"));
filterer.file_doesnt_pass(&format!("{}/carrots/cauliflowers/artichokes/oranges", name)); filterer.file_doesnt_pass(&format!("{name}/carrots/cauliflowers/artichokes/oranges"));
filterer.file_doesnt_pass(&format!("{}/oranges/bananas", name)); filterer.file_doesnt_pass(&format!("{name}/oranges/bananas"));
} }
#[tokio::test] #[tokio::test]

View File

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

View File

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

View File

@ -9,7 +9,7 @@ _Experimental filterer using tagged filters._
- **[API documentation][docs]**. - **[API documentation][docs]**.
- Licensed under [Apache 2.0][license]. - 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. - Status: maintained.
[docs]: https://docs.rs/watchexec-filterer-tagged [docs]: https://docs.rs/watchexec-filterer-tagged

View File

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

View File

@ -1,9 +1,9 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
use dunce::canonicalize;
use globset::Glob; use globset::Glob;
use regex::Regex; use regex::Regex;
use tokio::fs::canonicalize;
use tracing::{trace, warn}; use tracing::{trace, warn};
use unicase::UniCase; use unicase::UniCase;
use watchexec::event::Tag; use watchexec::event::Tag;
@ -49,7 +49,7 @@ impl Filter {
(Op::InSet, Pattern::Exact(pat)) => subject == pat, (Op::InSet, Pattern::Exact(pat)) => subject == pat,
(Op::NotInSet, Pattern::Set(set)) => !set.contains(subject), (Op::NotInSet, Pattern::Set(set)) => !set.contains(subject),
(Op::NotInSet, Pattern::Exact(pat)) => subject != pat, (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 // FIXME: someway that isn't this horrible
match Glob::new(glob) { match Glob::new(glob) {
Ok(glob) => { Ok(glob) => {
@ -86,6 +86,7 @@ impl Filter {
/// ///
/// The resulting filter matches on [`Path`][Matcher::Path], with the [`NotGlob`][Op::NotGlob] /// 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. /// 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 { 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)); 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. /// 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 { if let Some(ctx) = self.in_path {
self.in_path = self.in_path =
Some( Some(
canonicalize(&ctx).map_err(|err| TaggedFiltererError::IoError { canonicalize(&ctx)
about: "canonicalise Filter in_path", .await
err, .map_err(|err| TaggedFiltererError::IoError {
})?, about: "canonicalise Filter in_path",
err,
})?,
); );
trace!(canon=?ctx, "canonicalised in_path"); trace!(canon=?ctx, "canonicalised in_path");
} }
@ -186,13 +189,13 @@ impl Matcher {
match tag { match tag {
Tag::Path { Tag::Path {
file_type: None, .. file_type: None, ..
} => &[Matcher::Path], } => &[Self::Path],
Tag::Path { .. } => &[Matcher::Path, Matcher::FileType], Tag::Path { .. } => &[Self::Path, Self::FileType],
Tag::FileEventKind(_) => &[Matcher::FileEventKind], Tag::FileEventKind(_) => &[Self::FileEventKind],
Tag::Source(_) => &[Matcher::Source], Tag::Source(_) => &[Self::Source],
Tag::Process(_) => &[Matcher::Process], Tag::Process(_) => &[Self::Process],
Tag::Signal(_) => &[Matcher::Signal], Tag::Signal(_) => &[Self::Signal],
Tag::ProcessCompletion(_) => &[Matcher::ProcessCompletion], Tag::ProcessCompletion(_) => &[Self::ProcessCompletion],
_ => { _ => {
warn!("unhandled tag: {:?}", tag); warn!("unhandled tag: {:?}", tag);
&[] &[]

View File

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

View File

@ -45,7 +45,7 @@ impl FromStr for Filter {
"process" | "pid" => Ok(Matcher::Process), "process" | "pid" => Ok(Matcher::Process),
"signal" | "sig" => Ok(Matcher::Signal), "signal" | "sig" => Ok(Matcher::Signal),
"complete" | "exit" => Ok(Matcher::ProcessCompletion), "complete" | "exit" => Ok(Matcher::ProcessCompletion),
m => Err(format!("unknown matcher: {}", m)), m => Err(format!("unknown matcher: {m}")),
}, },
)(i) )(i)
} }
@ -73,7 +73,7 @@ impl FromStr for Filter {
":=" => Ok(Op::InSet), ":=" => Ok(Op::InSet),
":!" => Ok(Op::NotInSet), ":!" => Ok(Op::NotInSet),
"=" => Ok(Op::Auto), "=" => Ok(Op::Auto),
o => Err(format!("unknown op: `{}`", o)), o => Err(format!("unknown op: `{o}`")),
}, },
)(i) )(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 /// 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. /// 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() }; let mut new = { self.r.borrow().clone() };
f(&mut new); f(&mut new);
@ -41,7 +41,7 @@ where
} }
/// Replace the value with a new one. /// 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) self.s.send(new)
} }
} }

View File

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

View File

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

View File

@ -16,13 +16,12 @@ edition = "2021"
[dependencies] [dependencies]
futures = "0.3.21" futures = "0.3.21"
ignore = "0.4.18"
git-config = "0.12.0" git-config = "0.12.0"
tokio = { version = "1.19.2", default-features = false, features = ["fs"] } ignore = "0.4.18"
tracing = "0.1.35"
dunce = "1.0.2"
miette = "5.3.0" miette = "5.3.0"
thiserror = "1.0.31" thiserror = "1.0.31"
tokio = { version = "1.19.2", default-features = false, features = ["fs"] }
tracing = "0.1.35"
[dependencies.project-origins] [dependencies.project-origins]
version = "1.1.1" 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 git_config::{path::interpolate::Context as InterpolateContext, File, Path as GitPath};
use project_origins::ProjectType; use project_origins::ProjectType;
use tokio::fs::{metadata, read_dir}; use tokio::fs::{canonicalize, metadata, read_dir};
use tracing::{trace, trace_span}; use tracing::{trace, trace_span};
use crate::{IgnoreFile, IgnoreFilter}; 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) }`. /// 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 /// 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**. /// 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 base = path.as_ref().to_owned();
let mut files = Vec::new(); let mut files = Vec::new();
let mut errors = 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(Err(err)) => errors.push(Error::new(ErrorKind::Other, err)),
Some(Ok(config)) => { 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 { match excludes.interpolate(InterpolateContext {
home_dir: env::var("HOME").ok().map(PathBuf::from).as_deref(), home_dir: env::var("HOME").ok().map(PathBuf::from).as_deref(),
..Default::default() ..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 /// 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 /// 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). /// 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>) { pub async fn from_environment(appname: Option<&str>) -> (Vec<IgnoreFile>, Vec<Error>) {
let mut files = Vec::new(); let mut files = Vec::new();
let mut errors = 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)), Err(err) => errors.push(Error::new(ErrorKind::Other, err)),
Ok(Err(err)) => errors.push(Error::new(ErrorKind::Other, err)), Ok(Err(err)) => errors.push(Error::new(ErrorKind::Other, err)),
Ok(Ok(config)) => { 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 { match excludes.interpolate(InterpolateContext {
home_dir: env::var("HOME").ok().map(PathBuf::from).as_deref(), home_dir: env::var("HOME").ok().map(PathBuf::from).as_deref(),
..Default::default() ..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. /// 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. /// 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] #[inline]
pub async fn discover_file( pub async fn discover_file(
files: &mut Vec<IgnoreFile>, files: &mut Vec<IgnoreFile>,
@ -322,7 +336,6 @@ pub async fn discover_file(
applies_to: Option<ProjectType>, applies_to: Option<ProjectType>,
path: PathBuf, path: PathBuf,
) -> bool { ) -> bool {
let _span = trace_span!("discover_file", ?path, ?applies_in, ?applies_to).entered();
match find_file(path).await { match find_file(path).await {
Err(err) => { Err(err) => {
trace!(?err, "found an error"); trace!(?err, "found an error");
@ -372,7 +385,7 @@ enum Visit {
impl DirTourist { impl DirTourist {
pub async fn new(base: &Path, files: &[IgnoreFile]) -> Result<Self, Error> { 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"); trace!("create IgnoreFilterer for visiting directories");
let mut filter = IgnoreFilter::new(&base, files) let mut filter = IgnoreFilter::new(&base, files)
.await .await
@ -389,9 +402,8 @@ impl DirTourist {
"/.svn", "/.svn",
"/.pijul", "/.pijul",
], ],
Some(base.clone()), Some(&base),
) )
.await
.map_err(|err| Error::new(ErrorKind::Other, err))?; .map_err(|err| Error::new(ErrorKind::Other, err))?;
Ok(Self { Ok(Self {
@ -403,74 +415,80 @@ impl DirTourist {
}) })
} }
#[allow(clippy::future_not_send)]
pub async fn next(&mut self) -> Visit { pub async fn next(&mut self) -> Visit {
if let Some(path) = self.to_visit.pop() { if let Some(path) = self.to_visit.pop() {
let _span = trace_span!("visit_path", ?path).entered(); self.visit_path(path).await
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)
} else { } else {
Visit::Done 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) { pub fn skip(&mut self, path: PathBuf) {
let check_path = path.as_path(); let check_path = path.as_path();
self.to_visit.retain(|p| !p.starts_with(check_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, /// Use [`empty()`](IgnoreFilterer::empty()) if you want an empty filterer,
/// or to construct one outside an async environment. /// 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 origin = origin.as_ref();
let _span = trace_span!("build_filterer", ?origin); let _span = trace_span!("build_filterer", ?origin);
@ -113,6 +113,7 @@ impl IgnoreFilter {
} }
/// Returns the number of ignores and allowlists loaded. /// Returns the number of ignores and allowlists loaded.
#[must_use]
pub fn num_ignores(&self) -> (u64, u64) { pub fn num_ignores(&self) -> (u64, u64) {
(self.compiled.num_ignores(), self.compiled.num_whitelists()) (self.compiled.num_ignores(), self.compiled.num_whitelists())
} }
@ -183,11 +184,7 @@ impl IgnoreFilter {
/// Adds some globs manually, if the builder is available. /// Adds some globs manually, if the builder is available.
/// ///
/// Does nothing silently otherwise. /// Does nothing silently otherwise.
pub async fn add_globs( pub fn add_globs(&mut self, globs: &[&str], applies_in: Option<&PathBuf>) -> Result<(), Error> {
&mut self,
globs: &[&str],
applies_in: Option<PathBuf>,
) -> Result<(), Error> {
if let Some(ref mut builder) = self.builder { if let Some(ref mut builder) = self.builder {
let _span = trace_span!("loading ignore globs", ?globs).entered(); let _span = trace_span!("loading ignore globs", ?globs).entered();
for line in globs { for line in globs {
@ -197,7 +194,7 @@ impl IgnoreFilter {
trace!(?line, "adding ignore line"); trace!(?line, "adding ignore line");
builder builder
.add_line(applies_in.clone(), line) .add_line(applies_in.cloned(), line)
.map_err(|err| Error::Glob { file: None, err })?; .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" repository = "https://github.com/watchexec/watchexec"
readme = "README.md" readme = "README.md"
rust-version = "1.60.0" rust-version = "1.61.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -20,11 +20,11 @@ async-priority-channel = "0.1.0"
async-recursion = "1.0.0" async-recursion = "1.0.0"
atomic-take = "1.0.0" atomic-take = "1.0.0"
clearscreen = "1.0.9" clearscreen = "1.0.9"
dunce = "1.0.2"
futures = "0.3.16" futures = "0.3.16"
miette = "5.3.0" miette = "5.3.0"
once_cell = "1.8.0" once_cell = "1.8.0"
thiserror = "1.0.26" thiserror = "1.0.26"
normalize-path = "0.2.0"
[dependencies.command-group] [dependencies.command-group]
version = "1.0.8" version = "1.0.8"
@ -57,8 +57,5 @@ features = [
version = "0.1.26" version = "0.1.26"
features = ["log"] features = ["log"]
[target.'cfg(unix)'.dependencies]
libc = "0.2.104"
[dev-dependencies] [dev-dependencies]
tracing-subscriber = "0.3.6" 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) [![Crates.io page](https://badgen.net/crates/v/watchexec)](https://crates.io/crates/watchexec)
[![API Docs](https://docs.rs/watchexec/badge.svg)][docs] [![API Docs](https://docs.rs/watchexec/badge.svg)][docs]
[![Crate license: Apache 2.0](https://badgen.net/badge/license/Apache%202.0)][license] [![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) [![CI status](https://github.com/watchexec/watchexec/actions/workflows/check.yml/badge.svg)](https://github.com/watchexec/watchexec/actions/workflows/check.yml)
# Watchexec library # Watchexec library
@ -10,7 +10,7 @@ _The library which powers [Watchexec CLI](https://watchexec.github.io) and other
- **[API documentation][docs]**. - **[API documentation][docs]**.
- Licensed under [Apache 2.0][license]. - 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. - Status: maintained.
[docs]: https://docs.rs/watchexec [docs]: https://docs.rs/watchexec

View File

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

View File

@ -66,21 +66,25 @@ impl Default for Outcome {
impl Outcome { impl Outcome {
/// Convenience function to create an outcome conditional on the state of the subprocess. /// 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)) Self::IfRunning(Box::new(then), Box::new(otherwise))
} }
/// Convenience function to create a sequence of outcomes. /// 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)) Self::Both(Box::new(one), Box::new(two))
} }
/// Convenience function to wait for the subprocess to complete before executing the outcome. /// Convenience function to wait for the subprocess to complete before executing the outcome.
pub fn wait(and_then: Outcome) -> Self { #[must_use]
Self::Both(Box::new(Outcome::Wait), Box::new(and_then)) 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. /// Resolves the outcome given the current state of the subprocess.
#[must_use]
pub fn resolve(self, is_running: bool) -> Self { pub fn resolve(self, is_running: bool) -> Self {
match (is_running, self) { match (is_running, self) {
(true, Self::IfRunning(then, _)) => then.resolve(true), (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 // TODO: use a select and a notifier of some kind so it cancels tasks
if self.gencheck.load(Ordering::SeqCst) != self.gen { if self.gencheck.load(Ordering::SeqCst) != self.gen {
warn!(when=%"pre", gen=%self.gen, "outcome worker was cycled, aborting"); warn!(when=%"pre", gen=%self.gen, "outcome worker was cycled, aborting");
@ -113,9 +113,7 @@ impl OutcomeWorker {
notry!(self.process.wait())?; notry!(self.process.wait())?;
notry!(self.process.drop_inner()); notry!(self.process.drop_inner());
} }
(false, o @ Outcome::Stop) (false, o @ (Outcome::Stop | Outcome::Wait | Outcome::Signal(_))) => {
| (false, o @ Outcome::Wait)
| (false, o @ Outcome::Signal(_)) => {
debug!(outcome=?o, "meaningless without a process, not doing anything"); debug!(outcome=?o, "meaningless without a process, not doing anything");
} }
(_, Outcome::Start) => { (_, Outcome::Start) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ async fn unix_shell_none() -> Result<(), std::io::Error> {
args: vec!["hi".into()] args: vec!["hi".into()]
} }
.to_spawnable() .to_spawnable()
.unwrap() .expect("echo directly")
.group_status() .group_status()
.await? .await?
.success()); .success());
@ -25,7 +25,7 @@ async fn unix_shell_sh() -> Result<(), std::io::Error> {
command: "echo hi".into() command: "echo hi".into()
} }
.to_spawnable() .to_spawnable()
.unwrap() .expect("echo with sh")
.group_status() .group_status()
.await? .await?
.success()); .success());
@ -41,7 +41,7 @@ async fn unix_shell_alternate() -> Result<(), std::io::Error> {
command: "echo hi".into() command: "echo hi".into()
} }
.to_spawnable() .to_spawnable()
.unwrap() .expect("echo with bash")
.group_status() .group_status()
.await? .await?
.success()); .success());
@ -57,7 +57,7 @@ async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
command: "echo hi".into() command: "echo hi".into()
} }
.to_spawnable() .to_spawnable()
.unwrap() .expect("echo with shopts")
.group_status() .group_status()
.await? .await?
.success()); .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 /// 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". /// [`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 /// You should see the detailed documentation on [`fs::WorkingData`][crate::fs::WorkingData] and
/// [action::WorkingData][crate::action::WorkingData] for important information and particulars /// [`action::WorkingData`][crate::action::WorkingData] for important information and particulars
/// about each field, especially the handlers. /// about each field, especially the handlers.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
#[non_exhaustive] #[non_exhaustive]

View File

@ -71,15 +71,16 @@ pub enum Tag {
impl Tag { impl Tag {
/// The name of the variant. /// The name of the variant.
#[must_use]
pub const fn discriminant_name(&self) -> &'static str { pub const fn discriminant_name(&self) -> &'static str {
match self { match self {
Tag::Path { .. } => "Path", Self::Path { .. } => "Path",
Tag::FileEventKind(_) => "FileEventKind", Self::FileEventKind(_) => "FileEventKind",
Tag::Source(_) => "Source", Self::Source(_) => "Source",
Tag::Keyboard(_) => "Keyboard", Self::Keyboard(_) => "Keyboard",
Tag::Process(_) => "Process", Self::Process(_) => "Process",
Tag::Signal(_) => "Signal", Self::Signal(_) => "Signal",
Tag::ProcessCompletion(_) => "ProcessCompletion", Self::ProcessCompletion(_) => "ProcessCompletion",
} }
} }
} }
@ -164,39 +165,24 @@ pub enum ProcessEnd {
} }
impl From<ExitStatus> for ProcessEnd { impl From<ExitStatus> for ProcessEnd {
#[cfg(target_os = "fuchsia")] #[cfg(unix)]
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")))]
fn from(es: ExitStatus) -> Self { fn from(es: ExitStatus) -> Self {
use std::os::unix::process::ExitStatusExt; 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?!") unreachable!("exitstatus cannot both be code and signal?!")
} }
(Some(code), None) => match NonZeroI64::try_from(i64::from(code)) { (Some(code), None, _) => {
Ok(code) => Self::ExitError(code), NonZeroI64::try_from(i64::from(code)).map_or(Self::Success, Self::ExitError)
Err(_) => Self::Success, }
}, (None, Some(_), Some(stopsig)) => {
// TODO: once unix_process_wait_more lands, use stopped_signal() instead and clear the libc dep NonZeroI32::try_from(stopsig).map_or(Self::Success, Self::ExitStop)
(None, Some(signal)) if libc::WIFSTOPPED(-signal) => {
match NonZeroI32::try_from(libc::WSTOPSIG(-signal)) {
Ok(signal) => Self::ExitStop(signal),
Err(_) => Self::Success,
}
} }
// TODO: once unix_process_wait_more lands, use continued() instead and clear the libc dep
#[cfg(not(target_os = "vxworks"))] #[cfg(not(target_os = "vxworks"))]
(None, Some(signal)) if libc::WIFCONTINUED(-signal) => Self::Continued, (None, Some(_), _) if es.continued() => Self::Continued,
(None, Some(signal)) => Self::ExitSignal(signal.into()), (None, Some(signal), _) => Self::ExitSignal(signal.into()),
(None, None) => Self::Success, (None, None, _) => Self::Success,
} }
} }
@ -302,6 +288,7 @@ impl Default for Priority {
impl Event { impl Event {
/// Returns true if the event has an Internal source tag. /// Returns true if the event has an Internal source tag.
#[must_use]
pub fn is_internal(&self) -> bool { pub fn is_internal(&self) -> bool {
self.tags self.tags
.iter() .iter()
@ -309,6 +296,7 @@ impl Event {
} }
/// Returns true if the event has no tags. /// Returns true if the event has no tags.
#[must_use]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.tags.is_empty() self.tags.is_empty()
} }
@ -346,16 +334,16 @@ impl fmt::Display for Event {
Tag::Path { path, file_type } => { Tag::Path { path, file_type } => {
write!(f, " path={}", path.display())?; write!(f, " path={}", path.display())?;
if let Some(ft) = file_type { if let Some(ft) = file_type {
write!(f, " filetype={}", ft)?; write!(f, " filetype={ft}")?;
} }
} }
Tag::FileEventKind(kind) => write!(f, " kind={:?}", kind)?, Tag::FileEventKind(kind) => write!(f, " kind={kind:?}")?,
Tag::Source(s) => write!(f, " source={:?}", s)?, Tag::Source(s) => write!(f, " source={s:?}")?,
Tag::Keyboard(k) => write!(f, " keyboard={:?}", k)?, Tag::Keyboard(k) => write!(f, " keyboard={k:?}")?,
Tag::Process(p) => write!(f, " process={}", p)?, Tag::Process(p) => write!(f, " process={p}")?,
Tag::Signal(s) => write!(f, " signal={:?}", s)?, Tag::Signal(s) => write!(f, " signal={s:?}")?,
Tag::ProcessCompletion(None) => write!(f, " command-completed")?, 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> { impl<T: Filterer> Filterer for Arc<T> {
fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> { 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 async_priority_channel as priority;
use normalize_path::NormalizePath;
use notify::{Config, Watcher as _}; use notify::{Config, Watcher as _};
use tokio::sync::{mpsc, watch}; use tokio::sync::{mpsc, watch};
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace};
use crate::{ use crate::{
error::{CriticalError, FsWatcherError, RuntimeError}, 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 /// _not_ to drop the watch sender: this will cause the worker to stop gracefully, which may not be
/// what was expected. /// what was expected.
/// ///
/// Note that the paths emitted by the watcher are canonicalised. No guarantee is made about the /// Note that the paths emitted by the watcher are normalised. No guarantee is made about the
/// implementation or output of that canonicalisation (i.e. it might not be `std`'s). /// implementation or output of that normalisation (it may change without notice).
/// ///
/// # Examples /// # Examples
/// ///
@ -188,13 +189,13 @@ pub async fn worker(
} else { } else {
let mut to_watch = Vec::with_capacity(data.pathset.len()); let mut to_watch = Vec::with_capacity(data.pathset.len());
let mut to_drop = Vec::with_capacity(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) { if !pathset.contains(path) {
to_watch.push(path.clone()); to_watch.push(path.clone());
} }
} }
for path in pathset.iter() { for path in &pathset {
if !data.pathset.contains(path) { if !data.pathset.contains(path) {
to_drop.push(path.clone()); to_drop.push(path.clone());
} }
@ -210,7 +211,7 @@ pub async fn worker(
let n_events = events.clone(); let n_events = events.clone();
match kind.create(move |nev: Result<notify::Event, notify::Error>| { match kind.create(move |nev: Result<notify::Event, notify::Error>| {
trace!(event = ?nev, "receiving possible event from watcher"); 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(); n_errors.try_send(e).ok();
} }
}) { }) {
@ -296,7 +297,7 @@ fn notify_multi_path_errors(
fn process_event( fn process_event(
nev: Result<notify::Event, notify::Error>, nev: Result<notify::Event, notify::Error>,
kind: Watcher, kind: Watcher,
n_events: priority::Sender<Event, Priority>, n_events: &priority::Sender<Event, Priority>,
) -> Result<(), RuntimeError> { ) -> Result<(), RuntimeError> {
let nev = nev.map_err(|err| RuntimeError::FsWatcher { let nev = nev.map_err(|err| RuntimeError::FsWatcher {
kind, kind,
@ -311,10 +312,7 @@ fn process_event(
// possibly pull file_type from whatever notify (or the native driver) returns? // possibly pull file_type from whatever notify (or the native driver) returns?
tags.push(Tag::Path { tags.push(Tag::Path {
file_type: metadata(&path).ok().map(|m| m.file_type().into()), file_type: metadata(&path).ok().map(|m| m.file_type().into()),
path: dunce::canonicalize(&path).unwrap_or_else(|err| { path: path.normalize(),
warn!(?err, ?path, "failed to canonicalise event path");
path
}),
}); });
} }

View File

@ -100,8 +100,12 @@ pub trait Handler<T> {
/// ///
/// Internally this is a Tokio [`Mutex`]. /// Internally this is a Tokio [`Mutex`].
pub struct HandlerLock<T>(Arc<Mutex<Box<dyn Handler<T> + Send>>>); 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. /// Wrap a [`Handler`] into a lock.
#[must_use]
pub fn new(handler: Box<dyn Handler<T> + Send>) -> Self { pub fn new(handler: Box<dyn Handler<T> + Send>) -> Self {
Self(Arc::new(Mutex::new(handler))) 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 { fn default() -> Self {
Self::new(Box::new(())) 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 { RuntimeError::Handler {
ctx, ctx,
err: err.to_string(), err: err.to_string(),
@ -239,7 +246,7 @@ where
W: Write, W: Write,
{ {
fn handle(&mut self, data: T) -> Result<(), Box<dyn Error>> { 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, W: Write,
{ {
fn handle(&mut self, data: T) -> Result<(), Box<dyn Error>> { 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")] #![doc(html_logo_url = "https://watchexec.github.io/logo:watchexec.svg")]
#![warn(clippy::unwrap_used, missing_docs)] #![warn(clippy::unwrap_used, missing_docs)]
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![cfg_attr(not(target_os = "fuchsia"), forbid(unsafe_code))] #![forbid(unsafe_code)]
// see event::ProcessEnd for why this is disabled on fuchsia
// the toolkit to make your own // the toolkit to make your own
pub mod action; pub mod action;

View File

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

View File

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

View File

@ -204,7 +204,7 @@ async fn error_hook(
let crit = hook.critical.clone(); let crit = hook.critical.clone();
if let Err(err) = handler.handle(hook) { if let Err(err) = handler.handle(hook) {
error!(%err, "error while handling error"); 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(); let recrit = rehook.critical.clone();
handler.handle(rehook).unwrap_or_else(|err| { handler.handle(rehook).unwrap_or_else(|err| {
error!(%err, "error while handling error of handling error"); error!(%err, "error while handling error of handling error");
@ -248,17 +248,16 @@ impl ErrorHook {
) -> Result<(), CriticalError> { ) -> Result<(), CriticalError> {
match Arc::try_unwrap(crit) { match Arc::try_unwrap(crit) {
Err(err) => { Err(err) => {
error!(?err, "{} hook has an outstanding ref", name); error!(?err, "{name} hook has an outstanding ref");
Ok(()) Ok(())
} }
Ok(crit) => { Ok(crit) => crit.into_inner().map_or_else(
if let Some(crit) = crit.into_inner() { || Ok(()),
debug!(%crit, "{} output a critical error", name); |crit| {
debug!(%crit, "{name} output a critical error");
Err(crit) 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 notify::event::CreateKind;
use watchexec::{ use watchexec::{
@ -12,7 +12,7 @@ const ENV_SEP: &str = ":";
const ENV_SEP: &str = ";"; const ENV_SEP: &str = ";";
fn ospath(path: &str) -> OsString { fn ospath(path: &str) -> OsString {
let root = dunce::canonicalize(".").unwrap(); let root = std::fs::canonicalize(".").unwrap();
if path.is_empty() { if path.is_empty() {
root root
} else { } else {
@ -171,11 +171,13 @@ fn single_type_multipath() {
( (
"CREATED", "CREATED",
OsString::from( OsString::from(
"".to_string() [
+ "deeper/sub/folder.txt" format!("deeper{MAIN_SEPARATOR}sub{MAIN_SEPARATOR}folder.txt"),
+ ENV_SEP + "dom/folder.txt" format!("dom{MAIN_SEPARATOR}folder.txt"),
+ ENV_SEP + "root.txt" + ENV_SEP "root.txt".to_string(),
+ "sub/folder.txt" format!("sub{MAIN_SEPARATOR}folder.txt"),
]
.join(ENV_SEP)
) )
), ),
("COMMON", ospath("")), ("COMMON", ospath("")),
@ -194,7 +196,13 @@ fn single_type_divergent_paths() {
HashMap::from([ HashMap::from([
( (
"CREATED", "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("")), ("COMMON", ospath("")),
]) ])
@ -218,11 +226,22 @@ fn multitype_multipath() {
HashMap::from([ HashMap::from([
( (
"CREATED", "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("")), ("COMMON", ospath("")),
]) ])
); );
@ -249,7 +268,7 @@ fn multiple_paths_in_one_event() {
HashMap::from([ HashMap::from([
( (
"OTHERWISE_CHANGED", "OTHERWISE_CHANGED",
OsString::from("".to_string() + "one.txt" + ENV_SEP + "two.txt") OsString::from(String::new() + "one.txt" + ENV_SEP + "two.txt")
), ),
("COMMON", ospath("")), ("COMMON", ospath("")),
]) ])
@ -275,7 +294,7 @@ fn mixed_non_paths_events() {
HashMap::from([ HashMap::from([
( (
"OTHERWISE_CHANGED", "OTHERWISE_CHANGED",
OsString::from("".to_string() + "one.txt" + ENV_SEP + "two.txt") OsString::from(String::new() + "one.txt" + ENV_SEP + "two.txt")
), ),
("COMMON", ospath("")), ("COMMON", ospath("")),
]) ])
@ -312,7 +331,7 @@ fn multipath_is_sorted() {
( (
"OTHERWISE_CHANGED", "OTHERWISE_CHANGED",
OsString::from( OsString::from(
"".to_string() String::new()
+ "0123.txt" + ENV_SEP + "a.txt" + "0123.txt" + ENV_SEP + "a.txt"
+ ENV_SEP + "b.txt" + ENV_SEP + ENV_SEP + "b.txt" + ENV_SEP
+ "c.txt" + ENV_SEP + "ᄁ.txt" + "c.txt" + ENV_SEP + "ᄁ.txt"
@ -342,7 +361,7 @@ fn multipath_is_deduped() {
( (
"OTHERWISE_CHANGED", "OTHERWISE_CHANGED",
OsString::from( OsString::from(
"".to_string() String::new()
+ "0123.txt" + ENV_SEP + "a.txt" + "0123.txt" + ENV_SEP + "a.txt"
+ ENV_SEP + "b.txt" + ENV_SEP + ENV_SEP + "b.txt" + ENV_SEP
+ "c.txt" + ENV_SEP + "ᄁ.txt" + "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"] } tokio-stream = { version = "0.1.9", features = ["fs"] }
[dev-dependencies] [dev-dependencies]
dunce = "1.0.2"
miette = "5.3.0" miette = "5.3.0"
tracing-subscriber = "0.3.11" tracing-subscriber = "0.3.11"

View File

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

View File

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