Split into more crates (#307)

This commit is contained in:
Félix Saparelli 2022-06-15 03:25:05 +00:00
parent 70aa48c59d
commit 35cf63bc85
120 changed files with 1803 additions and 864 deletions

100
.github/workflows/release-pr.yml vendored Normal file
View File

@ -0,0 +1,100 @@
name: Open a release PR
on:
workflow_dispatch:
inputs:
crate:
description: Crate to release
required: true
type: choice
choices:
- cli
- lib
- filterer-globset
- filterer-ignore
- filterer-tagged
- ignore-files
- project-origins
version:
description: Version to release
required: true
type: string
jobs:
make-branch:
runs-on: ubuntu-latest
steps:
- name: Install cargo-release
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-release
version: "0.21"
- uses: actions/checkout@v2
with:
ref: main
- name: Make branch "release-${{ inputs.crate }}-${{ inputs.version }}"
run: git switch -c "release-${{ inputs.crate }}-${{ inputs.version }}"
- name: Find crate name
run: |
set -euxo pipefail
pushd "crates/${{ inputs.crate }}"
crate_name=$(head Cargo.toml -n2 | grep name | cut -d '"' -f2)
echo "crate_name=${crate_name}" >> $GITHUB_ENV
popd
- name: Do release
run: |
set -euxo pipefail
git config user.name github-actions
git config user.email github-actions@github.com
cargo release \
--execute \
--no-push \
--no-tag \
--no-publish \
--no-confirm \
--verbose \
--allow-branch "release-${{ inputs.crate }}-${{ inputs.version }}"
--package "${{ env.crate_name }}" \
"${{ inputs.version }}"
- name: Push new branch
run: |
set -euxo pipefail
git push origin "release-${{ inputs.crate }}-${{ inputs.version }}"
make-pr:
runs-on: ubuntu-latest
needs: make-branch
steps:
- run: |
set -euxo pipefail
title="release: ${{ inputs.crate }} v${{ inputs.version }}"
body="This is a release PR for **${{ inputs.crate }}** to version **${{ inputs.version }}**."
if [[ "${{ inputs.crate }}" == "cli" ]]; then
body="$body
Upon merging, this will automatically build the CLI and create a GitHub release. You still need to manually publish the cargo crate."
else
body="$body
Upon merging, you will still need to manually publish the cargo crate."
fi
body="$body
\`\`\`
$ cd crates/${{ inputs.crate }}
$ cargo publish
\`\`\`
To merge this release, review the changes then say:
\`\`\`
bors r+
\`\`\`"
pr_url=$(gh pr create --title "$title" --body "$body" --base main --head "release-${{ inputs.crate }}-${{ inputs.version }}" --label "release")
gh pr comment "$pr_url" --body "bors single on\nbors p=10"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

29
.github/workflows/release-tag.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Tag a release
on:
push:
branches:
- main
jobs:
make-tag:
runs-on: ubuntu-latest
# because only bors can push to main, and it squashes, only PRs
# that are named `release: {crate-name} v{version}` will get tagged!
# the commit message will look like: `release: {crate-name} v{version} (#{pr-number})`
if: "startsWith(github.event.head_commit.message, 'release: ')"
steps:
- name: Extract tag from commit message
run: |
set -euxo pipefail
message="${{ github.event.head_commit.message }}"
crate="$(cut -d ' ' -f 2 <<< "${message}")"
version="$(cut -d ' ' -f 3 <<< "${message}" | tr -d 'v')"
echo "CUSTOM_TAG=${crate}-${version}" >> $GITHUB_ENV
- uses: actions/checkout@v2
- name: Bump version and push tag
id: tag_version
uses: mathieudutour/github-tag-action@v6.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ env.CUSTOM_TAG }}

View File

@ -3,9 +3,8 @@
cff-version: "1.1.0"
message: |
If you use this software, please cite it using these metadata.
The library has its own CITATION.cff file in lib/.
title: Watchexec (command-line tool)
title: Watchexec
version: "1.19.0"
date-released: 2022-04-15

119
Cargo.lock generated
View File

