Split into more crates (#307)
This commit is contained in:
parent
70aa48c59d
commit
35cf63bc85
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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 = [
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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?;
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## Next (YYYY-MM-DD)
|
||||
|
||||
- Initial release as a separate crate.
|
|
@ -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",
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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");
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## Next (YYYY-MM-DD)
|
||||
|
||||
- Initial release as a separate crate.
|
|
@ -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",
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use watchexec::ignore::IgnoreFilterer;
|
||||
use watchexec_filterer_ignore::IgnoreFilterer;
|
||||
|
||||
mod helpers;
|
||||
use helpers::ignore::*;
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## Next (YYYY-MM-DD)
|
||||
|
||||
- Initial release as a separate crate.
|
|
@ -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",
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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 _,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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")
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
Cargo.toml
|
||||
package.json
|
||||
*.gemspec
|
||||
test-*
|
||||
*.sw*
|
||||
sources.*/
|
||||
/output.*
|
||||
**/possum
|
||||
zebra/**
|
||||
elep/**/hant
|
||||
song/**/bird/
|
|
@ -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::*;
|
||||
|
|
@ -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::*;
|
|
@ -1,6 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use watchexec::filter::tagged::TaggedFilterer;
|
||||
use watchexec_filterer_tagged::TaggedFilterer;
|
||||
|
||||
mod helpers;
|
||||
use helpers::tagged::*;
|
|
@ -0,0 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## Next (YYYY-MM-DD)
|
||||
|
||||
- Initial release as a separate crate.
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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))?;
|
||||
|
|
@ -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>),
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>,
|
||||
}
|
|
@ -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"]
|
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
|||
pre-release-commit-message = "release: lib v{{version}}"
|
||||
tag-prefix = "lib-"
|
||||
tag-message = "watchexec-lib {{version}}"
|
|
@ -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>),
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue