From 35cf63bc8584d4574bb95eb9c5e2f206ee00df27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= Date: Wed, 15 Jun 2022 03:25:05 +0000 Subject: [PATCH] Split into more crates (#307) --- .../{release.yml => release-cli.yml} | 0 .github/workflows/release-pr.yml | 100 +++++ .github/workflows/release-tag.yml | 29 ++ CITATION.cff | 3 +- Cargo.lock | 119 ++++-- Cargo.toml | 9 +- README.md | 4 +- bors.toml | 2 + {cli => crates/cli}/Cargo.toml | 12 +- {cli => crates/cli}/README.md | 0 {cli => crates/cli}/build.rs | 0 {cli => crates/cli}/release.toml | 31 +- {cli => crates/cli}/src/args.rs | 0 {cli => crates/cli}/src/config.rs | 0 {cli => crates/cli}/src/config/init.rs | 0 {cli => crates/cli}/src/config/runtime.rs | 0 {cli => crates/cli}/src/filterer.rs | 0 {cli => crates/cli}/src/filterer/common.rs | 16 +- {cli => crates/cli}/src/filterer/globset.rs | 3 +- {cli => crates/cli}/src/filterer/tagged.rs | 11 +- {cli => crates/cli}/src/main.rs | 0 {cli => crates/cli}/tests/help.rs | 0 .../cli}/tests/snapshots/help__help_unix.snap | 0 .../tests/snapshots/help__help_windows.snap | 0 {cli => crates/cli}/watchexec-manifest.rc | 0 {cli => crates/cli}/watchexec.exe.manifest | 0 crates/filterer/globset/CHANGELOG.md | 5 + crates/filterer/globset/Cargo.toml | 47 +++ crates/filterer/globset/README.md | 17 + crates/filterer/globset/release.toml | 10 + .../filterer/globset/src/lib.rs | 48 +-- .../filterer/globset/tests/filtering.rs | 0 crates/filterer/globset/tests/helpers/mod.rs | 142 +++++++ crates/filterer/ignore/CHANGELOG.md | 5 + crates/filterer/ignore/Cargo.toml | 43 ++ crates/filterer/ignore/README.md | 17 + crates/filterer/ignore/release.toml | 10 + crates/filterer/ignore/src/lib.rs | 58 +++ .../filterer/ignore/tests/filtering.rs | 2 +- crates/filterer/ignore/tests/helpers/mod.rs | 220 ++++++++++ .../filterer/ignore}/tests/ignores/allowlist | 0 .../filterer/ignore}/tests/ignores/folders | 0 .../filterer/ignore}/tests/ignores/globs | 0 .../filterer/ignore}/tests/ignores/negate | 0 .../ignore}/tests/ignores/scopes-global | 0 .../ignore}/tests/ignores/scopes-local | 0 .../ignore}/tests/ignores/scopes-sublocal | 0 .../ignore}/tests/ignores/self.ignore | 0 crates/filterer/tagged/CHANGELOG.md | 5 + crates/filterer/tagged/Cargo.toml | 57 +++ crates/filterer/tagged/README.md | 17 + crates/filterer/tagged/release.toml | 10 + crates/filterer/tagged/src/error.rs | 81 ++++ .../filterer/tagged/src}/files.rs | 12 +- crates/filterer/tagged/src/filter.rs | 273 +++++++++++++ .../filterer/tagged/src/filterer.rs | 376 +----------------- crates/filterer/tagged/src/lib.rs | 87 ++++ .../filterer/tagged/src}/parse.rs | 4 +- .../filterer/tagged}/src/swaplock.rs | 0 .../filterer/tagged/tests/filter_files.rs | 0 .../filterer/tagged}/tests/helpers/mod.rs | 52 +-- .../filterer/tagged}/tests/ignores/empty.wef | 0 .../filterer/tagged}/tests/ignores/folder.wef | 0 crates/filterer/tagged/tests/ignores/globs | 11 + .../filterer/tagged}/tests/ignores/negate.wef | 0 .../tagged}/tests/ignores/path-patterns.wef | 0 .../filterer/tagged/tests/non_paths.rs | 3 +- .../filterer/tagged/tests/parser.rs | 5 +- .../filterer/tagged/tests/paths.rs | 2 +- crates/ignore-files/CHANGELOG.md | 5 + crates/ignore-files/Cargo.toml | 29 ++ crates/ignore-files/README.md | 17 + crates/ignore-files/release.toml | 10 + .../ignore-files/src/discover.rs | 98 ++--- crates/ignore-files/src/error.rs | 40 ++ .../ignore-files/src}/filter.rs | 127 ++---- crates/ignore-files/src/lib.rs | 39 ++ {lib => crates/lib}/Cargo.toml | 23 +- {lib => crates/lib}/README.md | 39 +- {lib => crates/lib}/examples/demo.rs | 0 {lib => crates/lib}/examples/fs.rs | 0 {lib => crates/lib}/examples/readme.rs | 0 {lib => crates/lib}/examples/signal.rs | 0 crates/lib/release.toml | 3 + {lib => crates/lib}/src/action.rs | 0 {lib => crates/lib}/src/action/outcome.rs | 0 .../lib}/src/action/outcome_worker.rs | 0 .../lib}/src/action/process_holder.rs | 0 {lib => crates/lib}/src/action/worker.rs | 0 {lib => crates/lib}/src/action/workingdata.rs | 0 {lib => crates/lib}/src/command.rs | 0 {lib => crates/lib}/src/command/process.rs | 0 {lib => crates/lib}/src/command/shell.rs | 0 {lib => crates/lib}/src/command/supervisor.rs | 0 {lib => crates/lib}/src/config.rs | 0 {lib => crates/lib}/src/error.rs | 0 {lib => crates/lib}/src/error/critical.rs | 0 {lib => crates/lib}/src/error/runtime.rs | 42 +- {lib => crates/lib}/src/error/specialised.rs | 84 +--- {lib => crates/lib}/src/event.rs | 0 {lib => crates/lib}/src/filter.rs | 5 +- {lib => crates/lib}/src/fs.rs | 13 +- {lib => crates/lib}/src/handler.rs | 0 {lib => crates/lib}/src/lib.rs | 3 - {lib => crates/lib}/src/paths.rs | 0 {lib => crates/lib}/src/signal.rs | 0 {lib => crates/lib}/src/signal/process.rs | 0 {lib => crates/lib}/src/signal/source.rs | 0 {lib => crates/lib}/src/watchexec.rs | 0 {lib => crates/lib}/tests/env_reporting.rs | 0 {lib => crates/lib}/tests/error_handler.rs | 0 crates/project-origins/CHANGELOG.md | 5 + crates/project-origins/Cargo.toml | 25 ++ crates/project-origins/README.md | 17 + .../project-origins/examples/find-origins.rs | 9 +- crates/project-origins/release.toml | 10 + .../project-origins/src/lib.rs | 12 +- lib/CITATION.cff | 20 - lib/release.toml | 25 -- lib/src/ignore.rs | 9 - 120 files changed, 1803 insertions(+), 864 deletions(-) rename .github/workflows/{release.yml => release-cli.yml} (100%) create mode 100644 .github/workflows/release-pr.yml create mode 100644 .github/workflows/release-tag.yml rename {cli => crates/cli}/Cargo.toml (89%) rename {cli => crates/cli}/README.md (100%) rename {cli => crates/cli}/build.rs (100%) rename {cli => crates/cli}/release.toml (84%) rename {cli => crates/cli}/src/args.rs (100%) rename {cli => crates/cli}/src/config.rs (100%) rename {cli => crates/cli}/src/config/init.rs (100%) rename {cli => crates/cli}/src/config/runtime.rs (100%) rename {cli => crates/cli}/src/filterer.rs (100%) rename {cli => crates/cli}/src/filterer/common.rs (92%) rename {cli => crates/cli}/src/filterer/globset.rs (98%) rename {cli => crates/cli}/src/filterer/tagged.rs (91%) rename {cli => crates/cli}/src/main.rs (100%) rename {cli => crates/cli}/tests/help.rs (100%) rename {cli => crates/cli}/tests/snapshots/help__help_unix.snap (100%) rename {cli => crates/cli}/tests/snapshots/help__help_windows.snap (100%) rename {cli => crates/cli}/watchexec-manifest.rc (100%) rename {cli => crates/cli}/watchexec.exe.manifest (100%) create mode 100644 crates/filterer/globset/CHANGELOG.md create mode 100644 crates/filterer/globset/Cargo.toml create mode 100644 crates/filterer/globset/README.md create mode 100644 crates/filterer/globset/release.toml rename lib/src/filter/globset.rs => crates/filterer/globset/src/lib.rs (79%) rename lib/tests/filter_globset.rs => crates/filterer/globset/tests/filtering.rs (100%) create mode 100644 crates/filterer/globset/tests/helpers/mod.rs create mode 100644 crates/filterer/ignore/CHANGELOG.md create mode 100644 crates/filterer/ignore/Cargo.toml create mode 100644 crates/filterer/ignore/README.md create mode 100644 crates/filterer/ignore/release.toml create mode 100644 crates/filterer/ignore/src/lib.rs rename lib/tests/filter_ignorefiles.rs => crates/filterer/ignore/tests/filtering.rs (99%) create mode 100644 crates/filterer/ignore/tests/helpers/mod.rs rename {lib => crates/filterer/ignore}/tests/ignores/allowlist (100%) rename {lib => crates/filterer/ignore}/tests/ignores/folders (100%) rename {lib => crates/filterer/ignore}/tests/ignores/globs (100%) rename {lib => crates/filterer/ignore}/tests/ignores/negate (100%) rename {lib => crates/filterer/ignore}/tests/ignores/scopes-global (100%) rename {lib => crates/filterer/ignore}/tests/ignores/scopes-local (100%) rename {lib => crates/filterer/ignore}/tests/ignores/scopes-sublocal (100%) rename {lib => crates/filterer/ignore}/tests/ignores/self.ignore (100%) create mode 100644 crates/filterer/tagged/CHANGELOG.md create mode 100644 crates/filterer/tagged/Cargo.toml create mode 100644 crates/filterer/tagged/README.md create mode 100644 crates/filterer/tagged/release.toml create mode 100644 crates/filterer/tagged/src/error.rs rename {lib/src/filter/tagged => crates/filterer/tagged/src}/files.rs (92%) create mode 100644 crates/filterer/tagged/src/filter.rs rename lib/src/filter/tagged.rs => crates/filterer/tagged/src/filterer.rs (56%) create mode 100644 crates/filterer/tagged/src/lib.rs rename {lib/src/filter/tagged => crates/filterer/tagged/src}/parse.rs (98%) rename {lib => crates/filterer/tagged}/src/swaplock.rs (100%) rename lib/tests/filter_tagged_filterfiles.rs => crates/filterer/tagged/tests/filter_files.rs (100%) rename {lib => crates/filterer/tagged}/tests/helpers/mod.rs (87%) rename {lib => crates/filterer/tagged}/tests/ignores/empty.wef (100%) rename {lib => crates/filterer/tagged}/tests/ignores/folder.wef (100%) create mode 100644 crates/filterer/tagged/tests/ignores/globs rename {lib => crates/filterer/tagged}/tests/ignores/negate.wef (100%) rename {lib => crates/filterer/tagged}/tests/ignores/path-patterns.wef (100%) rename lib/tests/filter_tagged_nonpaths.rs => crates/filterer/tagged/tests/non_paths.rs (99%) rename lib/tests/filter_tagged_parser.rs => crates/filterer/tagged/tests/parser.rs (97%) rename lib/tests/filter_tagged_paths.rs => crates/filterer/tagged/tests/paths.rs (99%) create mode 100644 crates/ignore-files/CHANGELOG.md create mode 100644 crates/ignore-files/Cargo.toml create mode 100644 crates/ignore-files/README.md create mode 100644 crates/ignore-files/release.toml rename lib/src/ignore/files.rs => crates/ignore-files/src/discover.rs (85%) create mode 100644 crates/ignore-files/src/error.rs rename {lib/src/ignore => crates/ignore-files/src}/filter.rs (67%) create mode 100644 crates/ignore-files/src/lib.rs rename {lib => crates/lib}/Cargo.toml (81%) rename {lib => crates/lib}/README.md (75%) rename {lib => crates/lib}/examples/demo.rs (100%) rename {lib => crates/lib}/examples/fs.rs (100%) rename {lib => crates/lib}/examples/readme.rs (100%) rename {lib => crates/lib}/examples/signal.rs (100%) create mode 100644 crates/lib/release.toml rename {lib => crates/lib}/src/action.rs (100%) rename {lib => crates/lib}/src/action/outcome.rs (100%) rename {lib => crates/lib}/src/action/outcome_worker.rs (100%) rename {lib => crates/lib}/src/action/process_holder.rs (100%) rename {lib => crates/lib}/src/action/worker.rs (100%) rename {lib => crates/lib}/src/action/workingdata.rs (100%) rename {lib => crates/lib}/src/command.rs (100%) rename {lib => crates/lib}/src/command/process.rs (100%) rename {lib => crates/lib}/src/command/shell.rs (100%) rename {lib => crates/lib}/src/command/supervisor.rs (100%) rename {lib => crates/lib}/src/config.rs (100%) rename {lib => crates/lib}/src/error.rs (100%) rename {lib => crates/lib}/src/error/critical.rs (100%) rename {lib => crates/lib}/src/error/runtime.rs (83%) rename {lib => crates/lib}/src/error/specialised.rs (62%) rename {lib => crates/lib}/src/event.rs (100%) rename {lib => crates/lib}/src/filter.rs (92%) rename {lib => crates/lib}/src/fs.rs (97%) rename {lib => crates/lib}/src/handler.rs (100%) rename {lib => crates/lib}/src/lib.rs (98%) rename {lib => crates/lib}/src/paths.rs (100%) rename {lib => crates/lib}/src/signal.rs (100%) rename {lib => crates/lib}/src/signal/process.rs (100%) rename {lib => crates/lib}/src/signal/source.rs (100%) rename {lib => crates/lib}/src/watchexec.rs (100%) rename {lib => crates/lib}/tests/env_reporting.rs (100%) rename {lib => crates/lib}/tests/error_handler.rs (100%) create mode 100644 crates/project-origins/CHANGELOG.md create mode 100644 crates/project-origins/Cargo.toml create mode 100644 crates/project-origins/README.md rename lib/examples/project-origins.rs => crates/project-origins/examples/find-origins.rs (51%) create mode 100644 crates/project-origins/release.toml rename lib/src/project.rs => crates/project-origins/src/lib.rs (92%) delete mode 100644 lib/CITATION.cff delete mode 100644 lib/release.toml delete mode 100644 lib/src/ignore.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release-cli.yml similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/release-cli.yml diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 0000000..bbce362 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -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 }} diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml new file mode 100644 index 0000000..360a266 --- /dev/null +++ b/.github/workflows/release-tag.yml @@ -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 }} diff --git a/CITATION.cff b/CITATION.cff index 24e9700..13f22bd 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -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 diff --git a/Cargo.lock b/Cargo.lock index fc34042..64af227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index a6f3209..df69618 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/README.md b/README.md index b4ca6a8..96ed657 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/bors.toml b/bors.toml index ff77578..b2c4dd3 100644 --- a/bors.toml +++ b/bors.toml @@ -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 = [ diff --git a/cli/Cargo.toml b/crates/cli/Cargo.toml similarity index 89% rename from cli/Cargo.toml rename to crates/cli/Cargo.toml index 11a4cd3..9c5e928 100644 --- a/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -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 = [ diff --git a/cli/README.md b/crates/cli/README.md similarity index 100% rename from cli/README.md rename to crates/cli/README.md diff --git a/cli/build.rs b/crates/cli/build.rs similarity index 100% rename from cli/build.rs rename to crates/cli/build.rs diff --git a/cli/release.toml b/crates/cli/release.toml similarity index 84% rename from cli/release.toml rename to crates/cli/release.toml index 7d54a7e..56fba5b 100644 --- a/cli/release.toml +++ b/crates/cli/release.toml @@ -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 diff --git a/cli/src/args.rs b/crates/cli/src/args.rs similarity index 100% rename from cli/src/args.rs rename to crates/cli/src/args.rs diff --git a/cli/src/config.rs b/crates/cli/src/config.rs similarity index 100% rename from cli/src/config.rs rename to crates/cli/src/config.rs diff --git a/cli/src/config/init.rs b/crates/cli/src/config/init.rs similarity index 100% rename from cli/src/config/init.rs rename to crates/cli/src/config/init.rs diff --git a/cli/src/config/runtime.rs b/crates/cli/src/config/runtime.rs similarity index 100% rename from cli/src/config/runtime.rs rename to crates/cli/src/config/runtime.rs diff --git a/cli/src/filterer.rs b/crates/cli/src/filterer.rs similarity index 100% rename from cli/src/filterer.rs rename to crates/cli/src/filterer.rs diff --git a/cli/src/filterer/common.rs b/crates/cli/src/filterer/common.rs similarity index 92% rename from cli/src/filterer/common.rs rename to crates/cli/src/filterer/common.rs index 5fd4542..9a1c45b 100644 --- a/cli/src/filterer/common.rs +++ b/crates/cli/src/filterer/common.rs @@ -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 { - 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 { - 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); } diff --git a/cli/src/filterer/globset.rs b/crates/cli/src/filterer/globset.rs similarity index 98% rename from cli/src/filterer/globset.rs rename to crates/cli/src/filterer/globset.rs index a54bcc9..86b6e6e 100644 --- a/cli/src/filterer/globset.rs +++ b/crates/cli/src/filterer/globset.rs @@ -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> { let (project_origin, workdir) = super::common::dirs(args).await?; diff --git a/cli/src/filterer/tagged.rs b/crates/cli/src/filterer/tagged.rs similarity index 91% rename from cli/src/filterer/tagged.rs rename to crates/cli/src/filterer/tagged.rs index 749f5d9..744643f 100644 --- a/cli/src/filterer/tagged.rs +++ b/crates/cli/src/filterer/tagged.rs @@ -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> { @@ -35,7 +32,7 @@ pub async fn tagged(args: &ArgMatches) -> Result> { 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); } diff --git a/cli/src/main.rs b/crates/cli/src/main.rs similarity index 100% rename from cli/src/main.rs rename to crates/cli/src/main.rs diff --git a/cli/tests/help.rs b/crates/cli/tests/help.rs similarity index 100% rename from cli/tests/help.rs rename to crates/cli/tests/help.rs diff --git a/cli/tests/snapshots/help__help_unix.snap b/crates/cli/tests/snapshots/help__help_unix.snap similarity index 100% rename from cli/tests/snapshots/help__help_unix.snap rename to crates/cli/tests/snapshots/help__help_unix.snap diff --git a/cli/tests/snapshots/help__help_windows.snap b/crates/cli/tests/snapshots/help__help_windows.snap similarity index 100% rename from cli/tests/snapshots/help__help_windows.snap rename to crates/cli/tests/snapshots/help__help_windows.snap diff --git a/cli/watchexec-manifest.rc b/crates/cli/watchexec-manifest.rc similarity index 100% rename from cli/watchexec-manifest.rc rename to crates/cli/watchexec-manifest.rc diff --git a/cli/watchexec.exe.manifest b/crates/cli/watchexec.exe.manifest similarity index 100% rename from cli/watchexec.exe.manifest rename to crates/cli/watchexec.exe.manifest diff --git a/crates/filterer/globset/CHANGELOG.md b/crates/filterer/globset/CHANGELOG.md new file mode 100644 index 0000000..1ab686f --- /dev/null +++ b/crates/filterer/globset/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Next (YYYY-MM-DD) + +- Initial release as a separate crate. diff --git a/crates/filterer/globset/Cargo.toml b/crates/filterer/globset/Cargo.toml new file mode 100644 index 0000000..83d7880 --- /dev/null +++ b/crates/filterer/globset/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "watchexec-filterer-globset" +version = "1.0.0" + +authors = ["Matt Green ", "Félix Saparelli "] +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", +] diff --git a/crates/filterer/globset/README.md b/crates/filterer/globset/README.md new file mode 100644 index 0000000..3c6412d --- /dev/null +++ b/crates/filterer/globset/README.md @@ -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 diff --git a/crates/filterer/globset/release.toml b/crates/filterer/globset/release.toml new file mode 100644 index 0000000..f4b2098 --- /dev/null +++ b/crates/filterer/globset/release.toml @@ -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 diff --git a/lib/src/filter/globset.rs b/crates/filterer/globset/src/lib.rs similarity index 79% rename from lib/src/filter/globset.rs rename to crates/filterer/globset/src/lib.rs index eacf2bf..17fbef5 100644 --- a/lib/src/filter/globset.rs +++ b/crates/filterer/globset/src/lib.rs @@ -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)>, ignore_files: impl IntoIterator, extensions: impl IntoIterator, - ) -> Result { + ) -> Result { 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 = extensions.into_iter().collect(); let mut ignore_files = - IgnoreFilterer::new(origin, &ignore_files.into_iter().collect::>()).await?; + IgnoreFilter::new(origin, &ignore_files.into_iter().collect::>()).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"); diff --git a/lib/tests/filter_globset.rs b/crates/filterer/globset/tests/filtering.rs similarity index 100% rename from lib/tests/filter_globset.rs rename to crates/filterer/globset/tests/filtering.rs diff --git a/crates/filterer/globset/tests/helpers/mod.rs b/crates/filterer/globset/tests/helpers/mod.rs new file mode 100644 index 0000000..2cb6527 --- /dev/null +++ b/crates/filterer/globset/tests/helpers/mod.rs @@ -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, + ) -> std::result::Result { + 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, 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 + } +} diff --git a/crates/filterer/ignore/CHANGELOG.md b/crates/filterer/ignore/CHANGELOG.md new file mode 100644 index 0000000..1ab686f --- /dev/null +++ b/crates/filterer/ignore/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Next (YYYY-MM-DD) + +- Initial release as a separate crate. diff --git a/crates/filterer/ignore/Cargo.toml b/crates/filterer/ignore/Cargo.toml new file mode 100644 index 0000000..e6fc754 --- /dev/null +++ b/crates/filterer/ignore/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "watchexec-filterer-ignore" +version = "1.0.0" + +authors = ["Félix Saparelli "] +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", +] diff --git a/crates/filterer/ignore/README.md b/crates/filterer/ignore/README.md new file mode 100644 index 0000000..d3f5bcd --- /dev/null +++ b/crates/filterer/ignore/README.md @@ -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 diff --git a/crates/filterer/ignore/release.toml b/crates/filterer/ignore/release.toml new file mode 100644 index 0000000..3710285 --- /dev/null +++ b/crates/filterer/ignore/release.toml @@ -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 diff --git a/crates/filterer/ignore/src/lib.rs b/crates/filterer/ignore/src/lib.rs new file mode 100644 index 0000000..984504f --- /dev/null +++ b/crates/filterer/ignore/src/lib.rs @@ -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 { + 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) + } +} diff --git a/lib/tests/filter_ignorefiles.rs b/crates/filterer/ignore/tests/filtering.rs similarity index 99% rename from lib/tests/filter_ignorefiles.rs rename to crates/filterer/ignore/tests/filtering.rs index 91afea7..daeb94a 100644 --- a/lib/tests/filter_ignorefiles.rs +++ b/crates/filterer/ignore/tests/filtering.rs @@ -1,4 +1,4 @@ -use watchexec::ignore::IgnoreFilterer; +use watchexec_filterer_ignore::IgnoreFilterer; mod helpers; use helpers::ignore::*; diff --git a/crates/filterer/ignore/tests/helpers/mod.rs b/crates/filterer/ignore/tests/helpers/mod.rs new file mode 100644 index 0000000..a9ef403 --- /dev/null +++ b/crates/filterer/ignore/tests/helpers/mod.rs @@ -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, + ) -> std::result::Result { + 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, 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; + + 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) { + self.tag_pass(Tag::ProcessCompletion(exit), true); + } + + fn complete_doesnt_pass(&self, exit: Option) { + 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 + } +} diff --git a/lib/tests/ignores/allowlist b/crates/filterer/ignore/tests/ignores/allowlist similarity index 100% rename from lib/tests/ignores/allowlist rename to crates/filterer/ignore/tests/ignores/allowlist diff --git a/lib/tests/ignores/folders b/crates/filterer/ignore/tests/ignores/folders similarity index 100% rename from lib/tests/ignores/folders rename to crates/filterer/ignore/tests/ignores/folders diff --git a/lib/tests/ignores/globs b/crates/filterer/ignore/tests/ignores/globs similarity index 100% rename from lib/tests/ignores/globs rename to crates/filterer/ignore/tests/ignores/globs diff --git a/lib/tests/ignores/negate b/crates/filterer/ignore/tests/ignores/negate similarity index 100% rename from lib/tests/ignores/negate rename to crates/filterer/ignore/tests/ignores/negate diff --git a/lib/tests/ignores/scopes-global b/crates/filterer/ignore/tests/ignores/scopes-global similarity index 100% rename from lib/tests/ignores/scopes-global rename to crates/filterer/ignore/tests/ignores/scopes-global diff --git a/lib/tests/ignores/scopes-local b/crates/filterer/ignore/tests/ignores/scopes-local similarity index 100% rename from lib/tests/ignores/scopes-local rename to crates/filterer/ignore/tests/ignores/scopes-local diff --git a/lib/tests/ignores/scopes-sublocal b/crates/filterer/ignore/tests/ignores/scopes-sublocal similarity index 100% rename from lib/tests/ignores/scopes-sublocal rename to crates/filterer/ignore/tests/ignores/scopes-sublocal diff --git a/lib/tests/ignores/self.ignore b/crates/filterer/ignore/tests/ignores/self.ignore similarity index 100% rename from lib/tests/ignores/self.ignore rename to crates/filterer/ignore/tests/ignores/self.ignore diff --git a/crates/filterer/tagged/CHANGELOG.md b/crates/filterer/tagged/CHANGELOG.md new file mode 100644 index 0000000..1ab686f --- /dev/null +++ b/crates/filterer/tagged/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Next (YYYY-MM-DD) + +- Initial release as a separate crate. diff --git a/crates/filterer/tagged/Cargo.toml b/crates/filterer/tagged/Cargo.toml new file mode 100644 index 0000000..086d2e9 --- /dev/null +++ b/crates/filterer/tagged/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "watchexec-filterer-tagged" +version = "0.1.0" + +authors = ["Félix Saparelli "] +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", +] diff --git a/crates/filterer/tagged/README.md b/crates/filterer/tagged/README.md new file mode 100644 index 0000000..b68da0b --- /dev/null +++ b/crates/filterer/tagged/README.md @@ -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 diff --git a/crates/filterer/tagged/release.toml b/crates/filterer/tagged/release.toml new file mode 100644 index 0000000..e8b8d6a --- /dev/null +++ b/crates/filterer/tagged/release.toml @@ -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 diff --git a/crates/filterer/tagged/src/error.rs b/crates/filterer/tagged/src/error.rs new file mode 100644 index 0000000..07bcc37 --- /dev/null +++ b/crates/filterer/tagged/src/error.rs @@ -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>>, + }, + + /// 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>), + + /// 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), +} + +impl From for RuntimeError { + fn from(err: TaggedFiltererError) -> Self { + Self::Filterer { + kind: "tagged", + err: Box::new(err) as _, + } + } +} diff --git a/lib/src/filter/tagged/files.rs b/crates/filterer/tagged/src/files.rs similarity index 92% rename from lib/src/filter/tagged/files.rs rename to crates/filterer/tagged/src/files.rs index d49a046..568ab60 100644 --- a/lib/src/filter/tagged/files.rs +++ b/crates/filterer/tagged/src/files.rs @@ -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, Vec) { +pub async fn discover_files_from_environment() -> (Vec, Vec) { let mut files = Vec::new(); let mut errors = Vec::new(); diff --git a/crates/filterer/tagged/src/filter.rs b/crates/filterer/tagged/src/filter.rs new file mode 100644 index 0000000..92f3812 --- /dev/null +++ b/crates/filterer/tagged/src/filter.rs @@ -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, + + /// 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) -> Result { + 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, 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 { + 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), +} + +impl PartialEq 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 {} diff --git a/lib/src/filter/tagged.rs b/crates/filterer/tagged/src/filterer.rs similarity index 56% rename from lib/src/filter/tagged.rs rename to crates/filterer/tagged/src/filterer.rs index dbd6e74..d5ddcb3 100644 --- a/lib/src/filter/tagged.rs +++ b/crates/filterer/tagged/src/filterer.rs @@ -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, - - /// 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) -> Result { - 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, 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 { - 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), -} - -impl PartialEq 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 {} diff --git a/crates/filterer/tagged/src/lib.rs b/crates/filterer/tagged/src/lib.rs new file mode 100644 index 0000000..c833612 --- /dev/null +++ b/crates/filterer/tagged/src/lib.rs @@ -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; diff --git a/lib/src/filter/tagged/parse.rs b/crates/filterer/tagged/src/parse.rs similarity index 98% rename from lib/src/filter/tagged/parse.rs rename to crates/filterer/tagged/src/parse.rs index fbc4aa9..cabc2d2 100644 --- a/lib/src/filter/tagged/parse.rs +++ b/crates/filterer/tagged/src/parse.rs @@ -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; diff --git a/lib/src/swaplock.rs b/crates/filterer/tagged/src/swaplock.rs similarity index 100% rename from lib/src/swaplock.rs rename to crates/filterer/tagged/src/swaplock.rs diff --git a/lib/tests/filter_tagged_filterfiles.rs b/crates/filterer/tagged/tests/filter_files.rs similarity index 100% rename from lib/tests/filter_tagged_filterfiles.rs rename to crates/filterer/tagged/tests/filter_files.rs diff --git a/lib/tests/helpers/mod.rs b/crates/filterer/tagged/tests/helpers/mod.rs similarity index 87% rename from lib/tests/helpers/mod.rs rename to crates/filterer/tagged/tests/helpers/mod.rs index fdd9710..5abb5a0 100644 --- a/lib/tests/helpers/mod.rs +++ b/crates/filterer/tagged/tests/helpers/mod.rs @@ -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") } diff --git a/lib/tests/ignores/empty.wef b/crates/filterer/tagged/tests/ignores/empty.wef similarity index 100% rename from lib/tests/ignores/empty.wef rename to crates/filterer/tagged/tests/ignores/empty.wef diff --git a/lib/tests/ignores/folder.wef b/crates/filterer/tagged/tests/ignores/folder.wef similarity index 100% rename from lib/tests/ignores/folder.wef rename to crates/filterer/tagged/tests/ignores/folder.wef diff --git a/crates/filterer/tagged/tests/ignores/globs b/crates/filterer/tagged/tests/ignores/globs new file mode 100644 index 0000000..29f15cf --- /dev/null +++ b/crates/filterer/tagged/tests/ignores/globs @@ -0,0 +1,11 @@ +Cargo.toml +package.json +*.gemspec +test-* +*.sw* +sources.*/ +/output.* +**/possum +zebra/** +elep/**/hant +song/**/bird/ diff --git a/lib/tests/ignores/negate.wef b/crates/filterer/tagged/tests/ignores/negate.wef similarity index 100% rename from lib/tests/ignores/negate.wef rename to crates/filterer/tagged/tests/ignores/negate.wef diff --git a/lib/tests/ignores/path-patterns.wef b/crates/filterer/tagged/tests/ignores/path-patterns.wef similarity index 100% rename from lib/tests/ignores/path-patterns.wef rename to crates/filterer/tagged/tests/ignores/path-patterns.wef diff --git a/lib/tests/filter_tagged_nonpaths.rs b/crates/filterer/tagged/tests/non_paths.rs similarity index 99% rename from lib/tests/filter_tagged_nonpaths.rs rename to crates/filterer/tagged/tests/non_paths.rs index 5f617b4..c775fbb 100644 --- a/lib/tests/filter_tagged_nonpaths.rs +++ b/crates/filterer/tagged/tests/non_paths.rs @@ -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::*; diff --git a/lib/tests/filter_tagged_parser.rs b/crates/filterer/tagged/tests/parser.rs similarity index 97% rename from lib/tests/filter_tagged_parser.rs rename to crates/filterer/tagged/tests/parser.rs index af73716..78d133d 100644 --- a/lib/tests/filter_tagged_parser.rs +++ b/crates/filterer/tagged/tests/parser.rs @@ -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::*; diff --git a/lib/tests/filter_tagged_paths.rs b/crates/filterer/tagged/tests/paths.rs similarity index 99% rename from lib/tests/filter_tagged_paths.rs rename to crates/filterer/tagged/tests/paths.rs index 36941ec..7faecf1 100644 --- a/lib/tests/filter_tagged_paths.rs +++ b/crates/filterer/tagged/tests/paths.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use watchexec::filter::tagged::TaggedFilterer; +use watchexec_filterer_tagged::TaggedFilterer; mod helpers; use helpers::tagged::*; diff --git a/crates/ignore-files/CHANGELOG.md b/crates/ignore-files/CHANGELOG.md new file mode 100644 index 0000000..1ab686f --- /dev/null +++ b/crates/ignore-files/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Next (YYYY-MM-DD) + +- Initial release as a separate crate. diff --git a/crates/ignore-files/Cargo.toml b/crates/ignore-files/Cargo.toml new file mode 100644 index 0000000..10828a6 --- /dev/null +++ b/crates/ignore-files/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ignore-files" +version = "1.0.0" + +authors = ["Félix Saparelli "] +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" diff --git a/crates/ignore-files/README.md b/crates/ignore-files/README.md new file mode 100644 index 0000000..0cd7591 --- /dev/null +++ b/crates/ignore-files/README.md @@ -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 diff --git a/crates/ignore-files/release.toml b/crates/ignore-files/release.toml new file mode 100644 index 0000000..7efb54a --- /dev/null +++ b/crates/ignore-files/release.toml @@ -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 diff --git a/lib/src/ignore/files.rs b/crates/ignore-files/src/discover.rs similarity index 85% rename from lib/src/ignore/files.rs rename to crates/ignore-files/src/discover.rs index 9d09b10..e9f7fec 100644 --- a/lib/src/ignore/files.rs +++ b/crates/ignore-files/src/discover.rs @@ -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, - - /// Which project type the ignore file applies to, or was found through. - pub applies_to: Option, -} +/// 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) -> (Vec, Vec /// 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, Vec) { +pub async fn from_environment(appname: Option<&str>) -> (Vec, Vec) { 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, Vec) { } } - 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, Vec) { // 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, errors: &mut Vec, applies_in: Option, @@ -348,7 +348,7 @@ struct DirTourist { to_visit: Vec, to_skip: HashSet, pub errors: Vec, - filter: IgnoreFilterer, + filter: IgnoreFilter, } #[derive(Debug)] @@ -362,7 +362,7 @@ impl DirTourist { pub async fn new(base: &Path, files: &[IgnoreFile]) -> Result { 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))?; diff --git a/crates/ignore-files/src/error.rs b/crates/ignore-files/src/error.rs new file mode 100644 index 0000000..d16b2b2 --- /dev/null +++ b/crates/ignore-files/src/error.rs @@ -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, + + /// 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), +} diff --git a/lib/src/ignore/filter.rs b/crates/ignore-files/src/filter.rs similarity index 67% rename from lib/src/ignore/filter.rs rename to crates/ignore-files/src/filter.rs index 5052692..58ac249 100644 --- a/lib/src/ignore/filter.rs +++ b/crates/ignore-files/src/filter.rs @@ -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, 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, files: &[IgnoreFile]) -> Result { + pub async fn new(origin: impl AsRef, files: &[IgnoreFile]) -> Result { 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::>() @@ -75,10 +66,10 @@ impl IgnoreFilterer { }) .unzip(); - let errors: Vec = errors.into_iter().flatten().collect(); + let errors: Vec = 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, - ) -> 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 { - 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) - } -} diff --git a/crates/ignore-files/src/lib.rs b/crates/ignore-files/src/lib.rs new file mode 100644 index 0000000..3462a6f --- /dev/null +++ b/crates/ignore-files/src/lib.rs @@ -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, + + /// Which project type the ignore file applies to, or was found through. + pub applies_to: Option, +} diff --git a/lib/Cargo.toml b/crates/lib/Cargo.toml similarity index 81% rename from lib/Cargo.toml rename to crates/lib/Cargo.toml index dfe7072..3968912 100644 --- a/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "watchexec" -version = "2.0.0-pre.14" +version = "2.0.0" authors = ["Matt Green ", "Félix Saparelli "] 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"] diff --git a/lib/README.md b/crates/lib/README.md similarity index 75% rename from lib/README.md rename to crates/lib/README.md index 4811906..eded49a 100644 --- a/lib/README.md +++ b/crates/lib/README.md @@ -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. diff --git a/lib/examples/demo.rs b/crates/lib/examples/demo.rs similarity index 100% rename from lib/examples/demo.rs rename to crates/lib/examples/demo.rs diff --git a/lib/examples/fs.rs b/crates/lib/examples/fs.rs similarity index 100% rename from lib/examples/fs.rs rename to crates/lib/examples/fs.rs diff --git a/lib/examples/readme.rs b/crates/lib/examples/readme.rs similarity index 100% rename from lib/examples/readme.rs rename to crates/lib/examples/readme.rs diff --git a/lib/examples/signal.rs b/crates/lib/examples/signal.rs similarity index 100% rename from lib/examples/signal.rs rename to crates/lib/examples/signal.rs diff --git a/crates/lib/release.toml b/crates/lib/release.toml new file mode 100644 index 0000000..a3bf580 --- /dev/null +++ b/crates/lib/release.toml @@ -0,0 +1,3 @@ +pre-release-commit-message = "release: lib v{{version}}" +tag-prefix = "lib-" +tag-message = "watchexec-lib {{version}}" diff --git a/lib/src/action.rs b/crates/lib/src/action.rs similarity index 100% rename from lib/src/action.rs rename to crates/lib/src/action.rs diff --git a/lib/src/action/outcome.rs b/crates/lib/src/action/outcome.rs similarity index 100% rename from lib/src/action/outcome.rs rename to crates/lib/src/action/outcome.rs diff --git a/lib/src/action/outcome_worker.rs b/crates/lib/src/action/outcome_worker.rs similarity index 100% rename from lib/src/action/outcome_worker.rs rename to crates/lib/src/action/outcome_worker.rs diff --git a/lib/src/action/process_holder.rs b/crates/lib/src/action/process_holder.rs similarity index 100% rename from lib/src/action/process_holder.rs rename to crates/lib/src/action/process_holder.rs diff --git a/lib/src/action/worker.rs b/crates/lib/src/action/worker.rs similarity index 100% rename from lib/src/action/worker.rs rename to crates/lib/src/action/worker.rs diff --git a/lib/src/action/workingdata.rs b/crates/lib/src/action/workingdata.rs similarity index 100% rename from lib/src/action/workingdata.rs rename to crates/lib/src/action/workingdata.rs diff --git a/lib/src/command.rs b/crates/lib/src/command.rs similarity index 100% rename from lib/src/command.rs rename to crates/lib/src/command.rs diff --git a/lib/src/command/process.rs b/crates/lib/src/command/process.rs similarity index 100% rename from lib/src/command/process.rs rename to crates/lib/src/command/process.rs diff --git a/lib/src/command/shell.rs b/crates/lib/src/command/shell.rs similarity index 100% rename from lib/src/command/shell.rs rename to crates/lib/src/command/shell.rs diff --git a/lib/src/command/supervisor.rs b/crates/lib/src/command/supervisor.rs similarity index 100% rename from lib/src/command/supervisor.rs rename to crates/lib/src/command/supervisor.rs diff --git a/lib/src/config.rs b/crates/lib/src/config.rs similarity index 100% rename from lib/src/config.rs rename to crates/lib/src/config.rs diff --git a/lib/src/error.rs b/crates/lib/src/error.rs similarity index 100% rename from lib/src/error.rs rename to crates/lib/src/error.rs diff --git a/lib/src/error/critical.rs b/crates/lib/src/error/critical.rs similarity index 100% rename from lib/src/error/critical.rs rename to crates/lib/src/error/critical.rs diff --git a/lib/src/error/runtime.rs b/crates/lib/src/error/runtime.rs similarity index 83% rename from lib/src/error/runtime.rs rename to crates/lib/src/error/runtime.rs index 884ce46..efe2d18 100644 --- a/lib/src/error/runtime.rs +++ b/crates/lib/src/error/runtime.rs @@ -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, - - /// 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, }, - - /// A set of related [`RuntimeError`]s. - #[error("related: {0:?}")] - #[diagnostic(code(watchexec::runtime::set))] - Set(#[related] Vec), } diff --git a/lib/src/error/specialised.rs b/crates/lib/src/error/specialised.rs similarity index 62% rename from lib/src/error/specialised.rs rename to crates/lib/src/error/specialised.rs index 906a67b..b183fba 100644 --- a/lib/src/error/specialised.rs +++ b/crates/lib/src/error/specialised.rs @@ -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>>, - }, - - /// 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>), - - /// 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), -} - -impl From 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] diff --git a/lib/src/event.rs b/crates/lib/src/event.rs similarity index 100% rename from lib/src/event.rs rename to crates/lib/src/event.rs diff --git a/lib/src/filter.rs b/crates/lib/src/filter.rs similarity index 92% rename from lib/src/filter.rs rename to crates/lib/src/filter.rs index 3ffe1ab..22ea101 100644 --- a/lib/src/filter.rs +++ b/crates/lib/src/filter.rs @@ -1,4 +1,4 @@ -//! The `Filterer` trait, two implementations, and some helper functions. +//! The `Filterer` trait for event filtering. use std::sync::Arc; @@ -7,9 +7,6 @@ use crate::{ event::{Event, Priority}, }; -pub mod globset; -pub mod tagged; - /// An interface for filtering events. pub trait Filterer: std::fmt::Debug + Send + Sync { /// Called on (almost) every event, and should return `false` if the event is to be discarded. diff --git a/lib/src/fs.rs b/crates/lib/src/fs.rs similarity index 97% rename from lib/src/fs.rs rename to crates/lib/src/fs.rs index a866a28..f19f75a 100644 --- a/lib/src/fs.rs +++ b/crates/lib/src/fs.rs @@ -9,7 +9,7 @@ use std::{ }; use async_priority_channel as priority; -use notify::Watcher as _; +use notify::{Watcher as _, poll::PollWatcherConfig}; use tokio::sync::{mpsc, watch}; use tracing::{debug, error, trace, warn}; @@ -48,9 +48,14 @@ impl Watcher { ) -> Result, RuntimeError> { match self { Self::Native => notify::RecommendedWatcher::new(f).map(|w| Box::new(w) as _), - Self::Poll(delay) => { - notify::PollWatcher::with_delay(f, delay).map(|w| Box::new(w) as _) - } + Self::Poll(delay) => notify::PollWatcher::with_config( + f, + PollWatcherConfig { + poll_interval: delay, + ..Default::default() + }, + ) + .map(|w| Box::new(w) as _), } .map_err(|err| RuntimeError::FsWatcher { kind: self, diff --git a/lib/src/handler.rs b/crates/lib/src/handler.rs similarity index 100% rename from lib/src/handler.rs rename to crates/lib/src/handler.rs diff --git a/lib/src/lib.rs b/crates/lib/src/lib.rs similarity index 98% rename from lib/src/lib.rs rename to crates/lib/src/lib.rs index a36e6a3..8595860 100644 --- a/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -106,11 +106,8 @@ pub mod error; pub mod event; pub mod filter; pub mod fs; -pub mod ignore; pub mod paths; -pub mod project; pub mod signal; -pub mod swaplock; // the core experience pub mod config; diff --git a/lib/src/paths.rs b/crates/lib/src/paths.rs similarity index 100% rename from lib/src/paths.rs rename to crates/lib/src/paths.rs diff --git a/lib/src/signal.rs b/crates/lib/src/signal.rs similarity index 100% rename from lib/src/signal.rs rename to crates/lib/src/signal.rs diff --git a/lib/src/signal/process.rs b/crates/lib/src/signal/process.rs similarity index 100% rename from lib/src/signal/process.rs rename to crates/lib/src/signal/process.rs diff --git a/lib/src/signal/source.rs b/crates/lib/src/signal/source.rs similarity index 100% rename from lib/src/signal/source.rs rename to crates/lib/src/signal/source.rs diff --git a/lib/src/watchexec.rs b/crates/lib/src/watchexec.rs similarity index 100% rename from lib/src/watchexec.rs rename to crates/lib/src/watchexec.rs diff --git a/lib/tests/env_reporting.rs b/crates/lib/tests/env_reporting.rs similarity index 100% rename from lib/tests/env_reporting.rs rename to crates/lib/tests/env_reporting.rs diff --git a/lib/tests/error_handler.rs b/crates/lib/tests/error_handler.rs similarity index 100% rename from lib/tests/error_handler.rs rename to crates/lib/tests/error_handler.rs diff --git a/crates/project-origins/CHANGELOG.md b/crates/project-origins/CHANGELOG.md new file mode 100644 index 0000000..1ab686f --- /dev/null +++ b/crates/project-origins/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Next (YYYY-MM-DD) + +- Initial release as a separate crate. diff --git a/crates/project-origins/Cargo.toml b/crates/project-origins/Cargo.toml new file mode 100644 index 0000000..070e695 --- /dev/null +++ b/crates/project-origins/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "project-origins" +version = "1.0.0" + +authors = ["Félix Saparelli "] +license = "Apache-2.0" +description = "Resolve project origins and kinds from a path" +keywords = ["project", "origin", "root", "git"] + +documentation = "https://docs.rs/project-origins" +repository = "https://github.com/watchexec/watchexec" +readme = "README.md" + +rust-version = "1.58.0" +edition = "2021" + +[dependencies] +futures = "0.3.21" +tokio = { version = "1.19.2", features = ["fs"] } +tokio-stream = { version = "0.1.9", features = ["fs"] } + +[dev-dependencies] +dunce = "1.0.2" +miette = "4.7.1" +tracing-subscriber = "0.3.11" diff --git a/crates/project-origins/README.md b/crates/project-origins/README.md new file mode 100644 index 0000000..915c9e2 --- /dev/null +++ b/crates/project-origins/README.md @@ -0,0 +1,17 @@ +[![Crates.io page](https://badgen.net/crates/v/project-origins)](https://crates.io/crates/project-origins) +[![API Docs](https://docs.rs/project-origins/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) + +# Project origins + +_Resolve project origins and kinds from a path._ + +- **[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/project-origins +[license]: ../../LICENSE diff --git a/lib/examples/project-origins.rs b/crates/project-origins/examples/find-origins.rs similarity index 51% rename from lib/examples/project-origins.rs rename to crates/project-origins/examples/find-origins.rs index ffbd187..e1b3d04 100644 --- a/lib/examples/project-origins.rs +++ b/crates/project-origins/examples/find-origins.rs @@ -1,15 +1,16 @@ use std::env::args; use miette::{IntoDiagnostic, Result}; -use watchexec::project::origins; +use project_origins::origins; -// Run with: `cargo run --example project-origins [PATH]` +// Run with: `cargo run --example find-origins [PATH]` #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); - let path = - dunce::canonicalize(args().nth(1).unwrap_or_else(|| ".".to_string())).into_diagnostic()?; + let first_arg = args().nth(1).unwrap_or_else(|| ".".to_string()); + let path = dunce::canonicalize(first_arg).into_diagnostic()?; + for origin in origins(&path).await { println!("{}", origin.display()); } diff --git a/crates/project-origins/release.toml b/crates/project-origins/release.toml new file mode 100644 index 0000000..3fa1217 --- /dev/null +++ b/crates/project-origins/release.toml @@ -0,0 +1,10 @@ +pre-release-commit-message = "release: project-origins v{{version}}" +tag-prefix = "project-origins-" +tag-message = "project-origins {{version}}" + +[[pre-release-replacements]] +file = "CHANGELOG.md" +search = "^## Next.*$" +replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})" +prerelease = true +max = 1 diff --git a/lib/src/project.rs b/crates/project-origins/src/lib.rs similarity index 92% rename from lib/src/project.rs rename to crates/project-origins/src/lib.rs index 4272ae3..61d8b61 100644 --- a/lib/src/project.rs +++ b/crates/project-origins/src/lib.rs @@ -1,4 +1,14 @@ -//! Detect project type and origin. +//! Resolve project origins and kinds from a path. +//! +//! This crate originated in [Watchexec](https://docs.rs/watchexec): it is used to resolve where a +//! project's origin (or root) is, starting either at that origin, or within a subdirectory of it. +//! +//! This crate also provides the kind of project it is, and defines two categories within this: +//! version control systems, and software development environments. +//! +//! As it is possible to find several project origins, of different or similar kinds, from a given +//! directory and walking up, [`origins`] returns a set, rather than a single path. Determining +//! which of these is the "one true origin" (if necessary) is left to the caller. use std::{ collections::{HashMap, HashSet}, diff --git a/lib/CITATION.cff b/lib/CITATION.cff deleted file mode 100644 index 9fc47ec..0000000 --- a/lib/CITATION.cff +++ /dev/null @@ -1,20 +0,0 @@ -# YAML 1.2 ---- -cff-version: "1.1.0" -message: | - If you use this software, please cite it using these metadata. - The command-line tool has its own CITATION.cff file. - -title: Watchexec (library) -version: "2.0.0-pre.14" -date-released: 2022-04-03 - -repository-code: https://github.com/watchexec/watchexec -license: Apache-2.0 - -authors: - - family-names: Green - given-names: Matt - - family-names: Saparelli - given-names: Félix -... diff --git a/lib/release.toml b/lib/release.toml deleted file mode 100644 index 41b706d..0000000 --- a/lib/release.toml +++ /dev/null @@ -1,25 +0,0 @@ -pre-release-hook = ["../bin/pre-release-pull"] -pre-release-commit-message = "lib: v{{version}}" -tag-prefix = "lib-" -tag-message = "watchexec-lib {{version}}" - -[[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 = "../cli/Cargo.toml" -search = "^watchexec = \\{ version = \"=[\\d.]+(-.+)?\", path = \"../lib\" \\}" -replace = "watchexec = { version = \"={{version}}\", path = \"../lib\" }" -prerelease = true -min = 0 diff --git a/lib/src/ignore.rs b/lib/src/ignore.rs deleted file mode 100644 index 2dcb97b..0000000 --- a/lib/src/ignore.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Ignore files: find them, parse them, interpret them. - -#[doc(inline)] -pub use files::*; -#[doc(inline)] -pub use filter::*; - -mod files; -mod filter;