@ -349,7 +349,7 @@ dependencies = [
"strsim",
"termcolor",
"terminal_size",
"textwrap 0.15.0",
"textwrap",
]
[[package]]
@ -839,9 +839,9 @@ dependencies = [
[[package]]
name = "git-hash"
version = "0.9.4"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05d7e760613f4118cdccaf881fc9d1323c3ea596d156aac25a4eb44b428f11e"
checksum = "3d3f93912499fa8935199743365e276e37551ceecf871c8be558dcf158abfc85"
dependencies = [
"hex",
"quick-error",
@ -1022,6 +1022,21 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "ignore-files"
version = "1.0.0"
dependencies = [
"dunce",
"futures",
"git-config",
"ignore",
"miette",
"project-origins",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "indexmap"
version = "1.8.2"
@ -1229,7 +1244,7 @@ dependencies = [
"supports-hyperlinks",
"supports-unicode",
"terminal_size",
"textwrap 0.15.0",
"textwrap",
"thiserror",
"unicode-width",
]
@ -1346,9 +1361,9 @@ dependencies = [
[[package]]
name = "notify"
version = "5.0.0-pre.14"
version = "5.0.0-pre.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d13c22db70a63592e098fb51735bab36646821e6389a0ba171f3549facdf0b74"
checksum = "553f9844ad0b0824605c20fb55a661679782680410abfb1a8144c2e7e437e7a7"
dependencies = [
"bitflags",
"crossbeam-channel",
@ -1642,6 +1657,18 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "project-origins"
version = "1.0.0"
dependencies = [
"dunce",
"futures",
"miette",
"tokio",
"tokio-stream",
"tracing-subscriber",
]
[[package]]
name = "prost"
version = "0.10.4"
@ -2147,24 +2174,16 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
dependencies = [
"smawk",
"terminal_size",
"unicode-linebreak",
"unicode-width",
]
[[package]]
@ -2589,31 +2608,25 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "watchexec"
version = "2.0.0-pre.14"
version = "2.0.0"
dependencies = [
"async-priority-channel",
"async-recursion 1.0.0",
"async-stream",
"atomic-take",
"clearscreen",
"command-group",
"dunce",
"futures",
"git-config",
"globset",
"ignore",
"ignore-files",
"libc",
"miette",
"nom 7.1.1",
"notify",
"once_cell",
"regex",
"project-origins",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
"tracing-subscriber",
"unicase",
]
[[package]]
@ -2627,15 +2640,69 @@ dependencies = [
"dunce",
"embed-resource",
"futures",
"ignore-files",
"insta",
"miette",
"mimalloc",
"notify-rust",
"project-origins",
"tokio",
"tracing",
"tracing-log",
"tracing-subscriber",
"watchexec",
"watchexec-filterer-globset",
"watchexec-filterer-tagged",
]
[[package]]
name = "watchexec-filterer-globset"
version = "1.0.0"
dependencies = [
"dunce",
"ignore",
"ignore-files",
"project-origins",
"tokio",
"tracing",
"tracing-subscriber",
"watchexec",
"watchexec-filterer-ignore",
]
[[package]]
name = "watchexec-filterer-ignore"
version = "1.0.0"
dependencies = [
"dunce",
"ignore",
"ignore-files",
"project-origins",
"tokio",
"tracing",
"tracing-subscriber",
"watchexec",
]
[[package]]
name = "watchexec-filterer-tagged"
version = "0.1.0"
dependencies = [
"dunce",
"globset",
"ignore",
"ignore-files",
"miette",
"nom 7.1.1",
"project-origins",
"regex",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
"unicase",
"watchexec",
"watchexec-filterer-ignore",
]
[[package]]

View File

@ -1,8 +1,13 @@
[workspace]
resolver = "2"
members = [
"lib",
"cli",
"crates/lib",
"crates/cli",
"crates/filterer/globset",
"crates/filterer/ignore",
"crates/filterer/tagged",
"crates/ignore-files",
"crates/project-origins",
]
[profile.release]

View File

@ -61,9 +61,11 @@ Watchexec pairs well with:
## Extend
- [watchexec library](./lib/): to create more specialised watchexec-powered tools! such as:
- [watchexec library](./crates/lib/): to create more specialised watchexec-powered tools! such as:
- [cargo watch](https://github.com/watchexec/cargo-watch): for Rust/Cargo projects.
- [clearscreen](https://github.com/watchexec/clearscreen): to clear the (terminal) screen on every platform.
- [command group](https://github.com/watchexec/command-group): to run commands in process groups.
- [ignore files](./crates/ignore-files/): to find, parse, and interpret ignore files.
- [project origins](./crates/project-origins/): to find the origin(s) directory of a project.
- [notify](https://github.com/notify-rs/notify): to respond to file modifications (third-party).
- [globset](https://crates.io/crates/globset): to match globs (third-party).

View File

@ -1,5 +1,7 @@
delete_merged_branches = true
update_base_for_deletes = true
# needs to remain squash for current release process
use_squash_merge = true
status = [

View File

@ -29,12 +29,22 @@ miette = { version = "4.7.1", features = ["fancy"] }
notify-rust = "4.5.2"
tracing = "0.1.26"
tracing-log = "0.1.3"
watchexec = { version = "=2.0.0-pre.14", path = "../lib" }
watchexec = { version = "2.0.0", path = "../lib" }
watchexec-filterer-globset = { version = "1.0.0", path = "../filterer/globset" }
watchexec-filterer-tagged = { version = "0.1.0", path = "../filterer/tagged" }
[dependencies.clap]
version = "3.1.18"
features = ["cargo", "wrap_help"]
[dependencies.ignore-files]
version = "1.0.0"
path = "../ignore-files"
[dependencies.project-origins]
version = "1.0.0"
path = "../project-origins"
[dependencies.tokio]
version = "1.19.2"
features = [

View File

@ -1,5 +1,4 @@
pre-release-hook = ["../bin/pre-release-pull"]
pre-release-commit-message = "cli: v{{version}}"
pre-release-commit-message = "release: cli v{{version}}"
tag-prefix = "cli-"
tag-message = "watchexec {{version}}"
@ -8,20 +7,6 @@ tag-message = "watchexec {{version}}"
# reverted a lot more easily.
publish = false
[[pre-release-replacements]]
file = "../CITATION.cff"
search = "^version: \"?[\\d.]+(-.+)?\"?"
replace = "version: \"{{version}}\""
prerelease = true
max = 1
[[pre-release-replacements]]
file = "../CITATION.cff"
search = "^date-released: .+(-.+)?"
replace = "date-released: {{date}}"
prerelease = true
max = 1
[[pre-release-replacements]]
file = "watchexec.exe.manifest"
search = "^ version=\"[\\d.]+[.]0\""
@ -42,3 +27,17 @@ search = "watchexec [\\d.]+(-.+)?"
replace = "watchexec {{version}}"
prerelease = true
max = 1
[[pre-release-replacements]]
file = "../../CITATION.cff"
search = "^version: \"?[\\d.]+(-.+)?\"?"
replace = "version: \"{{version}}\""
prerelease = true
max = 1
[[pre-release-replacements]]
file = "../../CITATION.cff"
search = "^date-released: .+"
replace = "date-released: {{date}}"
prerelease = true
max = 1

View File

@ -6,13 +6,11 @@ use std::{
use clap::ArgMatches;
use dunce::canonicalize;
use ignore_files::IgnoreFile;
use miette::{miette, IntoDiagnostic, Result};
use project_origins::ProjectType;
use tracing::{debug, warn};
use watchexec::{
ignore::{self, IgnoreFile},
paths::common_prefix,
project::{self, ProjectType},
};
use watchexec::paths::common_prefix;
pub async fn dirs(args: &ArgMatches) -> Result<(PathBuf, PathBuf)> {
let curdir = env::current_dir()
@ -50,7 +48,7 @@ pub async fn dirs(args: &ArgMatches) -> Result<(PathBuf, PathBuf)> {
let mut origins = HashSet::new();
for path in paths {
origins.extend(project::origins(&path).await);
origins.extend(project_origins::origins(&path).await);
}
match (homedir, homedir_requested) {
@ -84,7 +82,7 @@ pub async fn dirs(args: &ArgMatches) -> Result<(PathBuf, PathBuf)> {
}
pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
let vcs_types = project::types(origin)
let vcs_types = project_origins::types(origin)
.await
.into_iter()
.filter(|pt| pt.is_vcs())
@ -98,7 +96,7 @@ pub async fn ignores(
vcs_types: &[ProjectType],
origin: &Path,
) -> Vec<IgnoreFile> {
let (mut ignores, errors) = ignore::from_origin(origin).await;
let (mut ignores, errors) = ignore_files::from_origin(origin).await;
for err in errors {
warn!("while discovering project-local ignore files: {}", err);
}
@ -129,7 +127,7 @@ pub async fn ignores(
debug!(?ignores, "filtered ignores to only those for project vcs");
}
let (mut global_ignores, errors) = ignore::from_environment().await;
let (mut global_ignores, errors) = ignore_files::from_environment(Some("watchexec")).await;
for err in errors {
warn!("while discovering global ignore files: {}", err);
}

View File

@ -12,8 +12,9 @@ use watchexec::{
filekind::{FileEventKind, ModifyKind},
Event, Priority, Tag,
},
filter::{globset::GlobsetFilterer, Filterer},
filter::Filterer,
};
use watchexec_filterer_globset::GlobsetFilterer;
pub async fn globset(args: &ArgMatches) -> Result<Arc<WatchexecFilterer>> {
let (project_origin, workdir) = super::common::dirs(args).await?;

View File

@ -2,14 +2,11 @@ use std::sync::Arc;
use clap::ArgMatches;
use futures::future::try_join_all;
use ignore_files::IgnoreFile;
use miette::{IntoDiagnostic, Result};
use tracing::{debug, trace, warn};
use watchexec::{
filter::tagged::{
files::{self, FilterFile},
Filter, Matcher, Op, Pattern, TaggedFilterer,
},
ignore::IgnoreFile,
use watchexec_filterer_tagged::{
discover_files_from_environment, Filter, FilterFile, Matcher, Op, Pattern, TaggedFilterer,
};
pub async fn tagged(args: &ArgMatches) -> Result<Arc<TaggedFilterer>> {
@ -35,7 +32,7 @@ pub async fn tagged(args: &ArgMatches) -> Result<Arc<TaggedFilterer>> {
debug!(?filter_files, "resolved command filter files");
if !args.is_present("no-global-filters") {
let (global_filter_files, errors) = files::from_environment().await;
let (global_filter_files, errors) = discover_files_from_environment().await;
for err in errors {
warn!("while discovering project-local filter files: {}", err);
}

View File

@ -0,0 +1,5 @@
# Changelog
## Next (YYYY-MM-DD)
- Initial release as a separate crate.

View File

@ -0,0 +1,47 @@
[package]
name = "watchexec-filterer-globset"
version = "1.0.0"
authors = ["Matt Green <mattgreenrocks@gmail.com>", "Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0"
description = "Watchexec filterer component based on globset"
keywords = ["watchexec", "filterer", "globset"]
documentation = "https://docs.rs/watchexec-filterer-globset"
homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.58.0"
edition = "2021"
[dependencies]
ignore = "0.4.18"
tracing = "0.1.26"
watchexec = { version = "2.0.0", path = "../../lib" }
[dependencies.ignore-files]
version = "1.0.0"
path = "../../ignore-files"
[dependencies.watchexec-filterer-ignore]
version = "1.0.0"
path = "../ignore"
[dev-dependencies]
dunce = "1.0.2"
tracing-subscriber = "0.3.6"
[dev-dependencies.project-origins]
version = "1.0.0"
path = "../../project-origins"
[dev-dependencies.tokio]
version = "1.19.2"
features = [
"fs",
"io-std",
"rt",
"rt-multi-thread",
"macros",
]

View File

@ -0,0 +1,17 @@
[![Crates.io page](https://badgen.net/crates/v/watchexec-filterer-globset)](https://crates.io/crates/watchexec-filterer-globset)
[![API Docs](https://docs.rs/watchexec-filterer-globset/badge.svg)][docs]
[![Crate license: Apache 2.0](https://badgen.net/badge/license/Apache%202.0)][license]
![MSRV: 1.58.0 (minor)](https://badgen.net/badge/MSRV/1.58.0%20%28minor%29/0b7261)
[![CI status](https://github.com/watchexec/watchexec/actions/workflows/check.yml/badge.svg)](https://github.com/watchexec/watchexec/actions/workflows/check.yml)
# Watchexec filterer: globset
_The default filterer implementation for Watchexec._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.58.0 (incurs a minor semver bump).
- Status: maintained.
[docs]: https://docs.rs/watchexec-filterer-globset
[license]: ../../../LICENSE

View File

@ -0,0 +1,10 @@
pre-release-commit-message = "release: filterer-globset v{{version}}"
tag-prefix = "filterer-globset-"
tag-message = "watchexec-filterer-globset {{version}}"
[[pre-release-replacements]]
file = "CHANGELOG.md"
search = "^## Next.*$"
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
prerelease = true
max = 1

View File

@ -1,25 +1,24 @@
//! A simple filterer in the style of the watchexec v1 filter.
//! A path-only Watchexec filterer based on globsets.
//!
//! This filterer mimics the behavior of the `watchexec` v1 filter, but does not match it exactly,
//! due to differing internals. It is used as the default filterer in Watchexec CLI currently.
use std::ffi::OsString;
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::{
ffi::OsString,
path::{Path, PathBuf, MAIN_SEPARATOR},
};
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use ignore_files::{Error, IgnoreFile, IgnoreFilter};
use tracing::{debug, trace, trace_span};
use watchexec::{
error::RuntimeError,
event::{Event, FileType, Priority},
filter::Filterer,
};
use watchexec_filterer_ignore::IgnoreFilterer;
use crate::error::RuntimeError;
use crate::event::{Event, FileType, Priority};
use crate::filter::Filterer;
use crate::ignore::{IgnoreFile, IgnoreFilterer};
/// A path-only filterer based on globsets.
///
/// This filterer mimics the behavior of the `watchexec` v1 filter, but does not match it exactly,
/// due to differing internals. It is intended to be used as a stopgap until the "tagged" filterer
/// or another advanced filterer, reaches a stable state or becomes the default. As such it does not
/// have an updatable configuration.
///
/// While it is experimental, the ["tagged" filterer](crate::filter::tagged) is generally easier
/// and more intuitive to use in code for new applications.
/// A simple filterer in the style of the watchexec v1.17 filter.
#[derive(Debug)]
pub struct GlobsetFilterer {
origin: PathBuf,
@ -49,7 +48,7 @@ impl GlobsetFilterer {
ignores: impl IntoIterator<Item = (String, Option<PathBuf>)>,
ignore_files: impl IntoIterator<Item = IgnoreFile>,
extensions: impl IntoIterator<Item = OsString>,
) -> Result<Self, RuntimeError> {
) -> Result<Self, Error> {
let origin = origin.as_ref();
let mut filters_builder = GitignoreBuilder::new(&origin);
let mut ignores_builder = GitignoreBuilder::new(&origin);
@ -58,35 +57,36 @@ impl GlobsetFilterer {
trace!(filter=?&filter, "add filter to globset filterer");
filters_builder
.add_line(in_path.clone(), &filter)
.map_err(|err| RuntimeError::GlobsetGlob { file: in_path, err })?;
.map_err(|err| Error::Glob { file: in_path, err })?;
}
for (ignore, in_path) in ignores {
trace!(ignore=?&ignore, "add ignore to globset filterer");
ignores_builder
.add_line(in_path.clone(), &ignore)
.map_err(|err| RuntimeError::GlobsetGlob { file: in_path, err })?;
.map_err(|err| Error::Glob { file: in_path, err })?;
}
let filters = filters_builder
.build()
.map_err(|err| RuntimeError::GlobsetGlob { file: None, err })?;
.map_err(|err| Error::Glob { file: None, err })?;
let ignores = ignores_builder
.build()
.map_err(|err| RuntimeError::GlobsetGlob { file: None, err })?;
.map_err(|err| Error::Glob { file: None, err })?;
let extensions: Vec<OsString> = extensions.into_iter().collect();
let mut ignore_files =
IgnoreFilterer::new(origin, &ignore_files.into_iter().collect::<Vec<_>>()).await?;
IgnoreFilter::new(origin, &ignore_files.into_iter().collect::<Vec<_>>()).await?;
ignore_files.finish();
let ignore_files = IgnoreFilterer(ignore_files);
debug!(
?origin,
num_filters=%filters.num_ignores(),
num_neg_filters=%filters.num_whitelists(),
num_ignores=%ignores.num_ignores(),
num_in_ignore_files=?ignore_files.num_ignores(),
num_in_ignore_files=?ignore_files.0.num_ignores(),
num_neg_ignores=%ignores.num_whitelists(),
num_extensions=%extensions.len(),
"globset filterer built");

View File

@ -0,0 +1,142 @@
use std::{
ffi::OsString,
path::{Path, PathBuf},
};
use ignore_files::IgnoreFile;
use project_origins::ProjectType;
use watchexec::{
error::RuntimeError,
event::{Event, FileType, Priority, Tag},
filter::Filterer,
};
use watchexec_filterer_globset::GlobsetFilterer;
use watchexec_filterer_ignore::IgnoreFilterer;
pub mod globset {
pub use super::globset_filt as filt;
pub use super::Applies;
pub use super::PathHarness;
pub use watchexec::event::Priority;
}
pub trait PathHarness: Filterer {
fn check_path(
&self,
path: PathBuf,
file_type: Option<FileType>,
) -> std::result::Result<bool, RuntimeError> {
let event = Event {
tags: vec![Tag::Path { path, file_type }],
metadata: Default::default(),
};
self.check_event(&event, Priority::Normal)
}
fn path_pass(&self, path: &str, file_type: Option<FileType>, pass: bool) {
let origin = dunce::canonicalize(".").unwrap();
let full_path = if let Some(suf) = path.strip_prefix("/test/") {
origin.join(suf)
} else if Path::new(path).has_root() {
path.into()
} else {
origin.join(path)
};
tracing::info!(?path, ?file_type, ?pass, "check");
assert_eq!(
self.check_path(full_path, file_type).unwrap(),
pass,
"{} {:?} (expected {})",
match file_type {
Some(FileType::File) => "file",
Some(FileType::Dir) => "dir",
Some(FileType::Symlink) => "symlink",
Some(FileType::Other) => "other",
None => "path",
},
path,
if pass { "pass" } else { "fail" }
);
}
fn file_does_pass(&self, path: &str) {
self.path_pass(path, Some(FileType::File), true);
}
fn file_doesnt_pass(&self, path: &str) {
self.path_pass(path, Some(FileType::File), false);
}
fn dir_does_pass(&self, path: &str) {
self.path_pass(path, Some(FileType::Dir), true);
}
fn dir_doesnt_pass(&self, path: &str) {
self.path_pass(path, Some(FileType::Dir), false);
}
fn unk_does_pass(&self, path: &str) {
self.path_pass(path, None, true);
}
fn unk_doesnt_pass(&self, path: &str) {
self.path_pass(path, None, false);
}
}
impl PathHarness for GlobsetFilterer {}
impl PathHarness for IgnoreFilterer {}
fn tracing_init() {
use tracing_subscriber::{
fmt::{format::FmtSpan, Subscriber},
util::SubscriberInitExt,
EnvFilter,
};
Subscriber::builder()
.pretty()
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.with_env_filter(EnvFilter::from_default_env())
.finish()
.try_init()
.ok();
}
pub async fn globset_filt(
filters: &[&str],
ignores: &[&str],
extensions: &[&str],
) -> GlobsetFilterer {
let origin = dunce::canonicalize(".").unwrap();
tracing_init();
GlobsetFilterer::new(
origin,
filters.iter().map(|s| (s.to_string(), None)),
ignores.iter().map(|s| (s.to_string(), None)),
vec![],
extensions.iter().map(OsString::from),
)
.await
.expect("making filterer")
}
pub trait Applies {
fn applies_in(self, origin: &str) -> Self;
fn applies_to(self, project_type: ProjectType) -> Self;
}
impl Applies for IgnoreFile {
fn applies_in(mut self, origin: &str) -> Self {
let origin = dunce::canonicalize(".").unwrap().join(origin);
self.applies_in = Some(origin);
self
}
fn applies_to(mut self, project_type: ProjectType) -> Self {
self.applies_to = Some(project_type);
self
}
}

View File

@ -0,0 +1,5 @@
# Changelog
## Next (YYYY-MM-DD)
- Initial release as a separate crate.

View File

@ -0,0 +1,43 @@
[package]
name = "watchexec-filterer-ignore"
version = "1.0.0"
authors = ["Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0"
description = "Watchexec filterer component for ignore files"
keywords = ["watchexec", "filterer", "ignore"]
documentation = "https://docs.rs/watchexec-filterer-ignore"
homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.58.0"
edition = "2021"
[dependencies]
ignore = "0.4.18"
tracing = "0.1.26"
watchexec = { version = "2.0.0", path = "../../lib" }
[dependencies.ignore-files]
version = "1.0.0"
path = "../../ignore-files"
[dev-dependencies]
dunce = "1.0.2"
tracing-subscriber = "0.3.6"
[dev-dependencies.project-origins]
version = "1.0.0"
path = "../../project-origins"
[dev-dependencies.tokio]
version = "1.19.2"
features = [
"fs",
"io-std",
"rt",
"rt-multi-thread",
"macros",
]

View File

@ -0,0 +1,17 @@
[![Crates.io page](https://badgen.net/crates/v/watchexec-filterer-ignore)](https://crates.io/crates/watchexec-filterer-ignore)
[![API Docs](https://docs.rs/watchexec-filterer-ignore/badge.svg)][docs]
[![Crate license: Apache 2.0](https://badgen.net/badge/license/Apache%202.0)][license]
![MSRV: 1.58.0 (minor)](https://badgen.net/badge/MSRV/1.58.0%20%28minor%29/0b7261)
[![CI status](https://github.com/watchexec/watchexec/actions/workflows/check.yml/badge.svg)](https://github.com/watchexec/watchexec/actions/workflows/check.yml)
# Watchexec filterer: ignore
_(Sub)filterer implementation for ignore files._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.58.0 (incurs a minor semver bump).
- Status: maintained.
[docs]: https://docs.rs/watchexec-filterer-ignore
[license]: ../../../LICENSE

View File

@ -0,0 +1,10 @@
pre-release-commit-message = "release: filterer-ignore v{{version}}"
tag-prefix = "filterer-ignore-"
tag-message = "watchexec-filterer-ignore {{version}}"
[[pre-release-replacements]]
file = "CHANGELOG.md"
search = "^## Next.*$"
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
prerelease = true
max = 1

View File

@ -0,0 +1,58 @@
//! A Watchexec Filterer implementation for ignore files.
//!
//! This filterer is meant to be used as a backing filterer inside a more complex or complete
//! filterer, and not as a standalone filterer.
use ignore::Match;
use ignore_files::IgnoreFilter;
use tracing::{trace, trace_span};
use watchexec::{
error::RuntimeError,
event::{Event, FileType, Priority},
filter::Filterer,
};
/// A Watchexec [`Filterer`] implementation for [`IgnoreFilter`].
#[derive(Clone, Debug)]
pub struct IgnoreFilterer(pub IgnoreFilter);
impl Filterer for IgnoreFilterer {
/// Filter an event.
///
/// This implementation never errors. It returns `Ok(false)` if the event is ignored according
/// to the ignore files, and `Ok(true)` otherwise. It ignores event priority.
fn check_event(&self, event: &Event, _priority: Priority) -> Result<bool, RuntimeError> {
let _span = trace_span!("filterer_check").entered();
let mut pass = true;
for (path, file_type) in event.paths() {
let _span = trace_span!("checking_against_compiled", ?path, ?file_type).entered();
let is_dir = file_type
.map(|t| matches!(t, FileType::Dir))
.unwrap_or(false);
match self.0.match_path(path, is_dir) {
Match::None => {
trace!("no match (pass)");
pass &= true;
}
Match::Ignore(glob) => {
if glob.from().map_or(true, |f| path.strip_prefix(f).is_ok()) {
trace!(?glob, "positive match (fail)");
pass &= false;
} else {
trace!(?glob, "positive match, but not in scope (ignore)");
}
}
Match::Whitelist(glob) => {
trace!(?glob, "negative match (pass)");
pass = true;
}
}
}
trace!(?pass, "verdict");
Ok(pass)
}
}

View File

@ -1,4 +1,4 @@
use watchexec::ignore::IgnoreFilterer;
use watchexec_filterer_ignore::IgnoreFilterer;
mod helpers;
use helpers::ignore::*;

View File

@ -0,0 +1,220 @@
use std::path::{Path, PathBuf};
use ignore_files::{IgnoreFile, IgnoreFilter};
use project_origins::ProjectType;
use watchexec::{
error::RuntimeError,
event::{filekind::FileEventKind, Event, FileType, Priority, ProcessEnd, Source, Tag},
filter::Filterer,
signal::source::MainSignal,
};
use watchexec_filterer_ignore::IgnoreFilterer;
pub mod ignore {
pub use super::ig_file as file;
pub use super::ignore_filt as filt;
pub use super::Applies;
pub use super::PathHarness;
pub use watchexec::event::Priority;
}
pub trait PathHarness: Filterer {
fn check_path(
&self,
path: PathBuf,
file_type: Option<FileType>,
) -> std::result::Result<bool, RuntimeError> {
let event = Event {
tags: vec![Tag::Path { path, file_type }],
metadata: Default::default(),
};
self.check_event(&event, Priority::Normal)
}
fn path_pass(&self, path: &str, file_type: Option<FileType>, pass: bool) {
let origin = dunce::canonicalize(".").unwrap();
let full_path = if let Some(suf) = path.strip_prefix("/test/") {
origin.join(suf)
} else if Path::new(path).has_root() {
path.into()
} else {
origin.join(path)
};
tracing::info!(?path, ?file_type, ?pass, "check");
assert_eq!(
self.check_path(full_path, file_type).unwrap(),
pass,
"{} {:?} (expected {})",
match file_type {
Some(FileType::File) => "file",
Some(FileType::Dir) => "dir",
Some(FileType::Symlink) => "symlink",
Some(FileType::Other) => "other",
None => "path",
},
path,
if pass { "pass" } else { "fail" }
);
}
fn file_does_pass(&self, path: &str) {
self.path_pass(path, Some(FileType::File), true);
}
fn file_doesnt_pass(&self, path: &str) {
self.path_pass(path, Some(FileType::File), false);
}
fn dir_does_pass(&self, path: &str) {
self.path_pass(path, Some(FileType::Dir), true);
}
fn dir_doesnt_pass(&self, path: &str) {
self.path_pass(path, Some(FileType::Dir), false);
}
fn unk_does_pass(&self, path: &str) {
self.path_pass(path, None, true);
}
fn unk_doesnt_pass(&self, path: &str) {
self.path_pass(path, None, false);
}
}
impl PathHarness for IgnoreFilterer {}
pub trait TaggedHarness {
fn check_tag(&self, tag: Tag, priority: Priority) -> std::result::Result<bool, RuntimeError>;
fn priority_pass(&self, priority: Priority, pass: bool) {
tracing::info!(?priority, ?pass, "check");
assert_eq!(
self.check_tag(Tag::Source(Source::Filesystem), priority)
.unwrap(),
pass,
"{priority:?} (expected {})",
if pass { "pass" } else { "fail" }
);
}
fn priority_does_pass(&self, priority: Priority) {
self.priority_pass(priority, true);
}
fn priority_doesnt_pass(&self, priority: Priority) {
self.priority_pass(priority, false);
}
fn tag_pass(&self, tag: Tag, pass: bool) {
tracing::info!(?tag, ?pass, "check");
assert_eq!(
self.check_tag(tag.clone(), Priority::Normal).unwrap(),
pass,
"{tag:?} (expected {})",
if pass { "pass" } else { "fail" }
);
}
fn fek_does_pass(&self, fek: FileEventKind) {
self.tag_pass(Tag::FileEventKind(fek), true);
}
fn fek_doesnt_pass(&self, fek: FileEventKind) {
self.tag_pass(Tag::FileEventKind(fek), false);
}
fn source_does_pass(&self, source: Source) {
self.tag_pass(Tag::Source(source), true);
}
fn source_doesnt_pass(&self, source: Source) {
self.tag_pass(Tag::Source(source), false);
}
fn pid_does_pass(&self, pid: u32) {
self.tag_pass(Tag::Process(pid), true);
}
fn pid_doesnt_pass(&self, pid: u32) {
self.tag_pass(Tag::Process(pid), false);
}
fn signal_does_pass(&self, sig: MainSignal) {
self.tag_pass(Tag::Signal(sig), true);
}
fn signal_doesnt_pass(&self, sig: MainSignal) {
self.tag_pass(Tag::Signal(sig), false);
}
fn complete_does_pass(&self, exit: Option<ProcessEnd>) {
self.tag_pass(Tag::ProcessCompletion(exit), true);
}
fn complete_doesnt_pass(&self, exit: Option<ProcessEnd>) {
self.tag_pass(Tag::ProcessCompletion(exit), false);
}
}
fn tracing_init() {
use tracing_subscriber::{
fmt::{format::FmtSpan, Subscriber},
util::SubscriberInitExt,
EnvFilter,
};
Subscriber::builder()
.pretty()
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.with_env_filter(EnvFilter::from_default_env())
.finish()
.try_init()
.ok();
}
pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> IgnoreFilterer {
tracing_init();
let origin = dunce::canonicalize(".").unwrap().join(origin);
IgnoreFilterer(
IgnoreFilter::new(origin, ignore_files)
.await
.expect("making filterer"),
)
}
pub fn ig_file(name: &str) -> IgnoreFile {
let path = dunce::canonicalize(".")
.unwrap()
.join("tests")
.join("ignores")
.join(name);
IgnoreFile {
path,
applies_in: None,
applies_to: None,
}
}
pub trait Applies {
fn applies_in(self, origin: &str) -> Self;
fn applies_to(self, project_type: ProjectType) -> Self;
}
impl Applies for IgnoreFile {
fn applies_in(mut self, origin: &str) -> Self {
let origin = dunce::canonicalize(".").unwrap().join(origin);
self.applies_in = Some(origin);
self
}
fn applies_to(mut self, project_type: ProjectType) -> Self {
self.applies_to = Some(project_type);
self
}
}

View File

@ -0,0 +1,5 @@
# Changelog
## Next (YYYY-MM-DD)
- Initial release as a separate crate.

View File

@ -0,0 +1,57 @@
[package]
name = "watchexec-filterer-tagged"
version = "0.1.0"
authors = ["Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0"
description = "Watchexec filterer component using tagged filters"
keywords = ["watchexec", "filterer", "tags"]
documentation = "https://docs.rs/watchexec-filterer-tagged"
homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.58.0"
edition = "2021"
[dependencies]
miette = "4.7.1"
thiserror = "1.0.26"
tracing = "0.1.26"
dunce = "1.0.2"
globset = "0.4.8"
ignore = "0.4.18"
nom = "7.0.0"
regex = "1.5.4"
unicase = "2.6.0"
watchexec = { version = "2.0.0", path = "../../lib" }
[dependencies.ignore-files]
version = "1.0.0"
path = "../../ignore-files"
[dependencies.tokio]
version = "1.19.2"
features = [
"fs",
]
[dependencies.watchexec-filterer-ignore]
version = "1.0.0"
path = "../ignore"
[dev-dependencies]
tracing-subscriber = "0.3.6"
[dev-dependencies.project-origins]
version = "1.0.0"
path = "../../project-origins"
[dev-dependencies.tokio]
version = "1.19.2"
features = [
"fs",
"io-std",
"sync",
]

View File

@ -0,0 +1,17 @@
[![Crates.io page](https://badgen.net/crates/v/watchexec-filterer-tagged)](https://crates.io/crates/watchexec-filterer-tagged)
[![API Docs](https://docs.rs/watchexec-filterer-tagged/badge.svg)][docs]
[![Crate license: Apache 2.0](https://badgen.net/badge/license/Apache%202.0)][license]
![MSRV: 1.58.0 (minor)](https://badgen.net/badge/MSRV/1.58.0%20%28minor%29/0b7261)
[![CI status](https://github.com/watchexec/watchexec/actions/workflows/check.yml/badge.svg)](https://github.com/watchexec/watchexec/actions/workflows/check.yml)
# Watchexec filterer: tagged
_Experimental filterer using tagged filters._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.58.0 (incurs a minor semver bump).
- Status: maintained.
[docs]: https://docs.rs/watchexec-filterer-tagged
[license]: ../../../LICENSE

View File

@ -0,0 +1,10 @@
pre-release-commit-message = "release: filterer-tagged v{{version}}"
tag-prefix = "filterer-tagged-"
tag-message = "watchexec-filterer-tagged {{version}}"
[[pre-release-replacements]]
file = "CHANGELOG.md"
search = "^## Next.*$"
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
prerelease = true
max = 1

View File

@ -0,0 +1,81 @@
use std::collections::HashMap;
use ignore::gitignore::Gitignore;
use miette::Diagnostic;
use thiserror::Error;
use tokio::sync::watch::error::SendError;
use watchexec::error::RuntimeError;
use watchexec_filterer_ignore::IgnoreFilterer;
use crate::{Filter, Matcher};
/// Errors emitted by the TaggedFilterer.
#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]
#[diagnostic(url(docsrs))]
pub enum TaggedFiltererError {
/// Generic I/O error, with some context.
#[error("io({about}): {err}")]
#[diagnostic(code(watchexec::filter::io_error))]
IoError {
/// What it was about.
about: &'static str,
/// The I/O error which occurred.
#[source]
err: std::io::Error,
},
/// Error received when a tagged filter cannot be parsed.
#[error("cannot parse filter `{src}`: {err:?}")]
#[diagnostic(code(watchexec::filter::tagged::parse))]
Parse {
/// The source of the filter.
#[source_code]
src: String,
/// What went wrong.
err: nom::error::ErrorKind,
},
/// Error received when a filter cannot be added or removed from a tagged filter list.
#[error("cannot {action} filter: {err:?}")]
#[diagnostic(code(watchexec::filter::tagged::filter_change))]
FilterChange {
/// The action that was attempted.
action: &'static str,
/// The underlying error.
#[source]
err: SendError<HashMap<Matcher, Vec<Filter>>>,
},
/// Error received when a glob cannot be parsed.
#[error("cannot parse glob: {0}")]
#[diagnostic(code(watchexec::filter::tagged::glob_parse))]
GlobParse(#[source] ignore::Error),
/// Error received when a compiled globset cannot be changed.
#[error("cannot change compiled globset: {0:?}")]
#[diagnostic(code(watchexec::filter::tagged::globset_change))]
GlobsetChange(#[source] SendError<Option<Gitignore>>),
/// Error received about the internal ignore filterer.
#[error("ignore filterer: {0}")]
#[diagnostic(code(watchexec::filter::tagged::ignore))]
Ignore(#[source] ignore_files::Error),
/// Error received when a new ignore filterer cannot be swapped in.
#[error("cannot swap in new ignore filterer: {0:?}")]
#[diagnostic(code(watchexec::filter::tagged::ignore_swap))]
IgnoreSwap(#[source] SendError<IgnoreFilterer>),
}
impl From<TaggedFiltererError> for RuntimeError {
fn from(err: TaggedFiltererError) -> Self {
Self::Filterer {
kind: "tagged",
err: Box::new(err) as _,
}
}
}

View File

@ -1,5 +1,3 @@
//! Load "tagged filter files".
use std::{
env,
io::Error,
@ -7,14 +5,10 @@ use std::{
str::FromStr,
};
use ignore_files::{discover_file, IgnoreFile};
use tokio::fs::read_to_string;
use crate::{
error::TaggedFiltererError,
ignore::{discover_file, IgnoreFile},
};
use super::Filter;
use crate::{Filter, TaggedFiltererError};
/// A filter file.
///
@ -32,7 +26,7 @@ pub struct FilterFile(pub IgnoreFile);
/// All errors (permissions, etc) are collected and returned alongside the ignore files: you may
/// want to show them to the user while still using whatever ignores were successfully found. Errors
/// from files not being found are silently ignored (the files are just not returned).
pub async fn from_environment() -> (Vec<FilterFile>, Vec<Error>) {
pub async fn discover_files_from_environment() -> (Vec<FilterFile>, Vec<Error>) {
let mut files = Vec::new();
let mut errors = Vec::new();

View File

@ -0,0 +1,273 @@
use std::collections::HashSet;
use std::path::PathBuf;
use dunce::canonicalize;
use globset::Glob;
use regex::Regex;
use tracing::{trace, warn};
use unicase::UniCase;
use watchexec::event::Tag;
use crate::TaggedFiltererError;
/// A tagged filter.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filter {
/// Path the filter applies from.
pub in_path: Option<PathBuf>,
/// Which tag the filter applies to.
pub on: Matcher,
/// The operation to perform on the tag's value.
pub op: Op,
/// The pattern to match against the tag's value.
pub pat: Pattern,
/// If true, a positive match with this filter will override negative matches from previous
/// filters on the same tag, and negative matches will be ignored.
pub negate: bool,
}
impl Filter {
/// Matches the filter against a subject.
///
/// This is really an internal method to the tagged filterer machinery, exposed so you can build
/// your own filterer using the same types or the textual syntax. As such its behaviour is not
/// guaranteed to be stable (its signature is, though).
pub fn matches(&self, subject: impl AsRef<str>) -> Result<bool, TaggedFiltererError> {
let subject = subject.as_ref();
trace!(op=?self.op, pat=?self.pat, ?subject, "performing filter match");
Ok(match (self.op, &self.pat) {
(Op::Equal, Pattern::Exact(pat)) => UniCase::new(subject) == UniCase::new(pat),
(Op::NotEqual, Pattern::Exact(pat)) => UniCase::new(subject) != UniCase::new(pat),
(Op::Regex, Pattern::Regex(pat)) => pat.is_match(subject),
(Op::NotRegex, Pattern::Regex(pat)) => !pat.is_match(subject),
(Op::InSet, Pattern::Set(set)) => set.contains(subject),
(Op::InSet, Pattern::Exact(pat)) => subject == pat,
(Op::NotInSet, Pattern::Set(set)) => !set.contains(subject),
(Op::NotInSet, Pattern::Exact(pat)) => subject != pat,
(op @ Op::Glob | op @ Op::NotGlob, Pattern::Glob(glob)) => {
// FIXME: someway that isn't this horrible
match Glob::new(glob) {
Ok(glob) => {
let matches = glob.compile_matcher().is_match(subject);
match op {
Op::Glob => matches,
Op::NotGlob => !matches,
_ => unreachable!(),
}
}
Err(err) => {
warn!(
"failed to compile glob for non-path match, skipping (pass): {}",
err
);
true
}
}
}
(op, pat) => {
warn!(
"trying to match pattern {:?} with op {:?}, that cannot work",
pat, op
);
false
}
})
}
/// Create a filter from a gitignore-style glob pattern.
///
/// The optional path is for the `in_path` field of the filter. When parsing gitignore files, it
/// should be set to the path of the _directory_ the ignore file is in.
///
/// 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.
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));
Self {
in_path,
on: Matcher::Path,
op: Op::NotGlob,
pat: Pattern::Glob(glob.to_string()),
negate,
}
}
/// Returns the filter with its `in_path` canonicalised.
pub fn canonicalised(mut self) -> Result<Self, TaggedFiltererError> {
if let Some(ctx) = self.in_path {
self.in_path =
Some(
canonicalize(&ctx).map_err(|err| TaggedFiltererError::IoError {
about: "canonicalise Filter in_path",
err,
})?,
);
trace!(canon=?ctx, "canonicalised in_path");
}
Ok(self)
}
}
/// What a filter matches on.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum Matcher {
/// The presence of a tag on an event.
Tag,
/// A path in a filesystem event. Paths are always canonicalised.
///
/// Note that there may be multiple paths in an event (e.g. both source and destination for renames), and filters
/// will be matched on all of them.
Path,
/// The file type of an object in a filesystem event.
///
/// This is not guaranteed to be present for every filesystem event.
///
/// It can be any of these values: `file`, `dir`, `symlink`, `other`. That last one means
/// "not any of the first three."
FileType,
/// The [`EventKind`][notify::event::EventKind] of a filesystem event.
///
/// This is the Debug representation of the event kind. Examples:
/// - `Access(Close(Write))`
/// - `Modify(Data(Any))`
/// - `Modify(Metadata(Permissions))`
/// - `Remove(Folder)`
///
/// You should probably use globs or regexes to match these, ex:
/// - `Create(*)`
/// - `Modify\(Name\(.+`
FileEventKind,
/// The [event source][crate::event::Source] the event came from.
///
/// These are the lowercase names of the variants.
Source,
/// The ID of the process which caused the event.
///
/// Note that it's rare for events to carry this information.
Process,
/// A signal sent to the main process.
///
/// This can be matched both on the signal number as an integer, and on the signal name as a
/// string. On Windows, only `BREAK` is supported; `CTRL_C` parses but won't work. Matching is
/// on both uppercase and lowercase forms.
///
/// Interrupt signals (`TERM` and `INT` on Unix, `CTRL_C` on Windows) are parsed, but these are
/// marked Urgent internally to Watchexec, and thus bypass filtering entirely.
Signal,
/// The exit status of a subprocess.
///
/// This is only present for events issued when the subprocess exits. The value is matched on
/// both the exit code as an integer, and either `success` or `fail`, whichever succeeds.
ProcessCompletion,
/// The [`Priority`] of the event.
///
/// This is never `urgent`, as urgent events bypass filtering.
Priority,
}
impl Matcher {
pub(crate) fn from_tag(tag: &Tag) -> &'static [Self] {
match tag {
Tag::Path {
file_type: None, ..
} => &[Matcher::Path],
Tag::Path { .. } => &[Matcher::Path, Matcher::FileType],
Tag::FileEventKind(_) => &[Matcher::FileEventKind],
Tag::Source(_) => &[Matcher::Source],
Tag::Process(_) => &[Matcher::Process],
Tag::Signal(_) => &[Matcher::Signal],
Tag::ProcessCompletion(_) => &[Matcher::ProcessCompletion],
_ => {
warn!("unhandled tag: {:?}", tag);
&[]
}
}
}
}
/// How a filter value is interpreted.
///
/// - `==` and `!=` match on the exact value as string equality (case-insensitively),
/// - `~=` and `~!` match using a [regex],
/// - `*=` and `*!` match using a glob, either via [globset] or [ignore]
/// - `:=` and `:!` match via exact string comparisons, but on any of the list of values separated
/// by `,`
/// - `=`, the "auto" operator, behaves as `*=` if the matcher is `Path`, and as `==` otherwise.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Op {
/// The auto operator, `=`, resolves to `*=` or `==` depending on the matcher.
Auto,
/// The `==` operator, matches on exact string equality.
Equal,
/// The `!=` operator, matches on exact string inequality.
NotEqual,
/// The `~=` operator, matches on a regex.
Regex,
/// The `~!` operator, matches on a regex (matches are fails).
NotRegex,
/// The `*=` operator, matches on a glob.
Glob,
/// The `*!` operator, matches on a glob (matches are fails).
NotGlob,
/// The `:=` operator, matches (with string compares) on a set of values (belongs are passes).
InSet,
/// The `:!` operator, matches on a set of values (belongs are fails).
NotInSet,
}
/// A filter value (pattern to match with).
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Pattern {
/// An exact string.
Exact(String),
/// A regex.
Regex(Regex),
/// A glob.
///
/// This is stored as a string as globs are compiled together rather than on a per-filter basis.
Glob(String),
/// A set of exact strings.
Set(HashSet<String>),
}
impl PartialEq<Self> for Pattern {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Exact(l), Self::Exact(r)) | (Self::Glob(l), Self::Glob(r)) => l == r,
(Self::Regex(l), Self::Regex(r)) => l.as_str() == r.as_str(),
(Self::Set(l), Self::Set(r)) => l == r,
_ => false,
}
}
}
impl Eq for Pattern {}

View File

@ -1,106 +1,27 @@
//! A complex filterer that can match any event tag and supports different matching operators.
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use dunce::canonicalize;
use globset::Glob;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use ignore::Match;
use tracing::{debug, trace, trace_span, warn};
use unicase::UniCase;
use ignore::{
gitignore::{Gitignore, GitignoreBuilder},
Match,
};
use ignore_files::{IgnoreFile, IgnoreFilter};
use tracing::{debug, trace, trace_span};
use watchexec::{
error::RuntimeError,
event::{Event, FileType, Priority, ProcessEnd, Tag},
filter::Filterer,
signal::{process::SubSignal, source::MainSignal},
};
use watchexec_filterer_ignore::IgnoreFilterer;
use crate::error::RuntimeError;
use crate::error::TaggedFiltererError;
use crate::event::{Event, FileType, Priority, ProcessEnd, Tag};
use crate::filter::Filterer;
use crate::ignore::{IgnoreFile, IgnoreFilterer};
use crate::signal::process::SubSignal;
use crate::signal::source::MainSignal;
use crate::swaplock::SwapLock;
use crate::{swaplock::SwapLock, Filter, Matcher, Op, Pattern, TaggedFiltererError};
// to make filters
pub use regex::Regex;
pub mod files;
mod parse;
/// A filterer implementation that exposes the full capabilities of Watchexec.
/// A complex filterer that can match any event tag and supports different matching operators.
///
/// **Note:** This filterer is experimental, and behaviour may change without semver notice. However,
/// types and its API are held to semver. This notice will eventually be removed when it stabilises.
///
/// Filters match against [event tags][Tag]; can be exact matches, glob matches, regex matches, or
/// set matches; can reverse the match (equal/not equal, etc); and can be negated.
///
/// [Filters][Filter] can be generated from your application and inserted directly, or they can be
/// parsed from a textual format:
///
/// ```text
/// [!]{Matcher}{Op}{Value}
/// ```
///
/// For example:
///
/// ```text
/// path==/foo/bar
/// path*=**/bar
/// path~=bar$
/// !kind=file
/// ```
///
/// There is a set of [operators][Op]:
/// - `==` and `!=`: exact match and exact not match (case insensitive)
/// - `~=` and `~!`: regex match and regex not match
/// - `*=` and `*!`: glob match and glob not match
/// - `:=` and `:!`: set match and set not match
///
/// Sets are a list of values separated by `,`.
///
/// In addition to the two-symbol operators, there is the `=` "auto" operator, which maps to the
/// most convenient operator for the given _matcher_. The current mapping is:
///
/// | Matcher | Operator |
/// |-------------------------------------------------|---------------|
/// | [Tag](Matcher::Tag) | `:=` (in set) |
/// | [Path](Matcher::Path) | `*=` (glob) |
/// | [FileType](Matcher::FileType) | `:=` (in set) |
/// | [FileEventKind](Matcher::FileEventKind) | `*=` (glob) |
/// | [Source](Matcher::Source) | `:=` (in set) |
/// | [Process](Matcher::Process) | `:=` (in set) |
/// | [Signal](Matcher::Signal) | `:=` (in set) |
/// | [ProcessCompletion](Matcher::ProcessCompletion) | `*=` (glob) |
/// | [Priority](Matcher::Priority) | `:=` (in set) |
///
/// [Matchers][Matcher] correspond to Tags, but are not one-to-one: the `path` matcher operates on
/// the `path` part of the `Path` tag, and the `type` matcher operates on the `file_type`, for
/// example.
///
/// | Matcher | Syntax | Tag |
/// |------------------------------------|----------|----------------------------------------------|
/// | [Tag](Matcher::Tag) | `tag` | _the presence of a Tag on the event_ |
/// | [Path](Matcher::Path) | `path` | [Path](Tag::Path) (`path` field) |
/// | [FileType](Matcher::FileType) | `type` | [Path](Tag::Path) (`file_type` field, when Some) |
/// | [FileEventKind](Matcher::FileEventKind) | `kind` or `fek` | [FileEventKind](Tag::FileEventKind) |
/// | [Source](Matcher::Source) | `source` or `src` | [Source](Tag::Source) |
/// | [Process](Matcher::Process) | `process` or `pid` | [Process](Tag::Process) |
/// | [Signal](Matcher::Signal) | `signal` | [Signal](Tag::Signal) |
/// | [ProcessCompletion](Matcher::ProcessCompletion) | `complete` or `exit` | [ProcessCompletion](Tag::ProcessCompletion) |
/// | [Priority](Matcher::Priority) | `priority` | special: event [Priority] |
///
/// Filters are checked in order, grouped per tag and per matcher. Filter groups may be checked in
/// any order, but the filters in the groups are checked in add order. Path glob filters are always
/// checked first, for internal reasons.
///
/// The `negate` boolean field behaves specially: it is not operator negation, but rather the same
/// kind of behaviour that is applied to `!`-prefixed globs in gitignore files: if a negated filter
/// matches the event, the result of the event checking for that matcher is reverted to `true`, even
/// if a previous filter set it to `false`. Unmatched negated filters are ignored.
///
/// Glob syntax is as supported by the [ignore] crate for Paths, and by [globset] otherwise. (As of
/// writing, the ignore crate uses globset internally). Regex syntax is the default syntax of the
/// [regex] crate.
/// See the crate-level documentation for more information.
#[derive(Debug)]
pub struct TaggedFilterer {
/// The directory the project is in, its origin.
@ -376,7 +297,7 @@ impl TaggedFilterer {
})?;
Ok(Arc::new(Self {
filters: SwapLock::new(HashMap::new()),
ignore_filterer: SwapLock::new(IgnoreFilterer::empty(&origin)),
ignore_filterer: SwapLock::new(IgnoreFilterer(IgnoreFilter::empty(&origin))),
glob_compiled: SwapLock::new(None),
not_glob_compiled: SwapLock::new(None),
workdir: canonicalize(workdir.into()).map_err(|err| TaggedFiltererError::IoError {
@ -589,7 +510,8 @@ impl TaggedFilterer {
pub async fn add_ignore_file(&self, file: &IgnoreFile) -> Result<(), TaggedFiltererError> {
let mut new = { self.ignore_filterer.borrow().clone() };
new.add_file(file)
new.0
.add_file(file)
.await
.map_err(TaggedFiltererError::Ignore)?;
self.ignore_filterer
@ -618,261 +540,3 @@ impl TaggedFilterer {
Ok(())
}
}
/// A tagged filter.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filter {
/// Path the filter applies from.
pub in_path: Option<PathBuf>,
/// Which tag the filter applies to.
pub on: Matcher,
/// The operation to perform on the tag's value.
pub op: Op,
/// The pattern to match against the tag's value.
pub pat: Pattern,
/// If true, a positive match with this filter will override negative matches from previous
/// filters on the same tag, and negative matches will be ignored.
pub negate: bool,
}
impl Filter {
/// Matches the filter against a subject.
///
/// This is really an internal method to the tagged filterer machinery, exposed so you can build
/// your own filterer using the same types or the textual syntax. As such its behaviour is not
/// guaranteed to be stable (its signature is, though).
pub fn matches(&self, subject: impl AsRef<str>) -> Result<bool, TaggedFiltererError> {
let subject = subject.as_ref();
trace!(op=?self.op, pat=?self.pat, ?subject, "performing filter match");
Ok(match (self.op, &self.pat) {
(Op::Equal, Pattern::Exact(pat)) => UniCase::new(subject) == UniCase::new(pat),
(Op::NotEqual, Pattern::Exact(pat)) => UniCase::new(subject) != UniCase::new(pat),
(Op::Regex, Pattern::Regex(pat)) => pat.is_match(subject),
(Op::NotRegex, Pattern::Regex(pat)) => !pat.is_match(subject),
(Op::InSet, Pattern::Set(set)) => set.contains(subject),
(Op::InSet, Pattern::Exact(pat)) => subject == pat,
(Op::NotInSet, Pattern::Set(set)) => !set.contains(subject),
(Op::NotInSet, Pattern::Exact(pat)) => subject != pat,
(op @ Op::Glob | op @ Op::NotGlob, Pattern::Glob(glob)) => {
// FIXME: someway that isn't this horrible
match Glob::new(glob) {
Ok(glob) => {
let matches = glob.compile_matcher().is_match(subject);
match op {
Op::Glob => matches,
Op::NotGlob => !matches,
_ => unreachable!(),
}
}
Err(err) => {
warn!(
"failed to compile glob for non-path match, skipping (pass): {}",
err
);
true
}
}
}
(op, pat) => {
warn!(
"trying to match pattern {:?} with op {:?}, that cannot work",
pat, op
);
false
}
})
}
/// Create a filter from a gitignore-style glob pattern.
///
/// The optional path is for the `in_path` field of the filter. When parsing gitignore files, it
/// should be set to the path of the _directory_ the ignore file is in.
///
/// 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.
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));
Self {
in_path,
on: Matcher::Path,
op: Op::NotGlob,
pat: Pattern::Glob(glob.to_string()),
negate,
}
}
/// Returns the filter with its `in_path` canonicalised.
pub fn canonicalised(mut self) -> Result<Self, TaggedFiltererError> {
if let Some(ctx) = self.in_path {
self.in_path =
Some(
canonicalize(&ctx).map_err(|err| TaggedFiltererError::IoError {
about: "canonicalise Filter in_path",
err,
})?,
);
trace!(canon=?ctx, "canonicalised in_path");
}
Ok(self)
}
}
/// What a filter matches on.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum Matcher {
/// The presence of a tag on an event.
Tag,
/// A path in a filesystem event. Paths are always canonicalised.
///
/// Note that there may be multiple paths in an event (e.g. both source and destination for renames), and filters
/// will be matched on all of them.
Path,
/// The file type of an object in a filesystem event.
///
/// This is not guaranteed to be present for every filesystem event.
///
/// It can be any of these values: `file`, `dir`, `symlink`, `other`. That last one means
/// "not any of the first three."
FileType,
/// The [`EventKind`][notify::event::EventKind] of a filesystem event.
///
/// This is the Debug representation of the event kind. Examples:
/// - `Access(Close(Write))`
/// - `Modify(Data(Any))`
/// - `Modify(Metadata(Permissions))`
/// - `Remove(Folder)`
///
/// You should probably use globs or regexes to match these, ex:
/// - `Create(*)`
/// - `Modify\(Name\(.+`
FileEventKind,
/// The [event source][crate::event::Source] the event came from.
///
/// These are the lowercase names of the variants.
Source,
/// The ID of the process which caused the event.
///
/// Note that it's rare for events to carry this information.
Process,
/// A signal sent to the main process.
///
/// This can be matched both on the signal number as an integer, and on the signal name as a
/// string. On Windows, only `BREAK` is supported; `CTRL_C` parses but won't work. Matching is
/// on both uppercase and lowercase forms.
///
/// Interrupt signals (`TERM` and `INT` on Unix, `CTRL_C` on Windows) are parsed, but these are
/// marked Urgent internally to Watchexec, and thus bypass filtering entirely.
Signal,
/// The exit status of a subprocess.
///
/// This is only present for events issued when the subprocess exits. The value is matched on
/// both the exit code as an integer, and either `success` or `fail`, whichever succeeds.
ProcessCompletion,
/// The [`Priority`] of the event.
///
/// This is never `urgent`, as urgent events bypass filtering.
Priority,
}
impl Matcher {
fn from_tag(tag: &Tag) -> &'static [Self] {
match tag {
Tag::Path {
file_type: None, ..
} => &[Matcher::Path],
Tag::Path { .. } => &[Matcher::Path, Matcher::FileType],
Tag::FileEventKind(_) => &[Matcher::FileEventKind],
Tag::Source(_) => &[Matcher::Source],
Tag::Process(_) => &[Matcher::Process],
Tag::Signal(_) => &[Matcher::Signal],
Tag::ProcessCompletion(_) => &[Matcher::ProcessCompletion],
}
}
}
/// How a filter value is interpreted.
///
/// - `==` and `!=` match on the exact value as string equality (case-insensitively),
/// - `~=` and `~!` match using a [regex],
/// - `*=` and `*!` match using a glob, either via [globset] or [ignore]
/// - `:=` and `:!` match via exact string comparisons, but on any of the list of values separated
/// by `,`
/// - `=`, the "auto" operator, behaves as `*=` if the matcher is `Path`, and as `==` otherwise.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Op {
/// The auto operator, `=`, resolves to `*=` or `==` depending on the matcher.
Auto,
/// The `==` operator, matches on exact string equality.
Equal,
/// The `!=` operator, matches on exact string inequality.
NotEqual,
/// The `~=` operator, matches on a regex.
Regex,
/// The `~!` operator, matches on a regex (matches are fails).
NotRegex,
/// The `*=` operator, matches on a glob.
Glob,
/// The `*!` operator, matches on a glob (matches are fails).
NotGlob,
/// The `:=` operator, matches (with string compares) on a set of values (belongs are passes).
InSet,
/// The `:!` operator, matches on a set of values (belongs are fails).
NotInSet,
}
/// A filter value (pattern to match with).
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Pattern {
/// An exact string.
Exact(String),
/// A regex.
Regex(Regex),
/// A glob.
///
/// This is stored as a string as globs are compiled together rather than on a per-filter basis.
Glob(String),
/// A set of exact strings.
Set(HashSet<String>),
}
impl PartialEq<Self> for Pattern {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Exact(l), Self::Exact(r)) | (Self::Glob(l), Self::Glob(r)) => l == r,
(Self::Regex(l), Self::Regex(r)) => l.as_str() == r.as_str(),
(Self::Set(l), Self::Set(r)) => l == r,
_ => false,
}
}
}
impl Eq for Pattern {}

View File

@ -0,0 +1,87 @@
//! A filterer implementation that exposes the full capabilities of Watchexec.
//!
//! Filters match against [event tags][Tag]; can be exact matches, glob matches, regex matches, or
//! set matches; can reverse the match (equal/not equal, etc); and can be negated.
//!
//! [Filters][Filter] can be generated from your application and inserted directly, or they can be
//! parsed from a textual format:
//!
//! ```text
//! [!]{Matcher}{Op}{Value}
//! ```
//!
//! For example:
//!
//! ```text
//! path==/foo/bar
//! path*=**/bar
//! path~=bar$
//! !kind=file
//! ```
//!
//! There is a set of [operators][Op]:
//! - `==` and `!=`: exact match and exact not match (case insensitive)
//! - `~=` and `~!`: regex match and regex not match
//! - `*=` and `*!`: glob match and glob not match
//! - `:=` and `:!`: set match and set not match
//!
//! Sets are a list of values separated by `,`.
//!
//! In addition to the two-symbol operators, there is the `=` "auto" operator, which maps to the
//! most convenient operator for the given _matcher_. The current mapping is:
//!
//! | Matcher | Operator |
//! |-------------------------------------------------|---------------|
//! | [Tag](Matcher::Tag) | `:=` (in set) |
//! | [Path](Matcher::Path) | `*=` (glob) |
//! | [FileType](Matcher::FileType) | `:=` (in set) |
//! | [FileEventKind](Matcher::FileEventKind) | `*=` (glob) |
//! | [Source](Matcher::Source) | `:=` (in set) |
//! | [Process](Matcher::Process) | `:=` (in set) |
//! | [Signal](Matcher::Signal) | `:=` (in set) |
//! | [ProcessCompletion](Matcher::ProcessCompletion) | `*=` (glob) |
//! | [Priority](Matcher::Priority) | `:=` (in set) |
//!
//! [Matchers][Matcher] correspond to Tags, but are not one-to-one: the `path` matcher operates on
//! the `path` part of the `Path` tag, and the `type` matcher operates on the `file_type`, for
//! example.
//!
//! | Matcher | Syntax | Tag |
//! |------------------------------------|----------|----------------------------------------------|
//! | [Tag](Matcher::Tag) | `tag` | _the presence of a Tag on the event_ |
//! | [Path](Matcher::Path) | `path` | [Path](Tag::Path) (`path` field) |
//! | [FileType](Matcher::FileType) | `type` | [Path](Tag::Path) (`file_type` field, when Some) |
//! | [FileEventKind](Matcher::FileEventKind) | `kind` or `fek` | [FileEventKind](Tag::FileEventKind) |
//! | [Source](Matcher::Source) | `source` or `src` | [Source](Tag::Source) |
//! | [Process](Matcher::Process) | `process` or `pid` | [Process](Tag::Process) |
//! | [Signal](Matcher::Signal) | `signal` | [Signal](Tag::Signal) |
//! | [ProcessCompletion](Matcher::ProcessCompletion) | `complete` or `exit` | [ProcessCompletion](Tag::ProcessCompletion) |
//! | [Priority](Matcher::Priority) | `priority` | special: event [Priority] |
//!
//! Filters are checked in order, grouped per tag and per matcher. Filter groups may be checked in
//! any order, but the filters in the groups are checked in add order. Path glob filters are always
//! checked first, for internal reasons.
//!
//! The `negate` boolean field behaves specially: it is not operator negation, but rather the same
//! kind of behaviour that is applied to `!`-prefixed globs in gitignore files: if a negated filter
//! matches the event, the result of the event checking for that matcher is reverted to `true`, even
//! if a previous filter set it to `false`. Unmatched negated filters are ignored.
//!
//! Glob syntax is as supported by the [ignore] crate for Paths, and by [globset] otherwise. (As of
//! writing, the ignore crate uses globset internally). Regex syntax is the default syntax of the
//! [regex] crate.
// to make filters
pub use regex::Regex;
pub use error::*;
pub use files::*;
pub use filter::*;
pub use filterer::*;
mod error;
mod files;
mod filter;
mod filterer;
mod parse;
mod swaplock;

View File

@ -11,9 +11,7 @@ use nom::{
use regex::Regex;
use tracing::trace;
use crate::error::TaggedFiltererError;
use super::*;
use crate::{Filter, Matcher, Op, Pattern, TaggedFiltererError};
impl FromStr for Filter {
type Err = TaggedFiltererError;

View File

@ -1,40 +1,21 @@
#![allow(dead_code)]
use std::{
ffi::OsString,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use ignore_files::{IgnoreFile, IgnoreFilter};
use project_origins::ProjectType;
use watchexec::{
error::RuntimeError,
event::{filekind::FileEventKind, Event, FileType, Priority, ProcessEnd, Source, Tag},
filter::{
globset::GlobsetFilterer,
tagged::{files::FilterFile, Filter, Matcher, Op, Pattern, TaggedFilterer},
Filterer,
},
ignore::{IgnoreFile, IgnoreFilterer},
project::ProjectType,
filter::Filterer,
signal::source::MainSignal,
};
pub mod ignore {
pub use super::ig_file as file;
pub use super::ignore_filt as filt;
pub use super::Applies;
pub use super::PathHarness;
pub use watchexec::event::Priority;
}
pub mod globset {
pub use super::globset_filt as filt;
pub use super::ig_file as file;
pub use super::Applies;
pub use super::PathHarness;
pub use watchexec::event::Priority;
}
use watchexec_filterer_ignore::IgnoreFilterer;
use watchexec_filterer_tagged::{Filter, FilterFile, Matcher, Op, Pattern, TaggedFilterer};
pub mod tagged {
pub use super::ig_file as file;
@ -120,7 +101,6 @@ pub trait PathHarness: Filterer {
}
}
impl PathHarness for GlobsetFilterer {}
impl PathHarness for TaggedFilterer {}
impl PathHarness for IgnoreFilterer {}
@ -225,28 +205,10 @@ fn tracing_init() {
.ok();
}
pub async fn globset_filt(
filters: &[&str],
ignores: &[&str],
extensions: &[&str],
) -> GlobsetFilterer {
let origin = dunce::canonicalize(".").unwrap();
tracing_init();
GlobsetFilterer::new(
origin,
filters.iter().map(|s| (s.to_string(), None)),
ignores.iter().map(|s| (s.to_string(), None)),
vec![],
extensions.iter().map(OsString::from),
)
.await
.expect("making filterer")
}
pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> IgnoreFilterer {
pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> IgnoreFilter {
tracing_init();
let origin = dunce::canonicalize(".").unwrap().join(origin);
IgnoreFilterer::new(origin, ignore_files)
IgnoreFilter::new(origin, ignore_files)
.await
.expect("making filterer")
}

View File

@ -0,0 +1,11 @@
Cargo.toml
package.json
*.gemspec
test-*
*.sw*
sources.*/
/output.*
**/possum
zebra/**
elep/**/hant
song/**/bird/

View File

@ -2,10 +2,11 @@ use std::num::{NonZeroI32, NonZeroI64};
use watchexec::{
event::{filekind::*, ProcessEnd, Source},
filter::tagged::TaggedFilterer,
signal::{process::SubSignal, source::MainSignal},
};
use watchexec_filterer_tagged::TaggedFilterer;
mod helpers;
use helpers::tagged::*;

View File

@ -1,9 +1,6 @@
use std::{collections::HashSet, str::FromStr};
use watchexec::{
error::TaggedFiltererError,
filter::tagged::{Filter, Matcher, Op, Pattern, Regex},
};
use watchexec_filterer_tagged::{Filter, Matcher, Op, Pattern, Regex, TaggedFiltererError};
mod helpers;
use helpers::tagged::*;

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use watchexec::filter::tagged::TaggedFilterer;
use watchexec_filterer_tagged::TaggedFilterer;
mod helpers;
use helpers::tagged::*;

View File

@ -0,0 +1,5 @@
# Changelog
## Next (YYYY-MM-DD)
- Initial release as a separate crate.

View File

@ -0,0 +1,29 @@
[package]
name = "ignore-files"
version = "1.0.0"
authors = ["Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0"
description = "Find, parse, and interpret ignore files"
keywords = ["ignore", "files", "discover", "find"]
documentation = "https://docs.rs/ignore-files"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.58.0"
edition = "2021"
[dependencies]
futures = "0.3.21"
ignore = "0.4.18"
git-config = "0.2.0"
tokio = { version = "1.19.2", default-features = false, features = ["fs"] }
tracing = "0.1.35"
dunce = "1.0.2"
miette = "4.7.1"
thiserror = "1.0.31"
[dependencies.project-origins]
version = "1.0.0"
path = "../project-origins"

View File

@ -0,0 +1,17 @@
[![Crates.io page](https://badgen.net/crates/v/ignore-files)](https://crates.io/crates/ignore-files)
[![API Docs](https://docs.rs/ignore-files/badge.svg)][docs]
[![Crate license: Apache 2.0](https://badgen.net/badge/license/Apache%202.0)][license]
![MSRV: 1.58.0 (minor)](https://badgen.net/badge/MSRV/1.58.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)
# Ignore files
_Find, parse, and interpret ignore files._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.58.0 (incurs a minor semver bump).
- Status: done.
[docs]: https://docs.rs/ignore-files
[license]: ../../LICENSE

View File

@ -0,0 +1,10 @@
pre-release-commit-message = "release: ignore-files v{{version}}"
tag-prefix = "ignore-files-"
tag-message = "ignore-files {{version}}"
[[pre-release-replacements]]
file = "CHANGELOG.md"
search = "^## Next.*$"
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
prerelease = true
max = 1

View File

@ -1,7 +1,3 @@
use git_config::{
file::{from_paths, GitConfig},
values::Path as GitPath,
};
use std::{
collections::HashSet,
env,
@ -9,28 +5,22 @@ use std::{
path::{Path, PathBuf},
};
use git_config::{
file::{from_paths, GitConfig},
values::Path as GitPath,
};
use project_origins::ProjectType;
use tokio::fs::{metadata, read_dir};
use tracing::{trace, trace_span};
use crate::{paths::PATH_SEPARATOR, project::ProjectType};
use crate::{IgnoreFile, IgnoreFilter};
use super::IgnoreFilterer;
/// An ignore file.
///
/// This records both the path to the ignore file and some basic metadata about it: which project
/// type it applies to if any, and which subtree it applies in if any (`None` = global ignore file).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IgnoreFile {
/// The path to the ignore file.
pub path: PathBuf,
/// The path to the subtree the ignore file applies to, or `None` for global ignores.
pub applies_in: Option<PathBuf>,
/// Which project type the ignore file applies to, or was found through.
pub applies_to: Option<ProjectType>,
}
/// The separator for paths used in environment variables.
#[cfg(unix)]
const PATH_SEPARATOR: &str = ":";
/// The separator for paths used in environment variables.
#[cfg(not(unix))]
const PATH_SEPARATOR: &str = ";";
/// Finds all ignore files in the given directory and subdirectories.
///
@ -184,24 +174,29 @@ pub async fn from_origin(path: impl AsRef<Path>) -> (Vec<IgnoreFile>, Vec<Error>
/// Finds all ignore files that apply to the current runtime.
///
/// Takes an optional `appname` for the calling application for looking at an environment variable
/// and an application-specific config location.
///
/// This considers:
/// - User-specific git ignore files (e.g. `~/.gitignore`)
/// - Git configurable ignore files (e.g. with `core.excludesFile` in system or user config)
/// - `$XDG_CONFIG_HOME/watchexec/ignore`, as well as other locations (APPDATA on Windows…)
/// - Files from the `WATCHEXEC_IGNORE_FILES` environment variable (separated the same was as `PATH`)
/// - `$XDG_CONFIG_HOME/{appname}/ignore`, as well as other locations (APPDATA on Windows…)
/// - Files from the `{APPNAME}_IGNORE_FILES` environment variable (separated the same was as `PATH`)
///
/// All errors (permissions, etc) are collected and returned alongside the ignore files: you may
/// want to show them to the user while still using whatever ignores were successfully found. Errors
/// from files not being found are silently ignored (the files are just not returned).
pub async fn from_environment() -> (Vec<IgnoreFile>, Vec<Error>) {
pub async fn from_environment(appname: Option<&str>) -> (Vec<IgnoreFile>, Vec<Error>) {
let mut files = Vec::new();
let mut errors = Vec::new();
for path in env::var("WATCHEXEC_IGNORE_FILES")
.unwrap_or_default()
.split(PATH_SEPARATOR)
{
discover_file(&mut files, &mut errors, None, None, PathBuf::from(path)).await;
if let Some(name) = appname {
for path in env::var(format!("{}_IGNORE_FILES", name.to_uppercase()))
.unwrap_or_default()
.split(PATH_SEPARATOR)
{
discover_file(&mut files, &mut errors, None, None, PathBuf::from(path)).await;
}
}
let mut found_git_global = false;
@ -277,23 +272,25 @@ pub async fn from_environment() -> (Vec<IgnoreFile>, Vec<Error>) {
}
}
let mut wgis = Vec::with_capacity(5);
if let Ok(home) = env::var("XDG_CONFIG_HOME") {
wgis.push(Path::new(&home).join("watchexec/ignore"));
}
if let Ok(home) = env::var("APPDATA") {
wgis.push(Path::new(&home).join("watchexec/ignore"));
}
if let Ok(home) = env::var("USERPROFILE") {
wgis.push(Path::new(&home).join(".watchexec/ignore"));
}
if let Ok(home) = env::var("HOME") {
wgis.push(Path::new(&home).join(".watchexec/ignore"));
}
if let Some(name) = appname {
let mut wgis = Vec::with_capacity(4);
if let Ok(home) = env::var("XDG_CONFIG_HOME") {
wgis.push(Path::new(&home).join(format!("{name}/ignore")));
}
if let Ok(home) = env::var("APPDATA") {
wgis.push(Path::new(&home).join(format!("{name}/ignore")));
}
if let Ok(home) = env::var("USERPROFILE") {
wgis.push(Path::new(&home).join(format!(".{name}/ignore")));
}
if let Ok(home) = env::var("HOME") {
wgis.push(Path::new(&home).join(format!(".{name}/ignore")));
}
for path in wgis {
if discover_file(&mut files, &mut errors, None, None, path).await {
break;
for path in wgis {
if discover_file(&mut files, &mut errors, None, None, path).await {
break;
}
}
}
@ -302,8 +299,11 @@ pub async fn from_environment() -> (Vec<IgnoreFile>, Vec<Error>) {
// TODO: add context to these errors
/// 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.
#[inline]
pub(crate) async fn discover_file(
pub async fn discover_file(
files: &mut Vec<IgnoreFile>,
errors: &mut Vec<Error>,
applies_in: Option<PathBuf>,
@ -348,7 +348,7 @@ struct DirTourist {
to_visit: Vec<PathBuf>,
to_skip: HashSet<PathBuf>,
pub errors: Vec<std::io::Error>,
filter: IgnoreFilterer,
filter: IgnoreFilter,
}
#[derive(Debug)]
@ -362,7 +362,7 @@ impl DirTourist {
pub async fn new(base: &Path, files: &[IgnoreFile]) -> Result<Self, Error> {
let base = dunce::canonicalize(base)?;
trace!("create IgnoreFilterer for visiting directories");
let mut filter = IgnoreFilterer::new(&base, files)
let mut filter = IgnoreFilter::new(&base, files)
.await
.map_err(|err| Error::new(ErrorKind::Other, err))?;

View File

@ -0,0 +1,40 @@
use std::path::PathBuf;
use miette::Diagnostic;
use thiserror::Error;
#[derive(Debug, Error, Diagnostic)]
#[non_exhaustive]
pub enum Error {
/// Error received when an [`IgnoreFile`] cannot be read.
///
/// [`IgnoreFile`]: crate::IgnoreFile
#[error("cannot read ignore '{file}': {err}")]
#[diagnostic(code(ignore_file::read))]
Read {
/// The path to the erroring ignore file.
file: PathBuf,
/// The underlying error.
#[source]
err: std::io::Error,
},
/// Error received when parsing a glob fails.
#[error("cannot parse glob from ignore '{file:?}': {err}")]
#[diagnostic(code(ignore_file::glob))]
Glob {
/// The path to the erroring ignore file.
file: Option<PathBuf>,
/// The underlying error.
#[source]
err: ignore::Error,
// TODO: extract glob error into diagnostic
},
/// Multiple related [`Error`]s.
#[error("multiple: {0:?}")]
#[diagnostic(code(ignore_file::set))]
Multi(#[related] Vec<Error>),
}

View File

@ -2,36 +2,27 @@ use std::path::{Path, PathBuf};
use futures::stream::{FuturesUnordered, StreamExt};
use ignore::{
gitignore::{Gitignore, GitignoreBuilder},
gitignore::{Gitignore, GitignoreBuilder, Glob},
Match,
};
use tokio::fs::read_to_string;
use tracing::{trace, trace_span};
use crate::{
error::RuntimeError,
event::{Event, FileType, Priority},
filter::Filterer,
};
use crate::{Error, IgnoreFile};
use super::files::IgnoreFile;
/// A path-only filterer dedicated to ignore files.
/// A mutable filter dedicated to ignore files and trees of ignore files.
///
/// This reads and compiles ignore files, and should be used for handling ignore files. It's created
/// with a project origin and a list of ignore files, and new ignore files can be added later
/// (unless [`finish`](IgnoreFilterer::finish()) is called).
///
/// It implements [`Filterer`] so it can be used directly in another filterer; it is not designed to
/// be used as a standalone filterer.
#[derive(Clone, Debug)]
pub struct IgnoreFilterer {
pub struct IgnoreFilter {
origin: PathBuf,
builder: Option<GitignoreBuilder>,
compiled: Gitignore,
}
impl IgnoreFilterer {
impl IgnoreFilter {
/// Create a new empty filterer.
///
/// Prefer [`new()`](IgnoreFilterer::new()) if you have ignore files ready to use.
@ -48,7 +39,7 @@ impl IgnoreFilterer {
///
/// Use [`empty()`](IgnoreFilterer::empty()) if you want an empty filterer,
/// or to construct one outside an async environment.
pub async fn new(origin: impl AsRef<Path>, files: &[IgnoreFile]) -> Result<Self, RuntimeError> {
pub async fn new(origin: impl AsRef<Path>, files: &[IgnoreFile]) -> Result<Self, Error> {
let origin = origin.as_ref();
let _span = trace_span!("build_filterer", ?origin);
@ -57,12 +48,12 @@ impl IgnoreFilterer {
.iter()
.map(|file| async move {
trace!(?file, "loading ignore file");
let content = read_to_string(&file.path).await.map_err(|err| {
RuntimeError::IgnoreFileRead {
let content = read_to_string(&file.path)
.await
.map_err(|err| Error::Read {
file: file.path.clone(),
err,
}
})?;
})?;
Ok((file.clone(), content))
})
.collect::<FuturesUnordered<_>>()
@ -75,10 +66,10 @@ impl IgnoreFilterer {
})
.unzip();
let errors: Vec<RuntimeError> = errors.into_iter().flatten().collect();
let errors: Vec<Error> = errors.into_iter().flatten().collect();
if !errors.is_empty() {
trace!("found {} errors", errors.len());
return Err(RuntimeError::Set(errors));
return Err(Error::Multi(errors));
}
// TODO: different parser/adapter for non-git-syntax ignore files?
@ -95,7 +86,7 @@ impl IgnoreFilterer {
trace!(?line, "adding ignore line");
builder
.add_line(file.applies_in.clone(), line)
.map_err(|err| RuntimeError::GlobsetGlob {
.map_err(|err| Error::Glob {
file: Some(file.path.clone()),
err,
})?;
@ -105,7 +96,7 @@ impl IgnoreFilterer {
trace!("compiling globset");
let compiled = builder
.build()
.map_err(|err| RuntimeError::GlobsetGlob { file: None, err })?;
.map_err(|err| Error::Glob { file: None, err })?;
trace!(
files=%files.len(),
@ -136,16 +127,15 @@ impl IgnoreFilterer {
/// Reads and adds an ignore file, if the builder is available.
///
/// Does nothing silently otherwise.
pub async fn add_file(&mut self, file: &IgnoreFile) -> Result<(), RuntimeError> {
pub async fn add_file(&mut self, file: &IgnoreFile) -> Result<(), Error> {
if let Some(ref mut builder) = self.builder {
trace!(?file, "reading ignore file");
let content =
read_to_string(&file.path)
.await
.map_err(|err| RuntimeError::IgnoreFileRead {
file: file.path.clone(),
err,
})?;
let content = read_to_string(&file.path)
.await
.map_err(|err| Error::Read {
file: file.path.clone(),
err,
})?;
let _span = trace_span!("loading ignore file", ?file).entered();
for line in content.lines() {
@ -156,7 +146,7 @@ impl IgnoreFilterer {
trace!(?line, "adding ignore line");
builder
.add_line(file.applies_in.clone(), line)
.map_err(|err| RuntimeError::GlobsetGlob {
.map_err(|err| Error::Glob {
file: Some(file.path.clone()),
err,
})?;
@ -168,13 +158,13 @@ impl IgnoreFilterer {
Ok(())
}
fn recompile(&mut self, file: PathBuf) -> Result<(), RuntimeError> {
fn recompile(&mut self, file: PathBuf) -> Result<(), Error> {
if let Some(builder) = &mut self.builder {
let pre_ignores = self.compiled.num_ignores();
let pre_allows = self.compiled.num_whitelists();
trace!("recompiling globset");
let recompiled = builder.build().map_err(|err| RuntimeError::GlobsetGlob {
let recompiled = builder.build().map_err(|err| Error::Glob {
file: Some(file),
err,
})?;
@ -197,7 +187,7 @@ impl IgnoreFilterer {
&mut self,
globs: &[&str],
applies_in: Option<PathBuf>,
) -> Result<(), RuntimeError> {
) -> Result<(), Error> {
if let Some(ref mut builder) = self.builder {
let _span = trace_span!("loading ignore globs", ?globs).entered();
for line in globs {
@ -208,7 +198,7 @@ impl IgnoreFilterer {
trace!(?line, "adding ignore line");
builder
.add_line(applies_in.clone(), line)
.map_err(|err| RuntimeError::GlobsetGlob { file: None, err })?;
.map_err(|err| Error::Glob { file: None, err })?;
}
self.recompile("manual glob".into())?;
@ -217,6 +207,17 @@ impl IgnoreFilterer {
Ok(())
}
/// Match a particular path against the ignore set.
pub fn match_path(&self, path: &Path, is_dir: bool) -> Match<&Glob> {
if path.strip_prefix(&self.origin).is_ok() {
trace!("checking against path or parents");
self.compiled.matched_path_or_any_parents(path, is_dir)
} else {
trace!("checking against path only");
self.compiled.matched(path, is_dir)
}
}
/// Check a particular folder path against the ignore set.
///
/// Returns `false` if the folder should be ignored.
@ -227,13 +228,7 @@ impl IgnoreFilterer {
let _span = trace_span!("check_dir", ?path).entered();
trace!("checking against compiled ignore files");
match if path.strip_prefix(&self.origin).is_ok() {
trace!("checking against path or parents");
self.compiled.matched_path_or_any_parents(path, true)
} else {
trace!("checking against path only");
self.compiled.matched(path, true)
} {
match self.match_path(path, true) {
Match::None => {
trace!("no match (pass)");
true
@ -254,49 +249,3 @@ impl IgnoreFilterer {
}
}
}
impl Filterer for IgnoreFilterer {
/// Filter an event.
///
/// This implementation never errors. It returns `Ok(false)` if the event is ignored according
/// to the ignore files, and `Ok(true)` otherwise. It ignores event priority.
fn check_event(&self, event: &Event, _priority: Priority) -> Result<bool, RuntimeError> {
let _span = trace_span!("filterer_check").entered();
let mut pass = true;
for (path, file_type) in event.paths() {
let _span = trace_span!("checking_against_compiled", ?path, ?file_type).entered();
let is_dir = file_type
.map(|t| matches!(t, FileType::Dir))
.unwrap_or(false);
match if path.strip_prefix(&self.origin).is_ok() {
trace!("checking against path or parents");
self.compiled.matched_path_or_any_parents(path, is_dir)
} else {
trace!("checking against path only");
self.compiled.matched(path, is_dir)
} {
Match::None => {
trace!("no match (pass)");
pass &= true;
}
Match::Ignore(glob) => {
if glob.from().map_or(true, |f| path.strip_prefix(f).is_ok()) {
trace!(?glob, "positive match (fail)");
pass &= false;
} else {
trace!(?glob, "positive match, but not in scope (ignore)");
}
}
Match::Whitelist(glob) => {
trace!(?glob, "negative match (pass)");
pass = true;
}
}
}
trace!(?pass, "verdict");
Ok(pass)
}
}

View File

@ -0,0 +1,39 @@
//! Find, parse, and interpret ignore files.
//!
//! Ignore files are files that contain ignore patterns, often following the `.gitignore` format.
//! There may be one or more global ignore files, which apply everywhere, and one or more per-folder
//! ignore files, which apply to a specific folder and its subfolders. Furthermore, there may be
//! more ignore files in _these_ subfolders, and so on. Discovering and interpreting all of these in
//! a single context is not a simple task: this is what this crate provides.
use std::path::PathBuf;
use project_origins::ProjectType;
#[doc(inline)]
pub use discover::*;
mod discover;
#[doc(inline)]
pub use error::*;
mod error;
#[doc(inline)]
pub use filter::*;
mod filter;
/// An ignore file.
///
/// This records both the path to the ignore file and some basic metadata about it: which project
/// type it applies to if any, and which subtree it applies in if any (`None` = global ignore file).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IgnoreFile {
/// The path to the ignore file.
pub path: PathBuf,
/// The path to the subtree the ignore file applies to, or `None` for global ignores.
pub applies_in: Option<PathBuf>,
/// Which project type the ignore file applies to, or was found through.
pub applies_to: Option<ProjectType>,
}

View File

@ -1,6 +1,6 @@
[package]
name = "watchexec"
version = "2.0.0-pre.14"
version = "2.0.0"
authors = ["Matt Green <mattgreenrocks@gmail.com>", "Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0"
@ -18,27 +18,28 @@ edition = "2021"
[dependencies]
async-priority-channel = "0.1.0"
async-recursion = "1.0.0"
async-stream = "0.3.2"
atomic-take = "1.0.0"
clearscreen = "1.0.9"
dunce = "1.0.2"
futures = "0.3.16"
git-config = "0.2.0"
globset = "0.4.8"
ignore = "0.4.18"
miette = "4.7.1"
nom = "7.0.0"
once_cell = "1.8.0"
regex = "1.5.4"
thiserror = "1.0.26"
unicase = "2.6.0"
[dependencies.command-group]
version = "1.0.8"
features = ["with-tokio"]
[dependencies.ignore-files]
version = "1.0.0"
path = "../ignore-files"
[dependencies.notify]
version = "=5.0.0-pre.14"
version = "=5.0.0-pre.15"
[dependencies.project-origins]
version = "1.0.0"
path = "../project-origins"
[dependencies.tokio]
version = "1.19.2"
@ -52,10 +53,6 @@ features = [
"sync",
]
[dependencies.tokio-stream]
version = "0.1.7"
features = ["fs"]
[dependencies.tracing]
version = "0.1.26"
features = ["log"]

View File

@ -11,10 +11,10 @@ _The library which powers [Watchexec CLI](https://watchexec.github.io) and other
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license].
- Minimum Supported Rust Version: 1.58.0 (incurs a minor semver bump).
- Status: in preview (`2.0.0-pre.N` series).
- Status: maintained.
[docs]: https://docs.rs/watchexec/2.0.0-pre.6
[license]: ../LICENSE
[docs]: https://docs.rs/watchexec
[license]: ../../LICENSE
## Quick start
@ -76,8 +76,8 @@ async fn main() -> Result<()> {
## Kitchen sink
The library also exposes a large amount of components which are available to make your own tool, or
to make anything else you may want:
The library also exposes a number of components which are available to make your own tool, or to
make anything else you may want:
- **[Command handling](https://docs.rs/watchexec/2.0.0-pre.6/watchexec/command/index.html)**, to
build a command with an arbitrary shell, deal with grouped and ungrouped processes the same way,
@ -89,14 +89,21 @@ to make anything else you may want:
- Finding **[a common prefix](https://docs.rs/watchexec/2.0.0-pre.6/watchexec/paths/fn.common_prefix.html)**
of a set of paths.
- Detecting the **[origin(s)](https://docs.rs/watchexec/2.0.0-pre.6/watchexec/project/fn.origins.html)**
and **[types](https://docs.rs/watchexec/2.0.0-pre.6/watchexec/project/fn.types.html)** of projects.
- Discovering project-local and global
**[ignore files](https://docs.rs/watchexec/2.0.0-pre.6/watchexec/ignore/index.html)**.
- And [more][docs]!
Filterers are split into their own crates, so they can be evolved independently:
- The **[Globset](https://docs.rs/watchexec-filterer-globset) filterer** implements the default
Watchexec filter, and mimics the pre-1.18 behaviour as much as possible.
- The **[Tagged](https://docs.rs/watchexec-filterer-tagged) filterer** is an experiment in creating
a more powerful filtering solution, which can operate on every part of events, not just their
paths.
- The **[Ignore](https://docs.rs/watchexec-filterer-ignore) filterer** implements ignore-file
semantics, and especially supports _trees_ of ignore files. It is used as a subfilterer in both
of the main filterers above.
There are also separate, standalone crates used to build Watchexec which you can tap into:
- **[ClearScreen](https://docs.rs/clearscreen)** makes clearing the terminal screen in a
@ -105,11 +112,7 @@ There are also separate, standalone crates used to build Watchexec which you can
- **[Command Group](https://docs.rs/command-group)** augments the std and tokio `Command` with
support for process groups, portable between Unix and Windows.
- **[Ignore files](https://docs.rs/ignore-files)** finds, parses, and interprets ignore files.
## Tagged filters (alpha)
This library is also the home of Watchexec's current _two_ filtering implementations: the v1
behaviour which has proven confusing and inconsistent over the years, and an upcoming complete
overhaul called "tagged filtering" which will potentially replace the legacy one.
Have a look at the [docs](https://docs.rs/watchexec/2.0.0-pre.6/watchexec/filter/tagged/struct.TaggedFilterer.html)!
- **[Project Origins](https://docs.rs/project-origins)** finds the origin (or root) path of a
project, and what kind of project it is.

3
crates/lib/release.toml Normal file
View File

@ -0,0 +1,3 @@
pre-release-commit-message = "release: lib v{{version}}"
tag-prefix = "lib-"
tag-message = "watchexec-lib {{version}}"

View File

@ -1,5 +1,3 @@
use std::path::PathBuf;
use miette::Diagnostic;
use thiserror::Error;
@ -128,34 +126,13 @@ pub enum RuntimeError {
#[diagnostic(code(watchexec::runtime::clearscreen))]
Clearscreen(#[from] clearscreen::Error),
/// Error received when parsing a glob (possibly from an [`IgnoreFile`]) fails.
///
/// [`IgnoreFile`]: crate::ignore::IgnoreFile
#[error("cannot parse glob from ignore '{file:?}': {err}")]
#[diagnostic(code(watchexec::runtime::ignore_glob))]
GlobsetGlob {
/// The path to the erroring ignore file.
file: Option<PathBuf>,
/// The underlying error.
#[source]
err: ignore::Error,
// TODO: extract glob error into diagnostic
},
/// Error received when an [`IgnoreFile`] cannot be read.
///
/// [`IgnoreFile`]: crate::ignore::IgnoreFile
#[error("cannot read ignore '{file}': {err}")]
#[diagnostic(code(watchexec::runtime::ignore_file_read))]
IgnoreFileRead {
/// The path to the erroring ignore file.
file: PathBuf,
/// The underlying error.
#[source]
err: std::io::Error,
},
/// Error received from the [`ignore-files`](ignore_files) crate.
#[error("ignore files: {0}")]
IgnoreFiles(
#[diagnostic_source]
#[from]
ignore_files::Error,
),
/// Error emitted by a [`Filterer`](crate::filter::Filterer).
///
@ -176,9 +153,4 @@ pub enum RuntimeError {
#[source]
err: Box<dyn std::error::Error + Send + Sync>,
},
/// A set of related [`RuntimeError`]s.
#[error("related: {0:?}")]
#[diagnostic(code(watchexec::runtime::set))]
Set(#[related] Vec<RuntimeError>),
}

View File

@ -1,17 +1,10 @@
use std::{collections::HashMap, path::PathBuf};
use std::path::PathBuf;
use ignore::gitignore::Gitignore;
use miette::Diagnostic;
use thiserror::Error;
use tokio::sync::watch::{self, error::SendError};
use tokio::sync::watch;
use crate::{
action,
error::RuntimeError,
filter::tagged::{Filter, Matcher},
fs,
ignore::IgnoreFilterer,
};
use crate::{action, fs};
/// Errors occurring from reconfigs.
#[derive(Debug, Diagnostic, Error)]
@ -56,77 +49,6 @@ impl SignalParseError {
}
}
/// Errors emitted by the TaggedFilterer.
#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]
#[diagnostic(url(docsrs))]
pub enum TaggedFiltererError {
/// Generic I/O error, with some context.
#[error("io({about}): {err}")]
#[diagnostic(code(watchexec::filter::io_error))]
IoError {
/// What it was about.
about: &'static str,
/// The I/O error which occurred.
#[source]
err: std::io::Error,
},
/// Error received when a tagged filter cannot be parsed.
#[error("cannot parse filter `{src}`: {err:?}")]
#[diagnostic(code(watchexec::filter::tagged::parse))]
Parse {
/// The source of the filter.
#[source_code]
src: String,
/// What went wrong.
err: nom::error::ErrorKind,
},
/// Error received when a filter cannot be added or removed from a tagged filter list.
#[error("cannot {action} filter: {err:?}")]
#[diagnostic(code(watchexec::filter::tagged::filter_change))]
FilterChange {
/// The action that was attempted.
action: &'static str,
/// The underlying error.
#[source]
err: SendError<HashMap<Matcher, Vec<Filter>>>,
},
/// Error received when a glob cannot be parsed.
#[error("cannot parse glob: {0}")]
#[diagnostic(code(watchexec::filter::tagged::glob_parse))]
GlobParse(#[source] ignore::Error),
/// Error received when a compiled globset cannot be changed.
#[error("cannot change compiled globset: {0:?}")]
#[diagnostic(code(watchexec::filter::tagged::globset_change))]
GlobsetChange(#[source] SendError<Option<Gitignore>>),
/// Error received about the internal ignore filterer.
#[error("ignore filterer: {0}")]
#[diagnostic(code(watchexec::filter::tagged::ignore))]
Ignore(#[source] RuntimeError),
/// Error received when a new ignore filterer cannot be swapped in.
#[error("cannot swap in new ignore filterer: {0:?}")]
#[diagnostic(code(watchexec::filter::tagged::ignore_swap))]
IgnoreSwap(#[source] SendError<IgnoreFilterer>),
}
impl From<TaggedFiltererError> for RuntimeError {
fn from(err: TaggedFiltererError) -> Self {
Self::Filterer {
kind: "tagged",
err: Box::new(err) as _,
}
}
}
/// Errors emitted by the filesystem watcher.
#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]

Some files were not shown because too many files have changed in this diff Show More