Compare commits

..

No commits in common. "main" and "v2.15.0" have entirely different histories.

231 changed files with 4702 additions and 8021 deletions

2
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,2 @@
github: asottile
open_collective: pre-commit

View file

@ -1,54 +0,0 @@
name: bug report
description: something went wrong
body:
- type: markdown
attributes:
value: |
this is for issues for `pre-commit` (the framework).
if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues]
[pre-commit.ci]: https://pre-commit.ci
[pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues
- type: input
id: search
attributes:
label: search you tried in the issue tracker
placeholder: ...
validations:
required: true
- type: markdown
attributes:
value: |
95% of issues created are duplicates.
please try extra hard to find them first.
it's very unlikely your problem is unique.
- type: textarea
id: freeform
attributes:
label: describe your issue
placeholder: 'I was doing ... I ran ... I expected ... I got ...'
validations:
required: true
- type: input
id: version
attributes:
label: pre-commit --version
placeholder: pre-commit x.x.x
validations:
required: true
- type: textarea
id: configuration
attributes:
label: .pre-commit-config.yaml
description: (auto-rendered as yaml, no need for backticks)
placeholder: 'repos: ...'
render: yaml
validations:
required: true
- type: textarea
id: error-log
attributes:
label: '~/.cache/pre-commit/pre-commit.log (if present)'
placeholder: "### version information\n..."
validations:
required: false

View file

@ -1,38 +0,0 @@
name: feature request
description: something new
body:
- type: markdown
attributes:
value: |
this is for issues for `pre-commit` (the framework).
if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues]
[pre-commit.ci]: https://pre-commit.ci
[pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues
- type: input
id: search
attributes:
label: search you tried in the issue tracker
placeholder: ...
validations:
required: true
- type: markdown
attributes:
value: |
95% of issues created are duplicates.
please try extra hard to find them first.
it's very unlikely your feature idea is a new one.
- type: textarea
id: freeform
attributes:
label: describe your actual problem
placeholder: 'I want to do ... I tried ... It does not work because ...'
validations:
required: true
- type: input
id: version
attributes:
label: pre-commit --version
placeholder: pre-commit x.x.x
validations:
required: true

View file

@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: documentation
url: https://pre-commit.com
about: please check the docs first
- name: pre-commit.ci issues
url: https://github.com/pre-commit-ci/issues
about: please report issues about pre-commit.ci here

View file

@ -1,9 +0,0 @@
inputs:
env:
default: ${{ matrix.env }}
runs:
using: composite
steps:
- uses: asottile/workflows/.github/actions/latest-git@v1.4.0
if: inputs.env == 'py39' && runner.os == 'Linux'

View file

@ -1,84 +0,0 @@
name: languages
on:
push:
branches: [main, test-me-*]
tags: '*'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
vars:
runs-on: ubuntu-latest
outputs:
languages: ${{ steps.vars.outputs.languages }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: install deps
run: python -mpip install -e . -r requirements-dev.txt
- name: vars
run: testing/languages ${{ github.event_name == 'push' && '--all' || '' }}
id: vars
language:
needs: [vars]
runs-on: ${{ matrix.os }}
if: needs.vars.outputs.languages != '[]'
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.vars.outputs.languages) }}
steps:
- uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- run: echo "$CONDA\Scripts" >> "$GITHUB_PATH"
shell: bash
if: matrix.os == 'windows-latest' && matrix.language == 'conda'
- run: testing/get-coursier.sh
shell: bash
if: matrix.language == 'coursier'
- run: testing/get-dart.sh
shell: bash
if: matrix.language == 'dart'
- run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
lua5.3 \
liblua5.3-dev \
luarocks
if: matrix.os == 'ubuntu-latest' && matrix.language == 'lua'
- run: |
echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH"
echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH"
echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH"
shell: bash
if: matrix.os == 'windows-latest' && matrix.language == 'perl'
- uses: haskell/actions/setup@v2
if: matrix.language == 'haskell'
- uses: r-lib/actions/setup-r@v2
if: matrix.os == 'ubuntu-latest' && matrix.language == 'r'
- name: install deps
run: python -mpip install -e . -r requirements-dev.txt
- name: run tests
run: coverage run -m pytest tests/languages/${{ matrix.language }}_test.py
- name: check coverage
run: coverage report --include pre_commit/languages/${{ matrix.language }}.py,tests/languages/${{ matrix.language }}_test.py
collector:
needs: [language]
if: always()
runs-on: ubuntu-latest
steps:
- name: check for failures
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: echo job failed && exit 1

View file

@ -1,23 +0,0 @@
name: main
on:
push:
branches: [main, test-me-*]
tags: '*'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
main-windows:
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
with:
env: '["py310"]'
os: windows-latest
main-linux:
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
with:
env: '["py310", "py311", "py312", "py313"]'
os: ubuntu-latest

3
.gitignore vendored
View file

@ -1,6 +1,9 @@
*.egg-info *.egg-info
*.py[co] *.py[co]
/.coverage /.coverage
/.mypy_cache
/.pytest_cache
/.tox /.tox
/dist /dist
/venv*
.vscode/ .vscode/

View file

@ -1,44 +1,55 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v4.0.1
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-docstring-first
- id: check-json
- id: check-yaml - id: check-yaml
- id: debug-statements - id: debug-statements
- id: double-quote-string-fixer
- id: name-tests-test - id: name-tests-test
- id: requirements-txt-fixer - id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt - id: double-quote-string-fixer
rev: v3.2.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.16.0
hooks:
- id: reorder-python-imports
exclude: ^pre_commit/resources/
args: [--py310-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma
rev: v4.0.0
hooks:
- id: add-trailing-comma
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/hhatto/autopep8
rev: v2.3.2
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 7.3.0 rev: 3.9.2
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-typing-imports==1.10.0]
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.5.7
hooks:
- id: autopep8
- repo: https://github.com/pre-commit/pre-commit
rev: v2.15.0
hooks:
- id: validate_manifest
- repo: https://github.com/asottile/pyupgrade
rev: v2.25.0
hooks:
- id: pyupgrade
args: [--py36-plus]
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.6.0
hooks:
- id: reorder-python-imports
args: [--py3-plus]
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.1.0
hooks:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.17.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1 rev: v0.910
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: [types-pyyaml] additional_dependencies: [types-all]
exclude: ^testing/resources/ exclude: ^testing/resources/
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes

View file

@ -1,6 +1,6 @@
- id: validate_manifest - id: validate_manifest
name: validate pre-commit manifest name: validate pre-commit manifest
description: This validator validates a pre-commit hooks manifest file description: This validator validates a pre-commit hooks manifest file
entry: pre-commit validate-manifest entry: pre-commit-validate-manifest
language: python language: python
files: ^\.pre-commit-hooks\.yaml$ files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$

View file

@ -1,579 +1,3 @@
4.5.1 - 2025-12-16
==================
### Fixes
- Fix `language: python` with `repo: local` without `additional_dependencies`.
- #3597 PR by @asottile.
4.5.0 - 2025-11-22
==================
### Features
- Add `pre-commit hazmat`.
- #3585 PR by @asottile.
4.4.0 - 2025-11-08
==================
### Features
- Add `--fail-fast` option to `pre-commit run`.
- #3528 PR by @JulianMaurin.
- Upgrade `ruby-build` / `rbenv`.
- #3566 PR by @asottile.
- #3565 issue by @MRigal.
- Add `language: unsupported` / `language: unsupported_script` as aliases
for `language: system` / `language: script` (which will eventually be
deprecated).
- #3577 PR by @asottile.
- Add support docker-in-docker detection for cgroups v2.
- #3535 PR by @br-rhrbacek.
- #3360 issue by @JasonAlt.
### Fixes
- Handle when docker gives `SecurityOptions: null`.
- #3537 PR by @asottile.
- #3514 issue by @jenstroeger.
- Fix error context for invalid `stages` in `.pre-commit-config.yaml`.
- #3576 PR by @asottile.
4.3.0 - 2025-08-09
==================
### Features
- `language: docker` / `language: docker_image`: detect rootless docker.
- #3446 PR by @matthewhughes934.
- #1243 issue by @dkolepp.
- `language: julia`: avoid `startup.jl` when executing hooks.
- #3496 PR by @ericphanson.
- `language: dart`: support latest dart versions which require a higher sdk
lower bound.
- #3507 PR by @bc-lee.
4.2.0 - 2025-03-18
==================
### Features
- For `language: python` first attempt a versioned python executable for
the default language version before consulting a potentially unversioned
`sys.executable`.
- #3430 PR by @asottile.
### Fixes
- Handle error during conflict detection when a file is named "HEAD"
- #3425 PR by @tusharsadhwani.
4.1.0 - 2025-01-20
==================
### Features
- Add `language: julia`.
- #3348 PR by @fredrikekre.
- #2689 issue @jmuchovej.
### Fixes
- Disable automatic toolchain switching for `language: golang`.
- #3304 PR by @AleksaC.
- #3300 issue by @AleksaC.
- #3149 issue by @nijel.
- Fix `language: r` installation when initiated by RStudio.
- #3389 PR by @lorenzwalthert.
- #3385 issue by @lorenzwalthert.
4.0.1 - 2024-10-08
==================
### Fixes
- Fix `pre-commit migrate-config` for unquoted deprecated stages names with
purelib `pyyaml`.
- #3324 PR by @asottile.
- pre-commit-ci/issues#234 issue by @lorenzwalthert.
4.0.0 - 2024-10-05
==================
### Features
- Improve `pre-commit migrate-config` to handle more yaml formats.
- #3301 PR by @asottile.
- Handle `stages` deprecation in `pre-commit migrate-config`.
- #3302 PR by @asottile.
- #2732 issue by @asottile.
- Upgrade `ruby-build`.
- #3199 PR by @ThisGuyCodes.
- Add "sensible regex" warnings to `repo: meta`.
- #3311 PR by @asottile.
- Add warnings for deprecated `stages` (`commit` -> `pre-commit`, `push` ->
`pre-push`, `merge-commit` -> `pre-merge-commit`).
- #3312 PR by @asottile.
- #3313 PR by @asottile.
- #3315 PR by @asottile.
- #2732 issue by @asottile.
### Updating
- `language: python_venv` has been removed -- use `language: python` instead.
- #3320 PR by @asottile.
- #2734 issue by @asottile.
3.8.0 - 2024-07-28
==================
### Features
- Implement health checks for `language: r` so environments are recreated if
the system version of R changes.
- #3206 issue by @lorenzwalthert.
- #3265 PR by @lorenzwalthert.
3.7.1 - 2024-05-10
==================
### Fixes
- Fix `language: rust` default language version check when `rust-toolchain.toml`
is present.
- issue by @gaborbernat.
- #3201 PR by @asottile.
3.7.0 - 2024-03-24
==================
### Features
- Use a tty for `docker` and `docker_image` hooks when `--color` is specified.
- #3122 PR by @glehmann.
### Fixes
- Fix `fail_fast` for individual hooks stopping when previous hooks had failed.
- #3167 issue by @tp832944.
- #3168 PR by @asottile.
### Updating
- The per-hook behaviour of `fail_fast` was fixed. If you want the pre-3.7.0
behaviour, add `fail_fast: true` to all hooks before the last `fail_fast`
hook.
3.6.2 - 2024-02-18
==================
### Fixes
- Fix building golang hooks during `git commit --all`.
- #3130 PR by @asottile.
- #2722 issue by @pestanko and @matthewhughes934.
3.6.1 - 2024-02-10
==================
### Fixes
- Remove `PYTHONEXECUTABLE` from environment when running.
- #3110 PR by @untitaker.
- Handle staged-files-only with only a crlf diff.
- #3126 PR by @asottile.
- issue by @tyyrok.
3.6.0 - 2023-12-09
==================
### Features
- Check `minimum_pre_commit_version` first when parsing configs.
- #3092 PR by @asottile.
### Fixes
- Fix deprecation warnings for `importlib.resources`.
- #3043 PR by @asottile.
- Fix deprecation warnings for rmtree.
- #3079 PR by @edgarrmondragon.
### Updating
- Drop support for python<3.9.
- #3042 PR by @asottile.
- #3093 PR by @asottile.
3.5.0 - 2023-10-13
==================
### Features
- Improve performance of `check-hooks-apply` and `check-useless-excludes`.
- #2998 PR by @mxr.
- #2935 issue by @mxr.
### Fixes
- Use `time.monotonic()` for more accurate hook timing.
- #3024 PR by @adamchainz.
### Updating
- Require npm 6.x+ for `language: node` hooks.
- #2996 PR by @RoelAdriaans.
- #1983 issue by @henryiii.
3.4.0 - 2023-09-02
==================
### Features
- Add `language: haskell`.
- #2932 by @alunduil.
- Improve cpu count detection when run under cgroups.
- #2979 PR by @jdb8.
- #2978 issue by @jdb8.
### Fixes
- Handle negative exit codes from hooks receiving posix signals.
- #2971 PR by @chriskuehl.
- #2970 issue by @chriskuehl.
3.3.3 - 2023-06-13
==================
### Fixes
- Work around OS packagers setting `--install-dir` / `--bin-dir` in gem settings.
- #2905 PR by @jaysoffian.
- #2799 issue by @lmilbaum.
3.3.2 - 2023-05-17
==================
### Fixes
- Work around `r` on windows sometimes double-un-quoting arguments.
- #2885 PR by @lorenzwalthert.
- #2870 issue by @lorenzwalthert.
3.3.1 - 2023-05-02
==================
### Fixes
- Work around `git` partial clone bug for `autoupdate` on windows.
- #2866 PR by @asottile.
- #2865 issue by @adehad.
3.3.0 - 2023-05-01
==================
### Features
- Upgrade ruby-build.
- #2846 PR by @jalessio.
- Use blobless clone for faster autoupdate.
- #2859 PR by @asottile.
- Add `-j` / `--jobs` argument to `autoupdate` for parallel execution.
- #2863 PR by @asottile.
- issue by @gaborbernat.
3.2.2 - 2023-04-03
==================
### Fixes
- Fix support for swift >= 5.8.
- #2836 PR by @edelabar.
- #2835 issue by @kgrobelny-intive.
3.2.1 - 2023-03-25
==================
### Fixes
- Fix `language_version` for `language: rust` without global `rustup`.
- #2823 issue by @daschuer.
- #2827 PR by @asottile.
3.2.0 - 2023-03-17
==================
### Features
- Allow `pre-commit`, `pre-push`, and `pre-merge-commit` as `stages`.
- #2732 issue by @asottile.
- #2808 PR by @asottile.
- Add `pre-rebase` hook support.
- #2582 issue by @BrutalSimplicity.
- #2725 PR by @mgaligniana.
### Fixes
- Remove bulky cargo cache from `language: rust` installs.
- #2820 PR by @asottile.
3.1.1 - 2023-02-27
==================
### Fixes
- Fix `rust` with `language_version` and a non-writable host `RUSTUP_HOME`.
- pre-commit-ci/issues#173 by @Swiftb0y.
- #2788 by @asottile.
3.1.0 - 2023-02-22
==================
### Fixes
- Fix `dotnet` for `.sln`-based hooks for dotnet>=7.0.200.
- #2763 PR by @m-rsha.
- Prevent stashing when `diff` fails to execute.
- #2774 PR by @asottile.
- #2773 issue by @strubbly.
- Dependencies are no longer sorted in repository key.
- #2776 PR by @asottile.
### Updating
- Deprecate `language: python_venv`. Use `language: python` instead.
- #2746 PR by @asottile.
- #2734 issue by @asottile.
3.0.4 - 2023-02-03
==================
### Fixes
- Fix hook diff detection for files affected by `--textconv`.
- #2743 PR by @adamchainz.
- #2743 issue by @adamchainz.
3.0.3 - 2023-02-01
==================
### Fixes
- Revert "Prevent local `Gemfile` from interfering with hook execution.".
- #2739 issue by @Roguelazer.
- #2740 PR by @asottile.
3.0.2 - 2023-01-29
==================
### Fixes
- Prevent local `Gemfile` from interfering with hook execution.
- #2727 PR by @asottile.
- Fix `language: r`, `repo: local` hooks
- pre-commit-ci/issues#107 by @lorenzwalthert.
- #2728 PR by @asottile.
3.0.1 - 2023-01-26
==================
### Fixes
- Ensure coursier hooks are available offline after install.
- #2723 PR by @asottile.
3.0.0 - 2023-01-23
==================
### Features
- Make `language: golang` bootstrap `go` if not present.
- #2651 PR by @taoufik07.
- #2649 issue by @taoufik07.
- `language: coursier` now supports `additional_dependencies` and `repo: local`
- #2702 PR by @asottile.
- Upgrade `ruby-build` to `20221225`.
- #2718 PR by @jalessio.
### Fixes
- Improve error message for invalid yaml for `pre-commit autoupdate`.
- #2686 PR by @asottile.
- #2685 issue by @CarstenGrohmann.
- `repo: local` no longer provisions an empty `git` repo.
- #2699 PR by @asottile.
### Updating
- Drop support for python<3.8
- #2655 PR by @asottile.
- Drop support for top-level list, use `pre-commit migrate-config` to update.
- #2656 PR by @asottile.
- Drop support for `sha` to specify revision, use `pre-commit migrate-config`
to update.
- #2657 PR by @asottile.
- Remove `pre-commit-validate-config` and `pre-commit-validate-manifest`, use
`pre-commit validate-config` and `pre-commit validate-manifest` instead.
- #2658 PR by @asottile.
- `language: golang` hooks must use `go.mod` to specify dependencies
- #2672 PR by @taoufik07.
2.21.0 - 2022-12-25
===================
### Features
- Require new-enough virtualenv to prevent 3.10 breakage
- #2467 PR by @asottile.
- Respect aliases with `SKIP` for environment install.
- #2480 PR by @kmARC.
- #2478 issue by @kmARC.
- Allow `pre-commit run --files` against unmerged paths.
- #2484 PR by @asottile.
- Also apply regex warnings to `repo: local` hooks.
- #2524 PR by @chrisRedwine.
- #2521 issue by @asottile.
- `rust` is now a "first class" language -- supporting `language_version` and
installation when not present.
- #2534 PR by @Holzhaus.
- `r` now uses more-reliable binary installation.
- #2460 PR by @lorenzwalthert.
- `GIT_ALLOW_PROTOCOL` is now passed through for git operations.
- #2555 PR by @asottile.
- `GIT_ASKPASS` is now passed through for git operations.
- #2564 PR by @mattp-.
- Remove `toml` dependency by using `cargo add` directly.
- #2568 PR by @m-rsha.
- Support `dotnet` hooks which have dotted prefixes.
- #2641 PR by @rkm.
- #2629 issue by @rkm.
### Fixes
- Properly adjust `--commit-msg-filename` if run from a sub directory.
- #2459 PR by @asottile.
- Simplify `--intent-to-add` detection by using `git diff`.
- #2580 PR by @m-rsha.
- Fix `R.exe` selection on windows.
- #2605 PR by @lorenzwalthert.
- #2599 issue by @SInginc.
- Skip default `nuget` source when installing `dotnet` packages.
- #2642 PR by @rkm.
2.20.0 - 2022-07-10
===================
### Features
- Expose `source` and `object-name` (positional args) of `prepare-commit-msg`
hook as `PRE_COMMIT_COMIT_MSG_SOURCE` and `PRE_COMMIT_COMMIT_OBJECT_NAME`.
- #2407 PR by @M-Whitaker.
- #2406 issue by @M-Whitaker.
### Fixes
- Fix `language: ruby` installs when `--user-install` is set in gemrc.
- #2394 PR by @narpfel.
- #2393 issue by @narpfel.
- Adjust pty setup for solaris.
- #2390 PR by @gaige.
- #2389 issue by @gaige.
- Remove unused `--config` option from `gc`, `sample-config`,
`validate-config`, `validate-manifest` sub-commands.
- #2429 PR by @asottile.
2.19.0 - 2022-05-05
===================
### Features
- Allow multiple outputs from `language: dotnet` hooks.
- #2332 PR by @WallucePinkham.
- Add more information to `healthy()` failure.
- #2348 PR by @asottile.
- Upgrade ruby-build.
- #2342 PR by @jalessio.
- Add `pre-commit validate-config` / `pre-commit validate-manifest` and
deprecate `pre-commit-validate-config` and `pre-commit-validate-manifest`.
- #2362 PR by @asottile.
### Fixes
- Fix `pre-push` when pushed ref contains spaces.
- #2345 PR by @wwade.
- #2344 issue by @wwade.
### Updating
- Change `pre-commit-validate-config` / `pre-commit-validate-manifest` to
`pre-commit validate-config` / `pre-commit validate-manifest`.
- #2362 PR by @asottile.
2.18.1 - 2022-04-02
===================
### Fixes
- Fix regression for `repo: local` hooks running `python<3.7`
- #2324 PR by @asottile.
2.18.0 - 2022-04-02
===================
### Features
- Keep `GIT_HTTP_PROXY_AUTHMETHOD` in git environ.
- #2272 PR by @VincentBerthier.
- #2271 issue by @VincentBerthier.
- Support both `cs` and `coursier` executables for coursier hooks.
- #2293 PR by @Holzhaus.
- Include more information in errors for `language_version` /
`additional_dependencies` for languages which do not support them.
- #2315 PR by @asottile.
- Have autoupdate preferentially pick tags which look like versions when
there are multiple equivalent tags.
- #2312 PR by @mblayman.
- #2311 issue by @mblayman.
- Upgrade `ruby-build`.
- #2319 PR by @jalessio.
- Add top level `default_install_hook_types` which will be installed when
`--hook-types` is not specified in `pre-commit install`.
- #2322 PR by @asottile.
### Fixes
- Fix typo in help message for `--from-ref` and `--to-ref`.
- #2266 PR by @leetrout.
- Prioritize binary builds for R dependencies.
- #2277 PR by @lorenzwalthert.
- Fix handling of git worktrees.
- #2252 PR by @daschuer.
- Fix handling of `$R_HOME` for R hooks.
- #2301 PR by @jeff-m-sullivan.
- #2300 issue by @jeff-m-sullivan.
- Fix a rare race condition in change stashing.
- #2323 PR by @asottile.
- #2287 issue by @ian-h-chamberlain.
### Updating
- Remove python3.6 support. Note that pre-commit still supports running hooks
written in older versions, but pre-commit itself requires python 3.7+.
- #2215 PR by @asottile.
- pre-commit has migrated from the `master` branch to `main`.
- #2302 PR by @asottile.
2.17.0 - 2022-01-18
===================
### Features
- add warnings for regexes containing `[\\/]`.
- #2151 issue by @sanjioh.
- #2154 PR by @kuviokelluja.
- upgrade supported ruby versions.
- #2205 PR by @jalessio.
- allow `language: conda` to use `mamba` or `micromamba` via
`PRE_COMMIT_USE_MAMBA=1` or `PRE_COMMIT_USE_MICROMAMBA=1` respectively.
- #2204 issue by @janjagusch.
- #2207 PR by @xhochy.
- display `git --version` in error report.
- #2210 PR by @asottile.
- add `language: lua` as a supported language.
- #2158 PR by @mblayman.
### Fixes
- temporarily add `setuptools` to the zipapp.
- #2122 issue by @andreoliwa.
- a737d5f commit by @asottile.
- use `go install` instead of `go get` for go 1.18+ support.
- #2161 PR by @schmir.
- fix `language: r` with a local renv and `RENV_PROJECT` set.
- #2170 PR by @lorenzwalthert.
- forbid overriding `entry` in `language: meta` hooks which breaks them.
- #2180 issue by @DanKaplanSES.
- #2181 PR by @asottile.
- always use `#!/bin/sh` on windows for hook script.
- #2182 issue by @hushigome-visco.
- #2187 PR by @asottile.
2.16.0 - 2021-11-30
===================
### Features
- add warning for regexes containing `[\/]` or `[/\\]`.
- #2053 PR by @radek-sprta.
- #2043 issue by @asottile.
- move hook template back to `bash` resolving shebang-portability issues.
- #2065 PR by @asottile.
- add support for `fail_fast` at the individual hook level.
- #2097 PR by @colens3.
- #1143 issue by @potiuk.
- allow passthrough of `GIT_CONFIG_KEY_*`, `GIT_CONFIG_VALUE_*`, and
`GIT_CONFIG_COUNT`.
- #2136 PR by @emzeat.
### Fixes
- fix pre-commit autoupdate for `core.useBuiltinFSMonitor=true` on windows.
- #2047 PR by @asottile.
- #2046 issue by @lcnittl.
- fix temporary file stashing with for `submodule.recurse=1`.
- #2071 PR by @asottile.
- #2063 issue by @a666.
- ban broken importlib-resources versions.
- #2098 PR by @asottile.
- replace `exit(...)` with `raise SystemExit(...)` for portability.
- #2103 PR by @asottile.
- #2104 PR by @asottile.
2.15.0 - 2021-09-02 2.15.0 - 2021-09-02
=================== ===================

View file

@ -5,6 +5,7 @@
- The complete test suite depends on having at least the following installed - The complete test suite depends on having at least the following installed
(possibly not a complete list) (possibly not a complete list)
- git (Version 2.24.0 or above is required to run pre-merge-commit tests) - git (Version 2.24.0 or above is required to run pre-merge-commit tests)
- python2 (Required by a test which checks different python versions)
- python3 (Required by a test which checks different python versions) - python3 (Required by a test which checks different python versions)
- tox (or virtualenv) - tox (or virtualenv)
- ruby + gem - ruby + gem
@ -64,14 +65,14 @@ to implement. The current implemented languages are at varying levels:
- 0th class - pre-commit does not require any dependencies for these languages - 0th class - pre-commit does not require any dependencies for these languages
as they're not actually languages (current examples: fail, pygrep) as they're not actually languages (current examples: fail, pygrep)
- 1st class - pre-commit will bootstrap a full interpreter requiring nothing to - 1st class - pre-commit will bootstrap a full interpreter requiring nothing to
be installed globally (current examples: go, node, ruby, rust) be installed globally (current examples: node, ruby)
- 2nd class - pre-commit requires the user to install the language globally but - 2nd class - pre-commit requires the user to install the language globally but
will install tools in an isolated fashion (current examples: python, swift, will install tools in an isolated fashion (current examples: python, go, rust,
docker). swift, docker).
- 3rd class - pre-commit requires the user to install both the tool and the - 3rd class - pre-commit requires the user to install both the tool and the
language globally (current examples: script, system) language globally (current examples: script, system)
"second class" is usually the easiest to implement first and is perfectly "third class" is usually the easiest to implement first and is perfectly
acceptable. acceptable.
Ideally the language works on the supported platforms for pre-commit (linux, Ideally the language works on the supported platforms for pre-commit (linux,
@ -92,7 +93,7 @@ language, for example:
here are the apis that should be implemented for a language here are the apis that should be implemented for a language
Note that these are also documented in [`pre_commit/lang_base.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/lang_base.py) Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/all.py)
#### `ENVIRONMENT_DIR` #### `ENVIRONMENT_DIR`
@ -111,21 +112,20 @@ one cannot be determined, return `'default'`.
You generally don't need to implement this on a first pass and can just use: You generally don't need to implement this on a first pass and can just use:
```python ```python
get_default_version = lang_base.basic_default_version get_default_version = helpers.basic_default_version
``` ```
`python` is currently the only language which implements this api `python` is currently the only language which implements this api
#### `health_check` #### `healthy`
This is used to check whether the installed environment is considered healthy. This is used to check whether the installed environment is considered healthy.
This function should return a detailed message if unhealthy or `None` if This function should return `True` or `False`.
healthy.
You generally don't need to implement this on a first pass and can just use: You generally don't need to implement this on a first pass and can just use:
```python ```python
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
``` ```
`python` is currently the only language which implements this api, for python `python` is currently the only language which implements this api, for python
@ -137,7 +137,7 @@ this is the trickiest one to implement and where all the smart parts happen.
this api should do the following things this api should do the following things
- (0th / 3rd class): `install_environment = lang_base.no_install` - (0th / 3rd class): `install_environment = helpers.no_install`
- (1st class): install a language runtime into the hook's directory - (1st class): install a language runtime into the hook's directory
- (2nd class): install the package at `.` into the `ENVIRONMENT_DIR` - (2nd class): install the package at `.` into the `ENVIRONMENT_DIR`
- (2nd class, optional): install packages listed in `additional_dependencies` - (2nd class, optional): install packages listed in `additional_dependencies`

View file

@ -1,5 +1,6 @@
[![build status](https://github.com/pre-commit/pre-commit/actions/workflows/main.yml/badge.svg)](https://github.com/pre-commit/pre-commit/actions/workflows/main.yml) [![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/main) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/master.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/master)
## pre-commit ## pre-commit

62
azure-pipelines.yml Normal file
View file

@ -0,0 +1,62 @@
trigger:
branches:
include: [master, test-me-*]
tags:
include: ['*']
resources:
repositories:
- repository: asottile
type: github
endpoint: github
name: asottile/azure-pipeline-templates
ref: refs/tags/v2.1.0
jobs:
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [py37]
os: windows
pre_test:
- task: UseRubyVersion@0
- powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts"
displayName: Add conda to PATH
- powershell: |
Write-Host "##vso[task.prependpath]C:\Strawberry\perl\bin"
Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin"
Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin"
displayName: Add strawberry perl to PATH
- bash: testing/get-dart.sh
displayName: install dart
- powershell: testing/get-r.ps1
displayName: install R
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [py37]
os: linux
name_postfix: _latest_git
pre_test:
- task: UseRubyVersion@0
- template: step--git-install.yml
- bash: testing/get-coursier.sh
displayName: install coursier
- bash: testing/get-dart.sh
displayName: install dart
- bash: testing/get-swift.sh
displayName: install swift
- bash: testing/get-r.sh
displayName: install R
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [pypy3, py36, py37, py38, py39]
os: linux
pre_test:
- task: UseRubyVersion@0
- bash: testing/get-coursier.sh
displayName: install coursier
- bash: testing/get-dart.sh
displayName: install dart
- bash: testing/get-swift.sh
displayName: install swift
- bash: testing/get-r.sh
displayName: install R

View file

@ -1,7 +1,5 @@
from __future__ import annotations
from pre_commit.main import main from pre_commit.main import main
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,50 +0,0 @@
from __future__ import annotations
from pre_commit.lang_base import Language
from pre_commit.languages import conda
from pre_commit.languages import coursier
from pre_commit.languages import dart
from pre_commit.languages import docker
from pre_commit.languages import docker_image
from pre_commit.languages import dotnet
from pre_commit.languages import fail
from pre_commit.languages import golang
from pre_commit.languages import haskell
from pre_commit.languages import julia
from pre_commit.languages import lua
from pre_commit.languages import node
from pre_commit.languages import perl
from pre_commit.languages import pygrep
from pre_commit.languages import python
from pre_commit.languages import r
from pre_commit.languages import ruby
from pre_commit.languages import rust
from pre_commit.languages import swift
from pre_commit.languages import unsupported
from pre_commit.languages import unsupported_script
languages: dict[str, Language] = {
'conda': conda,
'coursier': coursier,
'dart': dart,
'docker': docker,
'docker_image': docker_image,
'dotnet': dotnet,
'fail': fail,
'golang': golang,
'haskell': haskell,
'julia': julia,
'lua': lua,
'node': node,
'perl': perl,
'pygrep': pygrep,
'python': python,
'r': r,
'ruby': ruby,
'rust': rust,
'swift': swift,
'unsupported': unsupported,
'unsupported_script': unsupported_script,
}
language_names = sorted(languages)

View file

@ -1,43 +1,29 @@
from __future__ import annotations import argparse
import functools import functools
import logging import logging
import os.path
import re import re
import shlex import shlex
import sys import sys
from collections.abc import Callable
from collections.abc import Sequence
from typing import Any from typing import Any
from typing import NamedTuple from typing import Dict
from typing import Optional
from typing import Sequence
import cfgv import cfgv
from identify.identify import ALL_TAGS from identify.identify import ALL_TAGS
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit.all_languages import language_names from pre_commit.color import add_color_option
from pre_commit.errors import FatalError from pre_commit.errors import FatalError
from pre_commit.yaml import yaml_load from pre_commit.languages.all import all_languages
from pre_commit.logging_handler import logging_handler
from pre_commit.util import parse_version
from pre_commit.util import yaml_load
logger = logging.getLogger('pre_commit') logger = logging.getLogger('pre_commit')
check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex)
HOOK_TYPES = (
'commit-msg',
'post-checkout',
'post-commit',
'post-merge',
'post-rewrite',
'pre-commit',
'pre-merge-commit',
'pre-push',
'pre-rebase',
'prepare-commit-msg',
)
# `manual` is not invoked by any installed git hook. See #719
STAGES = (*HOOK_TYPES, 'manual')
def check_type_tag(tag: str) -> None: def check_type_tag(tag: str) -> None:
if tag not in ALL_TAGS: if tag not in ALL_TAGS:
@ -47,11 +33,6 @@ def check_type_tag(tag: str) -> None:
) )
def parse_version(s: str) -> tuple[int, ...]:
"""poor man's version comparison"""
return tuple(int(p) for p in s.split('.'))
def check_min_version(version: str) -> None: def check_min_version(version: str) -> None:
if parse_version(version) > parse_version(C.VERSION): if parse_version(version) > parse_version(C.VERSION):
raise cfgv.ValidationError( raise cfgv.ValidationError(
@ -61,186 +42,21 @@ def check_min_version(version: str) -> None:
) )
_STAGES = { def _make_argparser(filenames_help: str) -> argparse.ArgumentParser:
'commit': 'pre-commit', parser = argparse.ArgumentParser()
'merge-commit': 'pre-merge-commit', parser.add_argument('filenames', nargs='*', help=filenames_help)
'push': 'pre-push', parser.add_argument('-V', '--version', action='version', version=C.VERSION)
} add_color_option(parser)
return parser
def transform_stage(stage: str) -> str:
return _STAGES.get(stage, stage)
MINIMAL_MANIFEST_SCHEMA = cfgv.Array(
cfgv.Map(
'Hook', 'id',
cfgv.Required('id', cfgv.check_string),
cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []),
),
)
def warn_for_stages_on_repo_init(repo: str, directory: str) -> None:
try:
manifest = cfgv.load_from_filename(
os.path.join(directory, C.MANIFEST_FILE),
schema=MINIMAL_MANIFEST_SCHEMA,
load_strategy=yaml_load,
exc_tp=InvalidManifestError,
)
except InvalidManifestError:
return # they'll get a better error message when it actually loads!
legacy_stages = {} # sorted set
for hook in manifest:
for stage in hook.get('stages', ()):
if stage in _STAGES:
legacy_stages[stage] = True
if legacy_stages:
logger.warning(
f'repo `{repo}` uses deprecated stage names '
f'({", ".join(legacy_stages)}) which will be removed in a '
f'future version. '
f'Hint: often `pre-commit autoupdate --repo {shlex.quote(repo)}` '
f'will fix this. '
f'if it does not -- consider reporting an issue to that repo.',
)
class StagesMigrationNoDefault(NamedTuple):
key: str
default: Sequence[str]
def check(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
return
with cfgv.validate_context(f'At key: {self.key}'):
val = dct[self.key]
cfgv.check_array(cfgv.check_any)(val)
val = [transform_stage(v) for v in val]
cfgv.check_array(cfgv.check_one_of(STAGES))(val)
def apply_default(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
return
dct[self.key] = [transform_stage(v) for v in dct[self.key]]
def remove_default(self, dct: dict[str, Any]) -> None:
raise NotImplementedError
class StagesMigration(StagesMigrationNoDefault):
def apply_default(self, dct: dict[str, Any]) -> None:
dct.setdefault(self.key, self.default)
super().apply_default(dct)
class DeprecatedStagesWarning(NamedTuple):
key: str
def check(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
return
val = dct[self.key]
cfgv.check_array(cfgv.check_any)(val)
legacy_stages = [stage for stage in val if stage in _STAGES]
if legacy_stages:
logger.warning(
f'hook id `{dct["id"]}` uses deprecated stage names '
f'({", ".join(legacy_stages)}) which will be removed in a '
f'future version. '
f'run: `pre-commit migrate-config` to automatically fix this.',
)
def apply_default(self, dct: dict[str, Any]) -> None:
pass
def remove_default(self, dct: dict[str, Any]) -> None:
raise NotImplementedError
class DeprecatedDefaultStagesWarning(NamedTuple):
key: str
def check(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
return
val = dct[self.key]
cfgv.check_array(cfgv.check_any)(val)
legacy_stages = [stage for stage in val if stage in _STAGES]
if legacy_stages:
logger.warning(
f'top-level `default_stages` uses deprecated stage names '
f'({", ".join(legacy_stages)}) which will be removed in a '
f'future version. '
f'run: `pre-commit migrate-config` to automatically fix this.',
)
def apply_default(self, dct: dict[str, Any]) -> None:
pass
def remove_default(self, dct: dict[str, Any]) -> None:
raise NotImplementedError
def _translate_language(name: str) -> str:
return {
'system': 'unsupported',
'script': 'unsupported_script',
}.get(name, name)
class LanguageMigration(NamedTuple): # remove
key: str
check_fn: Callable[[object], None]
def check(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
return
with cfgv.validate_context(f'At key: {self.key}'):
self.check_fn(_translate_language(dct[self.key]))
def apply_default(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
return
dct[self.key] = _translate_language(dct[self.key])
def remove_default(self, dct: dict[str, Any]) -> None:
raise NotImplementedError
class LanguageMigrationRequired(LanguageMigration): # replace with Required
def check(self, dct: dict[str, Any]) -> None:
if self.key not in dct:
raise cfgv.ValidationError(f'Missing required key: {self.key}')
super().check(dct)
MANIFEST_HOOK_DICT = cfgv.Map( MANIFEST_HOOK_DICT = cfgv.Map(
'Hook', 'id', 'Hook', 'id',
# check first in case it uses some newer, incompatible feature
cfgv.Optional(
'minimum_pre_commit_version',
cfgv.check_and(cfgv.check_string, check_min_version),
'0',
),
cfgv.Required('id', cfgv.check_string), cfgv.Required('id', cfgv.check_string),
cfgv.Required('name', cfgv.check_string), cfgv.Required('name', cfgv.check_string),
cfgv.Required('entry', cfgv.check_string), cfgv.Required('entry', cfgv.check_string),
LanguageMigrationRequired('language', cfgv.check_one_of(language_names)), cfgv.Required('language', cfgv.check_one_of(all_languages)),
cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional('alias', cfgv.check_string, ''),
cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('files', check_string_regex, ''),
@ -254,13 +70,13 @@ MANIFEST_HOOK_DICT = cfgv.Map(
), ),
cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []), cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []),
cfgv.Optional('always_run', cfgv.check_bool, False), cfgv.Optional('always_run', cfgv.check_bool, False),
cfgv.Optional('fail_fast', cfgv.check_bool, False),
cfgv.Optional('pass_filenames', cfgv.check_bool, True), cfgv.Optional('pass_filenames', cfgv.check_bool, True),
cfgv.Optional('description', cfgv.check_string, ''), cfgv.Optional('description', cfgv.check_string, ''),
cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT),
cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('log_file', cfgv.check_string, ''),
cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'),
cfgv.Optional('require_serial', cfgv.check_bool, False), cfgv.Optional('require_serial', cfgv.check_bool, False),
StagesMigration('stages', []), cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []),
cfgv.Optional('verbose', cfgv.check_bool, False), cfgv.Optional('verbose', cfgv.check_bool, False),
) )
MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)
@ -270,29 +86,36 @@ class InvalidManifestError(FatalError):
pass pass
def _load_manifest_forward_compat(contents: str) -> object:
obj = yaml_load(contents)
if isinstance(obj, dict):
check_min_version('5')
raise AssertionError('unreachable')
else:
return obj
load_manifest = functools.partial( load_manifest = functools.partial(
cfgv.load_from_filename, cfgv.load_from_filename,
schema=MANIFEST_SCHEMA, schema=MANIFEST_SCHEMA,
load_strategy=_load_manifest_forward_compat, load_strategy=yaml_load,
exc_tp=InvalidManifestError, exc_tp=InvalidManifestError,
) )
def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int:
parser = _make_argparser('Manifest filenames.')
args = parser.parse_args(argv)
with logging_handler(args.color):
ret = 0
for filename in args.filenames:
try:
load_manifest(filename)
except InvalidManifestError as e:
print(e)
ret = 1
return ret
LOCAL = 'local' LOCAL = 'local'
META = 'meta' META = 'meta'
class WarnMutableRev(cfgv.Conditional): # should inherit from cfgv.Conditional if sha support is dropped
def check(self, dct: dict[str, Any]) -> None: class WarnMutableRev(cfgv.ConditionalOptional):
def check(self, dct: Dict[str, Any]) -> None:
super().check(dct) super().check(dct)
if self.key in dct: if self.key in dct:
@ -311,7 +134,7 @@ class WarnMutableRev(cfgv.Conditional):
class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault):
def check(self, dct: dict[str, Any]) -> None: def check(self, dct: Dict[str, Any]) -> None:
super().check(dct) super().check(dct)
if '/*' in dct.get(self.key, ''): if '/*' in dct.get(self.key, ''):
@ -320,17 +143,10 @@ class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault):
f"regex, not a glob -- matching '/*' probably isn't what you " f"regex, not a glob -- matching '/*' probably isn't what you "
f'want here', f'want here',
) )
for fwd_slash_re in (r'[\\/]', r'[\/]', r'[/\\]'):
if fwd_slash_re in dct.get(self.key, ''):
logger.warning(
fr'pre-commit normalizes slashes in the {self.key!r} '
fr'field in hook {dct.get("id")!r} to forward slashes, '
fr'so you can use / instead of {fwd_slash_re}',
)
class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault):
def check(self, dct: dict[str, Any]) -> None: def check(self, dct: Dict[str, Any]) -> None:
super().check(dct) super().check(dct)
if '/*' in dct.get(self.key, ''): if '/*' in dct.get(self.key, ''):
@ -338,13 +154,36 @@ class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault):
f'The top-level {self.key!r} field is a regex, not a glob -- ' f'The top-level {self.key!r} field is a regex, not a glob -- '
f"matching '/*' probably isn't what you want here", f"matching '/*' probably isn't what you want here",
) )
for fwd_slash_re in (r'[\\/]', r'[\/]', r'[/\\]'):
if fwd_slash_re in dct.get(self.key, ''):
logger.warning( class MigrateShaToRev:
fr'pre-commit normalizes the slashes in the top-level ' key = 'rev'
fr'{self.key!r} field to forward slashes, so you '
fr'can use / instead of {fwd_slash_re}', @staticmethod
) def _cond(key: str) -> cfgv.Conditional:
return cfgv.Conditional(
key, cfgv.check_string,
condition_key='repo',
condition_value=cfgv.NotIn(LOCAL, META),
ensure_absent=True,
)
def check(self, dct: Dict[str, Any]) -> None:
if dct.get('repo') in {LOCAL, META}:
self._cond('rev').check(dct)
self._cond('sha').check(dct)
elif 'sha' in dct and 'rev' in dct:
raise cfgv.ValidationError('Cannot specify both sha and rev')
elif 'sha' in dct:
self._cond('sha').check(dct)
else:
self._cond('rev').check(dct)
def apply_default(self, dct: Dict[str, Any]) -> None:
if 'sha' in dct:
dct['rev'] = dct.pop('sha')
remove_default = cfgv.Required.remove_default
def _entry(modname: str) -> str: def _entry(modname: str) -> str:
@ -358,7 +197,7 @@ def _entry(modname: str) -> str:
def warn_unknown_keys_root( def warn_unknown_keys_root(
extra: Sequence[str], extra: Sequence[str],
orig_keys: Sequence[str], orig_keys: Sequence[str],
dct: dict[str, str], dct: Dict[str, str],
) -> None: ) -> None:
logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}') logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}')
@ -366,7 +205,7 @@ def warn_unknown_keys_root(
def warn_unknown_keys_repo( def warn_unknown_keys_repo(
extra: Sequence[str], extra: Sequence[str],
orig_keys: Sequence[str], orig_keys: Sequence[str],
dct: dict[str, str], dct: Dict[str, str],
) -> None: ) -> None:
logger.warning( logger.warning(
f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}', f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}',
@ -397,29 +236,12 @@ _meta = (
), ),
) )
class NotAllowed(cfgv.OptionalNoDefault):
def check(self, dct: dict[str, Any]) -> None:
if self.key in dct:
raise cfgv.ValidationError(f'{self.key!r} cannot be overridden')
_COMMON_HOOK_WARNINGS = (
OptionalSensibleRegexAtHook('files', cfgv.check_string),
OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
DeprecatedStagesWarning('stages'),
)
META_HOOK_DICT = cfgv.Map( META_HOOK_DICT = cfgv.Map(
'Hook', 'id', 'Hook', 'id',
cfgv.Required('id', cfgv.check_string), cfgv.Required('id', cfgv.check_string),
cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))),
# language must be `unsupported` # language must be system
cfgv.Optional( cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'),
'language', cfgv.check_one_of({'unsupported'}), 'unsupported',
),
# entry cannot be overridden
NotAllowed('entry', cfgv.check_any),
*( *(
# default to the hook definition for the meta hooks # default to the hook definition for the meta hooks
cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id)
@ -434,7 +256,6 @@ META_HOOK_DICT = cfgv.Map(
item item
for item in MANIFEST_HOOK_DICT.items for item in MANIFEST_HOOK_DICT.items
), ),
*_COMMON_HOOK_WARNINGS,
) )
CONFIG_HOOK_DICT = cfgv.Map( CONFIG_HOOK_DICT = cfgv.Map(
'Hook', 'id', 'Hook', 'id',
@ -449,18 +270,9 @@ CONFIG_HOOK_DICT = cfgv.Map(
cfgv.OptionalNoDefault(item.key, item.check_fn) cfgv.OptionalNoDefault(item.key, item.check_fn)
for item in MANIFEST_HOOK_DICT.items for item in MANIFEST_HOOK_DICT.items
if item.key != 'id' if item.key != 'id'
if item.key != 'stages'
if item.key != 'language' # remove
), ),
StagesMigrationNoDefault('stages', []), OptionalSensibleRegexAtHook('files', cfgv.check_string),
LanguageMigration('language', cfgv.check_one_of(language_names)), # remove OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
*_COMMON_HOOK_WARNINGS,
)
LOCAL_HOOK_DICT = cfgv.Map(
'Hook', 'id',
*MANIFEST_HOOK_DICT.items,
*_COMMON_HOOK_WARNINGS,
) )
CONFIG_REPO_DICT = cfgv.Map( CONFIG_REPO_DICT = cfgv.Map(
'Repository', 'repo', 'Repository', 'repo',
@ -472,7 +284,7 @@ CONFIG_REPO_DICT = cfgv.Map(
'repo', cfgv.NotIn(LOCAL, META), 'repo', cfgv.NotIn(LOCAL, META),
), ),
cfgv.ConditionalRecurse( cfgv.ConditionalRecurse(
'hooks', cfgv.Array(LOCAL_HOOK_DICT), 'hooks', cfgv.Array(MANIFEST_HOOK_DICT),
'repo', LOCAL, 'repo', LOCAL,
), ),
cfgv.ConditionalRecurse( cfgv.ConditionalRecurse(
@ -480,47 +292,45 @@ CONFIG_REPO_DICT = cfgv.Map(
'repo', META, 'repo', META,
), ),
MigrateShaToRev(),
WarnMutableRev( WarnMutableRev(
'rev', cfgv.check_string, 'rev',
condition_key='repo', cfgv.check_string,
condition_value=cfgv.NotIn(LOCAL, META), '',
ensure_absent=True, 'repo',
cfgv.NotIn(LOCAL, META),
True,
), ),
cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo), cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo),
) )
DEFAULT_LANGUAGE_VERSION = cfgv.Map( DEFAULT_LANGUAGE_VERSION = cfgv.Map(
'DefaultLanguageVersion', None, 'DefaultLanguageVersion', None,
cfgv.NoAdditionalKeys(language_names), cfgv.NoAdditionalKeys(all_languages),
*(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in language_names), *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages),
) )
CONFIG_SCHEMA = cfgv.Map( CONFIG_SCHEMA = cfgv.Map(
'Config', None, 'Config', None,
# check first in case it uses some newer, incompatible feature cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
cfgv.OptionalRecurse(
'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
),
cfgv.Optional(
'default_stages',
cfgv.check_array(cfgv.check_one_of(C.STAGES)),
C.STAGES,
),
cfgv.Optional('files', check_string_regex, ''),
cfgv.Optional('exclude', check_string_regex, '^$'),
cfgv.Optional('fail_fast', cfgv.check_bool, False),
cfgv.Optional( cfgv.Optional(
'minimum_pre_commit_version', 'minimum_pre_commit_version',
cfgv.check_and(cfgv.check_string, check_min_version), cfgv.check_and(cfgv.check_string, check_min_version),
'0', '0',
), ),
cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
cfgv.Optional(
'default_install_hook_types',
cfgv.check_array(cfgv.check_one_of(HOOK_TYPES)),
['pre-commit'],
),
cfgv.OptionalRecurse(
'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
),
StagesMigration('default_stages', STAGES),
DeprecatedDefaultStagesWarning('default_stages'),
cfgv.Optional('files', check_string_regex, ''),
cfgv.Optional('exclude', check_string_regex, '^$'),
cfgv.Optional('fail_fast', cfgv.check_bool, False),
cfgv.WarnAdditionalKeys( cfgv.WarnAdditionalKeys(
( (
'repos', 'repos',
'default_install_hook_types',
'default_language_version', 'default_language_version',
'default_stages', 'default_stages',
'files', 'files',
@ -543,9 +353,37 @@ class InvalidConfigError(FatalError):
pass pass
def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]:
data = yaml_load(contents)
if isinstance(data, list):
logger.warning(
'normalizing pre-commit configuration to a top-level map. '
'support for top level list will be removed in a future version. '
'run: `pre-commit migrate-config` to automatically fix this.',
)
return {'repos': data}
else:
return data
load_config = functools.partial( load_config = functools.partial(
cfgv.load_from_filename, cfgv.load_from_filename,
schema=CONFIG_SCHEMA, schema=CONFIG_SCHEMA,
load_strategy=yaml_load, load_strategy=ordered_load_normalize_legacy_config,
exc_tp=InvalidConfigError, exc_tp=InvalidConfigError,
) )
def validate_config_main(argv: Optional[Sequence[str]] = None) -> int:
parser = _make_argparser('Config filenames.')
args = parser.parse_args(argv)
with logging_handler(args.color):
ret = 0
for filename in args.filenames:
try:
load_config(filename)
except InvalidConfigError as e:
print(e)
ret = 1
return ret

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import argparse import argparse
import os import os
import sys import sys

View file

@ -1,85 +1,62 @@
from __future__ import annotations
import concurrent.futures
import os.path import os.path
import re import re
import tempfile
from collections.abc import Sequence
from typing import Any from typing import Any
from typing import Dict
from typing import List
from typing import NamedTuple from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import git from pre_commit import git
from pre_commit import output from pre_commit import output
from pre_commit import xargs
from pre_commit.clientlib import InvalidManifestError from pre_commit.clientlib import InvalidManifestError
from pre_commit.clientlib import load_config from pre_commit.clientlib import load_config
from pre_commit.clientlib import load_manifest from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META from pre_commit.clientlib import META
from pre_commit.commands.migrate_config import migrate_config from pre_commit.commands.migrate_config import migrate_config
from pre_commit.store import Store
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.yaml import yaml_dump from pre_commit.util import tmpdir
from pre_commit.yaml import yaml_load from pre_commit.util import yaml_dump
from pre_commit.util import yaml_load
class RevInfo(NamedTuple): class RevInfo(NamedTuple):
repo: str repo: str
rev: str rev: str
frozen: str | None = None frozen: Optional[str]
hook_ids: frozenset[str] = frozenset()
@classmethod @classmethod
def from_config(cls, config: dict[str, Any]) -> RevInfo: def from_config(cls, config: Dict[str, Any]) -> 'RevInfo':
return cls(config['repo'], config['rev']) return cls(config['repo'], config['rev'], None)
def update(self, tags_only: bool, freeze: bool) -> RevInfo: def update(self, tags_only: bool, freeze: bool) -> 'RevInfo':
with tempfile.TemporaryDirectory() as tmp: if tags_only:
_git = ('git', *git.NO_FS_MONITOR, '-C', tmp) tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0')
else:
if tags_only: tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact')
tag_opt = '--abbrev=0'
else:
tag_opt = '--exact'
tag_cmd = (*_git, 'describe', 'FETCH_HEAD', '--tags', tag_opt)
with tmpdir() as tmp:
git.init_repo(tmp, self.repo) git.init_repo(tmp, self.repo)
cmd_output_b(*_git, 'config', 'extensions.partialClone', 'true') cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp)
cmd_output_b(
*_git, 'fetch', 'origin', 'HEAD',
'--quiet', '--filter=blob:none', '--tags',
)
try: try:
rev = cmd_output(*tag_cmd)[1].strip() rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip()
except CalledProcessError: except CalledProcessError:
rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip() cmd = ('git', 'rev-parse', 'FETCH_HEAD')
else: rev = cmd_output(*cmd, cwd=tmp)[1].strip()
if tags_only:
rev = git.get_best_candidate_tag(rev, tmp)
frozen = None frozen = None
if freeze: if freeze:
exact = cmd_output(*_git, 'rev-parse', rev)[1].strip() exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip()
if exact != rev: if exact != rev:
rev, frozen = exact, rev rev, frozen = exact, rev
return self._replace(rev=rev, frozen=frozen)
try:
# workaround for windows -- see #2865
cmd_output_b(*_git, 'show', f'{rev}:{C.MANIFEST_FILE}')
cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE)
except CalledProcessError:
pass # this will be caught by manifest validating code
try:
manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE))
except InvalidManifestError as e:
raise RepositoryCannotBeUpdatedError(f'[{self.repo}] {e}')
else:
hook_ids = frozenset(hook['id'] for hook in manifest)
return self._replace(rev=rev, frozen=frozen, hook_ids=hook_ids)
class RepositoryCannotBeUpdatedError(RuntimeError): class RepositoryCannotBeUpdatedError(RuntimeError):
@ -87,40 +64,34 @@ class RepositoryCannotBeUpdatedError(RuntimeError):
def _check_hooks_still_exist_at_rev( def _check_hooks_still_exist_at_rev(
repo_config: dict[str, Any], repo_config: Dict[str, Any],
info: RevInfo, info: RevInfo,
store: Store,
) -> None: ) -> None:
try:
path = store.clone(repo_config['repo'], info.rev)
manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE))
except InvalidManifestError as e:
raise RepositoryCannotBeUpdatedError(str(e))
# See if any of our hooks were deleted with the new commits # See if any of our hooks were deleted with the new commits
hooks = {hook['id'] for hook in repo_config['hooks']} hooks = {hook['id'] for hook in repo_config['hooks']}
hooks_missing = hooks - info.hook_ids hooks_missing = hooks - {hook['id'] for hook in manifest}
if hooks_missing: if hooks_missing:
raise RepositoryCannotBeUpdatedError( raise RepositoryCannotBeUpdatedError(
f'[{info.repo}] Cannot update because the update target is ' f'Cannot update because the update target is missing these '
f'missing these hooks: {", ".join(sorted(hooks_missing))}', f'hooks:\n{", ".join(sorted(hooks_missing))}',
) )
def _update_one(
i: int,
repo: dict[str, Any],
*,
tags_only: bool,
freeze: bool,
) -> tuple[int, RevInfo, RevInfo]:
old = RevInfo.from_config(repo)
new = old.update(tags_only=tags_only, freeze=freeze)
_check_hooks_still_exist_at_rev(repo, new)
return i, old, new
REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$')
def _original_lines( def _original_lines(
path: str, path: str,
rev_infos: list[RevInfo | None], rev_infos: List[Optional[RevInfo]],
retry: bool = False, retry: bool = False,
) -> tuple[list[str], list[int]]: ) -> Tuple[List[str], List[int]]:
"""detect `rev:` lines or reformat the file""" """detect `rev:` lines or reformat the file"""
with open(path, newline='') as f: with open(path, newline='') as f:
original = f.read() original = f.read()
@ -137,7 +108,7 @@ def _original_lines(
return _original_lines(path, rev_infos, retry=True) return _original_lines(path, rev_infos, retry=True)
def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None: def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None:
lines, idxs = _original_lines(path, rev_infos) lines, idxs = _original_lines(path, rev_infos)
for idx, rev_info in zip(idxs, rev_infos): for idx, rev_info in zip(idxs, rev_infos):
@ -161,53 +132,49 @@ def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None:
def autoupdate( def autoupdate(
config_file: str, config_file: str,
store: Store,
tags_only: bool, tags_only: bool,
freeze: bool, freeze: bool,
repos: Sequence[str] = (), repos: Sequence[str] = (),
jobs: int = 1,
) -> int: ) -> int:
"""Auto-update the pre-commit config to the latest versions of repos.""" """Auto-update the pre-commit config to the latest versions of repos."""
migrate_config(config_file, quiet=True) migrate_config(config_file, quiet=True)
changed = False
retv = 0 retv = 0
rev_infos: List[Optional[RevInfo]] = []
changed = False
config_repos = [ config = load_config(config_file)
repo for repo in load_config(config_file)['repos'] for repo_config in config['repos']:
if repo['repo'] not in {LOCAL, META} if repo_config['repo'] in {LOCAL, META}:
] continue
rev_infos: list[RevInfo | None] = [None] * len(config_repos) info = RevInfo.from_config(repo_config)
jobs = jobs or xargs.cpu_count() # 0 => number of cpus if repos and info.repo not in repos:
jobs = min(jobs, len(repos) or len(config_repos)) # max 1-per-thread rev_infos.append(None)
jobs = max(jobs, 1) # at least one thread continue
with concurrent.futures.ThreadPoolExecutor(jobs) as exe:
futures = [ output.write(f'Updating {info.repo} ... ')
exe.submit( new_info = info.update(tags_only=tags_only, freeze=freeze)
_update_one, try:
i, repo, tags_only=tags_only, freeze=freeze, _check_hooks_still_exist_at_rev(repo_config, new_info, store)
) except RepositoryCannotBeUpdatedError as error:
for i, repo in enumerate(config_repos) output.write_line(error.args[0])
if not repos or repo['repo'] in repos rev_infos.append(None)
] retv = 1
for future in concurrent.futures.as_completed(futures): continue
try:
i, old, new = future.result() if new_info.rev != info.rev:
except RepositoryCannotBeUpdatedError as e: changed = True
output.write_line(str(e)) if new_info.frozen:
retv = 1 updated_to = f'{new_info.frozen} (frozen)'
else: else:
if new.rev != old.rev: updated_to = new_info.rev
changed = True msg = f'updating {info.rev} -> {updated_to}.'
if new.frozen: output.write_line(msg)
new_s = f'{new.frozen} (frozen)' rev_infos.append(new_info)
else: else:
new_s = new.rev output.write_line('already up to date.')
msg = f'updating {old.rev} -> {new_s}' rev_infos.append(None)
rev_infos[i] = new
else:
msg = 'already up to date!'
output.write_line(f'[{old.repo}] {msg}')
if changed: if changed:
_write_new_config(config_file, rev_infos) _write_new_config(config_file, rev_infos)

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import os.path import os.path
from pre_commit import output from pre_commit import output

View file

@ -1,7 +1,8 @@
from __future__ import annotations
import os.path import os.path
from typing import Any from typing import Any
from typing import Dict
from typing import Set
from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import output from pre_commit import output
@ -12,14 +13,13 @@ from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META from pre_commit.clientlib import META
from pre_commit.store import Store from pre_commit.store import Store
from pre_commit.util import rmtree
def _mark_used_repos( def _mark_used_repos(
store: Store, store: Store,
all_repos: dict[tuple[str, str], str], all_repos: Dict[Tuple[str, str], str],
unused_repos: set[tuple[str, str]], unused_repos: Set[Tuple[str, str]],
repo: dict[str, Any], repo: Dict[str, Any],
) -> None: ) -> None:
if repo['repo'] == META: if repo['repo'] == META:
return return
@ -27,8 +27,7 @@ def _mark_used_repos(
for hook in repo['hooks']: for hook in repo['hooks']:
deps = hook.get('additional_dependencies') deps = hook.get('additional_dependencies')
unused_repos.discard(( unused_repos.discard((
store.db_repo_name(repo['repo'], deps), store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION,
C.LOCAL_REPO_VERSION,
)) ))
else: else:
key = (repo['repo'], repo['rev']) key = (repo['repo'], repo['rev'])
@ -58,41 +57,34 @@ def _mark_used_repos(
)) ))
def _gc(store: Store) -> int: def _gc_repos(store: Store) -> int:
with store.exclusive_lock(), store.connect() as db: configs = store.select_all_configs()
store._create_configs_table(db) repos = store.select_all_repos()
repos = db.execute('SELECT repo, ref, path FROM repos').fetchall() # delete config paths which do not exist
all_repos = {(repo, ref): path for repo, ref, path in repos} dead_configs = [p for p in configs if not os.path.exists(p)]
unused_repos = set(all_repos) live_configs = [p for p in configs if os.path.exists(p)]
configs_rows = db.execute('SELECT path FROM configs').fetchall() all_repos = {(repo, ref): path for repo, ref, path in repos}
configs = [path for path, in configs_rows] unused_repos = set(all_repos)
for config_path in live_configs:
try:
config = load_config(config_path)
except InvalidConfigError:
dead_configs.append(config_path)
continue
else:
for repo in config['repos']:
_mark_used_repos(store, all_repos, unused_repos, repo)
dead_configs = [] store.delete_configs(dead_configs)
for config_path in configs: for db_repo_name, ref in unused_repos:
try: store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)])
config = load_config(config_path) return len(unused_repos)
except InvalidConfigError:
dead_configs.append(config_path)
continue
else:
for repo in config['repos']:
_mark_used_repos(store, all_repos, unused_repos, repo)
paths = [(path,) for path in dead_configs]
db.executemany('DELETE FROM configs WHERE path = ?', paths)
db.executemany(
'DELETE FROM repos WHERE repo = ? and ref = ?',
sorted(unused_repos),
)
for k in unused_repos:
rmtree(all_repos[k])
return len(unused_repos)
def gc(store: Store) -> int: def gc(store: Store) -> int:
output.write_line(f'{_gc(store)} repo(s) removed.') with store.exclusive_lock():
repos_removed = _gc_repos(store)
output.write_line(f'{repos_removed} repo(s) removed.')
return 0 return 0

View file

@ -1,95 +0,0 @@
from __future__ import annotations
import argparse
import subprocess
from collections.abc import Sequence
from pre_commit.parse_shebang import normalize_cmd
def add_parsers(parser: argparse.ArgumentParser) -> None:
subparsers = parser.add_subparsers(dest='tool')
cd_parser = subparsers.add_parser(
'cd', help='cd to a subdir and run the command',
)
cd_parser.add_argument('subdir')
cd_parser.add_argument('cmd', nargs=argparse.REMAINDER)
ignore_exit_code_parser = subparsers.add_parser(
'ignore-exit-code', help='run the command but ignore the exit code',
)
ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER)
n1_parser = subparsers.add_parser(
'n1', help='run the command once per filename',
)
n1_parser.add_argument('cmd', nargs=argparse.REMAINDER)
def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[
tuple[str, ...],
tuple[str, ...],
]:
for idx, val in enumerate(reversed(cmd)):
if val == '--':
split = len(cmd) - idx
break
else:
raise SystemExit('hazmat entry must end with `--`')
return cmd[:split - 1], cmd[split:]
def cd(subdir: str, cmd: tuple[str, ...]) -> int:
cmd, filenames = _cmd_filenames(cmd)
prefix = f'{subdir}/'
new_filenames = []
for filename in filenames:
if not filename.startswith(prefix):
raise SystemExit(f'unexpected file without {prefix=}: {filename}')
else:
new_filenames.append(filename.removeprefix(prefix))
cmd = normalize_cmd(cmd)
return subprocess.call((*cmd, *new_filenames), cwd=subdir)
def ignore_exit_code(cmd: tuple[str, ...]) -> int:
cmd = normalize_cmd(cmd)
subprocess.call(cmd)
return 0
def n1(cmd: tuple[str, ...]) -> int:
cmd, filenames = _cmd_filenames(cmd)
cmd = normalize_cmd(cmd)
ret = 0
for filename in filenames:
ret |= subprocess.call((*cmd, filename))
return ret
def impl(args: argparse.Namespace) -> int:
args.cmd = tuple(args.cmd)
if args.tool == 'cd':
return cd(args.subdir, args.cmd)
elif args.tool == 'ignore-exit-code':
return ignore_exit_code(args.cmd)
elif args.tool == 'n1':
return n1(args.cmd)
else:
raise NotImplementedError(f'unexpected tool: {args.tool}')
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
add_parsers(parser)
args = parser.parse_args(argv)
return impl(args)
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -1,10 +1,10 @@
from __future__ import annotations
import argparse import argparse
import os.path import os.path
import subprocess import subprocess
import sys import sys
from collections.abc import Sequence from typing import Optional
from typing import Sequence
from typing import Tuple
from pre_commit.commands.run import run from pre_commit.commands.run import run
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
@ -18,7 +18,7 @@ def _run_legacy(
hook_type: str, hook_type: str,
hook_dir: str, hook_dir: str,
args: Sequence[str], args: Sequence[str],
) -> tuple[int, bytes]: ) -> Tuple[int, bytes]:
if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'): if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'):
raise SystemExit( raise SystemExit(
f"bug: pre-commit's script is installed in migration mode\n" f"bug: pre-commit's script is installed in migration mode\n"
@ -69,35 +69,27 @@ def _ns(
color: bool, color: bool,
*, *,
all_files: bool = False, all_files: bool = False,
remote_branch: str | None = None, remote_branch: Optional[str] = None,
local_branch: str | None = None, local_branch: Optional[str] = None,
from_ref: str | None = None, from_ref: Optional[str] = None,
to_ref: str | None = None, to_ref: Optional[str] = None,
pre_rebase_upstream: str | None = None, remote_name: Optional[str] = None,
pre_rebase_branch: str | None = None, remote_url: Optional[str] = None,
remote_name: str | None = None, commit_msg_filename: Optional[str] = None,
remote_url: str | None = None, checkout_type: Optional[str] = None,
commit_msg_filename: str | None = None, is_squash_merge: Optional[str] = None,
prepare_commit_message_source: str | None = None, rewrite_command: Optional[str] = None,
commit_object_name: str | None = None,
checkout_type: str | None = None,
is_squash_merge: str | None = None,
rewrite_command: str | None = None,
) -> argparse.Namespace: ) -> argparse.Namespace:
return argparse.Namespace( return argparse.Namespace(
color=color, color=color,
hook_stage=hook_type, hook_stage=hook_type.replace('pre-', ''),
remote_branch=remote_branch, remote_branch=remote_branch,
local_branch=local_branch, local_branch=local_branch,
from_ref=from_ref, from_ref=from_ref,
to_ref=to_ref, to_ref=to_ref,
pre_rebase_upstream=pre_rebase_upstream,
pre_rebase_branch=pre_rebase_branch,
remote_name=remote_name, remote_name=remote_name,
remote_url=remote_url, remote_url=remote_url,
commit_msg_filename=commit_msg_filename, commit_msg_filename=commit_msg_filename,
prepare_commit_message_source=prepare_commit_message_source,
commit_object_name=commit_object_name,
all_files=all_files, all_files=all_files,
checkout_type=checkout_type, checkout_type=checkout_type,
is_squash_merge=is_squash_merge, is_squash_merge=is_squash_merge,
@ -106,7 +98,6 @@ def _ns(
hook=None, hook=None,
verbose=False, verbose=False,
show_diff_on_failure=False, show_diff_on_failure=False,
fail_fast=False,
) )
@ -118,13 +109,12 @@ def _pre_push_ns(
color: bool, color: bool,
args: Sequence[str], args: Sequence[str],
stdin: bytes, stdin: bytes,
) -> argparse.Namespace | None: ) -> Optional[argparse.Namespace]:
remote_name = args[0] remote_name = args[0]
remote_url = args[1] remote_url = args[1]
for line in stdin.decode().splitlines(): for line in stdin.decode().splitlines():
parts = line.rsplit(maxsplit=3) local_branch, local_sha, remote_branch, remote_sha = line.split()
local_branch, local_sha, remote_branch, remote_sha = parts
if local_sha == Z40: if local_sha == Z40:
continue continue
elif remote_sha != Z40 and _rev_exists(remote_sha): elif remote_sha != Z40 and _rev_exists(remote_sha):
@ -190,12 +180,6 @@ def _check_args_length(hook_type: str, args: Sequence[str]) -> None:
f'hook-impl for {hook_type} expected 1, 2, or 3 arguments ' f'hook-impl for {hook_type} expected 1, 2, or 3 arguments '
f'but got {len(args)}: {args}', f'but got {len(args)}: {args}',
) )
elif hook_type == 'pre-rebase':
if len(args) < 1 or len(args) > 2:
raise SystemExit(
f'hook-impl for {hook_type} expected 1 or 2 arguments '
f'but got {len(args)}: {args}',
)
elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK: elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK:
expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type] expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type]
if len(args) != expected: if len(args) != expected:
@ -213,24 +197,12 @@ def _run_ns(
color: bool, color: bool,
args: Sequence[str], args: Sequence[str],
stdin: bytes, stdin: bytes,
) -> argparse.Namespace | None: ) -> Optional[argparse.Namespace]:
_check_args_length(hook_type, args) _check_args_length(hook_type, args)
if hook_type == 'pre-push': if hook_type == 'pre-push':
return _pre_push_ns(color, args, stdin) return _pre_push_ns(color, args, stdin)
elif hook_type in 'commit-msg': elif hook_type in {'commit-msg', 'prepare-commit-msg'}:
return _ns(hook_type, color, commit_msg_filename=args[0]) return _ns(hook_type, color, commit_msg_filename=args[0])
elif hook_type == 'prepare-commit-msg' and len(args) == 1:
return _ns(hook_type, color, commit_msg_filename=args[0])
elif hook_type == 'prepare-commit-msg' and len(args) == 2:
return _ns(
hook_type, color, commit_msg_filename=args[0],
prepare_commit_message_source=args[1],
)
elif hook_type == 'prepare-commit-msg' and len(args) == 3:
return _ns(
hook_type, color, commit_msg_filename=args[0],
prepare_commit_message_source=args[1], commit_object_name=args[2],
)
elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}: elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}:
return _ns(hook_type, color) return _ns(hook_type, color)
elif hook_type == 'post-checkout': elif hook_type == 'post-checkout':
@ -242,13 +214,6 @@ def _run_ns(
return _ns(hook_type, color, is_squash_merge=args[0]) return _ns(hook_type, color, is_squash_merge=args[0])
elif hook_type == 'post-rewrite': elif hook_type == 'post-rewrite':
return _ns(hook_type, color, rewrite_command=args[0]) return _ns(hook_type, color, rewrite_command=args[0])
elif hook_type == 'pre-rebase' and len(args) == 1:
return _ns(hook_type, color, pre_rebase_upstream=args[0])
elif hook_type == 'pre-rebase' and len(args) == 2:
return _ns(
hook_type, color, pre_rebase_upstream=args[0],
pre_rebase_branch=args[1],
)
else: else:
raise AssertionError(f'unexpected hook type: {hook_type}') raise AssertionError(f'unexpected hook type: {hook_type}')

View file

@ -1,7 +1,6 @@
from __future__ import annotations
import logging import logging
import os.path import os.path
from typing import Sequence
from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install
from pre_commit.store import Store from pre_commit.store import Store
@ -15,7 +14,7 @@ def init_templatedir(
config_file: str, config_file: str,
store: Store, store: Store,
directory: str, directory: str,
hook_types: list[str] | None, hook_types: Sequence[str],
skip_on_missing_config: bool = True, skip_on_missing_config: bool = True,
) -> int: ) -> int:
install( install(

View file

@ -1,14 +1,14 @@
from __future__ import annotations import itertools
import logging import logging
import os.path import os.path
import shlex
import shutil import shutil
import sys import sys
from typing import Optional
from typing import Sequence
from typing import Tuple
from pre_commit import git from pre_commit import git
from pre_commit import output from pre_commit import output
from pre_commit.clientlib import InvalidConfigError
from pre_commit.clientlib import load_config from pre_commit.clientlib import load_config
from pre_commit.repository import all_hooks from pre_commit.repository import all_hooks
from pre_commit.repository import install_hook_envs from pre_commit.repository import install_hook_envs
@ -30,25 +30,17 @@ PRIOR_HASHES = (
CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03' CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03'
TEMPLATE_START = '# start templated\n' TEMPLATE_START = '# start templated\n'
TEMPLATE_END = '# end templated\n' TEMPLATE_END = '# end templated\n'
# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH`
# #1312 os.defpath is too restrictive on BSD
def _hook_types(cfg_filename: str, hook_types: list[str] | None) -> list[str]: POSIX_SEARCH_PATH = ('/usr/local/bin', '/usr/bin', '/bin')
if hook_types is not None: SYS_EXE = os.path.basename(os.path.realpath(sys.executable))
return hook_types
else:
try:
cfg = load_config(cfg_filename)
except InvalidConfigError:
return ['pre-commit']
else:
return cfg['default_install_hook_types']
def _hook_paths( def _hook_paths(
hook_type: str, hook_type: str,
git_dir: str | None = None, git_dir: Optional[str] = None,
) -> tuple[str, str]: ) -> Tuple[str, str]:
git_dir = git_dir if git_dir is not None else git.get_git_common_dir() git_dir = git_dir if git_dir is not None else git.get_git_dir()
pth = os.path.join(git_dir, 'hooks', hook_type) pth = os.path.join(git_dir, 'hooks', hook_type)
return pth, f'{pth}.legacy' return pth, f'{pth}.legacy'
@ -61,12 +53,32 @@ def is_our_script(filename: str) -> bool:
return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES)
def shebang() -> str:
if sys.platform == 'win32':
py, _ = os.path.splitext(SYS_EXE)
else:
exe_choices = [
f'python{sys.version_info[0]}.{sys.version_info[1]}',
f'python{sys.version_info[0]}',
]
# avoid searching for bare `python` as it's likely to be python 2
if SYS_EXE != 'python':
exe_choices.append(SYS_EXE)
for path, exe in itertools.product(POSIX_SEARCH_PATH, exe_choices):
if os.access(os.path.join(path, exe), os.X_OK):
py = exe
break
else:
py = SYS_EXE
return f'#!/usr/bin/env {py}'
def _install_hook_script( def _install_hook_script(
config_file: str, config_file: str,
hook_type: str, hook_type: str,
overwrite: bool = False, overwrite: bool = False,
skip_on_missing_config: bool = False, skip_on_missing_config: bool = False,
git_dir: str | None = None, git_dir: Optional[str] = None,
) -> None: ) -> None:
hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir)
@ -88,23 +100,19 @@ def _install_hook_script(
args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}'] args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}']
if skip_on_missing_config: if skip_on_missing_config:
args.append('--skip-on-missing-config') args.append('--skip-on-missing-config')
params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args}
with open(hook_path, 'w') as hook_file: with open(hook_path, 'w') as hook_file:
contents = resource_text('hook-tmpl') contents = resource_text('hook-tmpl')
before, rest = contents.split(TEMPLATE_START) before, rest = contents.split(TEMPLATE_START)
_, after = rest.split(TEMPLATE_END) to_template, after = rest.split(TEMPLATE_END)
# on windows always use `/bin/sh` since `bash` might not be on PATH before = before.replace('#!/usr/bin/env python3', shebang())
# though we use bash-specific features `sh` on windows is actually
# bash in "POSIXLY_CORRECT" mode which still supports the features we
# use: subshells / arrays
if sys.platform == 'win32': # pragma: win32 cover
hook_file.write('#!/bin/sh\n')
hook_file.write(before + TEMPLATE_START) hook_file.write(before + TEMPLATE_START)
hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n') for line in to_template.splitlines():
args_s = shlex.join(args) var = line.split()[0]
hook_file.write(f'ARGS=({args_s})\n') hook_file.write(f'{var} = {params[var]!r}\n')
hook_file.write(TEMPLATE_END + after) hook_file.write(TEMPLATE_END + after)
make_executable(hook_path) make_executable(hook_path)
@ -114,11 +122,11 @@ def _install_hook_script(
def install( def install(
config_file: str, config_file: str,
store: Store, store: Store,
hook_types: list[str] | None, hook_types: Sequence[str],
overwrite: bool = False, overwrite: bool = False,
hooks: bool = False, hooks: bool = False,
skip_on_missing_config: bool = False, skip_on_missing_config: bool = False,
git_dir: str | None = None, git_dir: Optional[str] = None,
) -> int: ) -> int:
if git_dir is None and git.has_core_hookpaths_set(): if git_dir is None and git.has_core_hookpaths_set():
logger.error( logger.error(
@ -127,7 +135,7 @@ def install(
) )
return 1 return 1
for hook_type in _hook_types(config_file, hook_types): for hook_type in hook_types:
_install_hook_script( _install_hook_script(
config_file, hook_type, config_file, hook_type,
overwrite=overwrite, overwrite=overwrite,
@ -161,7 +169,7 @@ def _uninstall_hook_script(hook_type: str) -> None:
output.write_line(f'Restored previous hooks to {hook_path}') output.write_line(f'Restored previous hooks to {hook_path}')
def uninstall(config_file: str, hook_types: list[str] | None) -> int: def uninstall(hook_types: Sequence[str]) -> int:
for hook_type in _hook_types(config_file, hook_types): for hook_type in hook_types:
_uninstall_hook_script(hook_type) _uninstall_hook_script(hook_type)
return 0 return 0

View file

@ -1,21 +1,9 @@
from __future__ import annotations import re
import functools
import itertools
import textwrap import textwrap
from collections.abc import Callable
import cfgv
import yaml import yaml
from yaml.nodes import ScalarNode
from pre_commit.clientlib import InvalidConfigError from pre_commit.util import yaml_load
from pre_commit.yaml import yaml_compose
from pre_commit.yaml import yaml_load
from pre_commit.yaml_rewrite import MappingKey
from pre_commit.yaml_rewrite import MappingValue
from pre_commit.yaml_rewrite import match
from pre_commit.yaml_rewrite import SequenceItem
def _is_header_line(line: str) -> bool: def _is_header_line(line: str) -> bool:
@ -46,84 +34,16 @@ def _migrate_map(contents: str) -> str:
return contents return contents
def _preserve_style(n: ScalarNode, *, s: str) -> str: def _migrate_sha_to_rev(contents: str) -> str:
style = n.style or '' return re.sub(r'(\n\s+)sha:', r'\1rev:', contents)
return f'{style}{s}{style}'
def _fix_stage(n: ScalarNode) -> str:
return _preserve_style(n, s=f'pre-{n.value}')
def _migrate_composed(contents: str) -> str:
tree = yaml_compose(contents)
rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = []
# sha -> rev
sha_to_rev_replace = functools.partial(_preserve_style, s='rev')
sha_to_rev_matcher = (
MappingValue('repos'),
SequenceItem(),
MappingKey('sha'),
)
for node in match(tree, sha_to_rev_matcher):
rewrites.append((node, sha_to_rev_replace))
# python_venv -> python
language_matcher = (
MappingValue('repos'),
SequenceItem(),
MappingValue('hooks'),
SequenceItem(),
MappingValue('language'),
)
python_venv_replace = functools.partial(_preserve_style, s='python')
for node in match(tree, language_matcher):
if node.value == 'python_venv':
rewrites.append((node, python_venv_replace))
# stages rewrites
default_stages_matcher = (MappingValue('default_stages'), SequenceItem())
default_stages_match = match(tree, default_stages_matcher)
hook_stages_matcher = (
MappingValue('repos'),
SequenceItem(),
MappingValue('hooks'),
SequenceItem(),
MappingValue('stages'),
SequenceItem(),
)
hook_stages_match = match(tree, hook_stages_matcher)
for node in itertools.chain(default_stages_match, hook_stages_match):
if node.value in {'commit', 'push', 'merge-commit'}:
rewrites.append((node, _fix_stage))
rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index)
src_parts = []
end: int | None = None
for node, func in rewrites:
src_parts.append(contents[node.end_mark.index:end])
src_parts.append(func(node))
end = node.start_mark.index
src_parts.append(contents[:end])
src_parts.reverse()
return ''.join(src_parts)
def migrate_config(config_file: str, quiet: bool = False) -> int: def migrate_config(config_file: str, quiet: bool = False) -> int:
with open(config_file) as f: with open(config_file) as f:
orig_contents = contents = f.read() orig_contents = contents = f.read()
with cfgv.reraise_as(InvalidConfigError):
with cfgv.validate_context(f'File {config_file}'):
try:
yaml_load(orig_contents)
except Exception as e:
raise cfgv.ValidationError(str(e))
contents = _migrate_map(contents) contents = _migrate_map(contents)
contents = _migrate_composed(contents) contents = _migrate_sha_to_rev(contents)
if contents != orig_contents: if contents != orig_contents:
with open(config_file, 'w') as f: with open(config_file, 'w') as f:

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import argparse import argparse
import contextlib import contextlib
import functools import functools
@ -9,20 +7,23 @@ import re
import subprocess import subprocess
import time import time
import unicodedata import unicodedata
from collections.abc import Generator
from collections.abc import Iterable
from collections.abc import MutableMapping
from collections.abc import Sequence
from typing import Any from typing import Any
from typing import Collection
from typing import Dict
from typing import List
from typing import MutableMapping
from typing import Sequence
from typing import Set
from typing import Tuple
from identify.identify import tags_from_path from identify.identify import tags_from_path
from pre_commit import color from pre_commit import color
from pre_commit import git from pre_commit import git
from pre_commit import output from pre_commit import output
from pre_commit.all_languages import languages
from pre_commit.clientlib import load_config from pre_commit.clientlib import load_config
from pre_commit.hook import Hook from pre_commit.hook import Hook
from pre_commit.languages.all import languages
from pre_commit.repository import all_hooks from pre_commit.repository import all_hooks
from pre_commit.repository import install_hook_envs from pre_commit.repository import install_hook_envs
from pre_commit.staged_files_only import staged_files_only from pre_commit.staged_files_only import staged_files_only
@ -58,36 +59,37 @@ def _full_msg(
def filter_by_include_exclude( def filter_by_include_exclude(
names: Iterable[str], names: Collection[str],
include: str, include: str,
exclude: str, exclude: str,
) -> Generator[str]: ) -> List[str]:
include_re, exclude_re = re.compile(include), re.compile(exclude) include_re, exclude_re = re.compile(include), re.compile(exclude)
return ( return [
filename for filename in names filename for filename in names
if include_re.search(filename) if include_re.search(filename)
if not exclude_re.search(filename) if not exclude_re.search(filename)
) ]
class Classifier: class Classifier:
def __init__(self, filenames: Iterable[str]) -> None: def __init__(self, filenames: Collection[str]) -> None:
self.filenames = [f for f in filenames if os.path.lexists(f)] self.filenames = [f for f in filenames if os.path.lexists(f)]
@functools.cache @functools.lru_cache(maxsize=None)
def _types_for_file(self, filename: str) -> set[str]: def _types_for_file(self, filename: str) -> Set[str]:
return tags_from_path(filename) return tags_from_path(filename)
def by_types( def by_types(
self, self,
names: Iterable[str], names: Sequence[str],
types: Iterable[str], types: Collection[str],
types_or: Iterable[str], types_or: Collection[str],
exclude_types: Iterable[str], exclude_types: Collection[str],
) -> Generator[str]: ) -> List[str]:
types = frozenset(types) types = frozenset(types)
types_or = frozenset(types_or) types_or = frozenset(types_or)
exclude_types = frozenset(exclude_types) exclude_types = frozenset(exclude_types)
ret = []
for filename in names: for filename in names:
tags = self._types_for_file(filename) tags = self._types_for_file(filename)
if ( if (
@ -95,38 +97,38 @@ class Classifier:
(not types_or or tags & types_or) and (not types_or or tags & types_or) and
not tags & exclude_types not tags & exclude_types
): ):
yield filename ret.append(filename)
return ret
def filenames_for_hook(self, hook: Hook) -> Generator[str]: def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]:
return self.by_types( names = self.filenames
filter_by_include_exclude( names = filter_by_include_exclude(names, hook.files, hook.exclude)
self.filenames, names = self.by_types(
hook.files, names,
hook.exclude,
),
hook.types, hook.types,
hook.types_or, hook.types_or,
hook.exclude_types, hook.exclude_types,
) )
return tuple(names)
@classmethod @classmethod
def from_config( def from_config(
cls, cls,
filenames: Iterable[str], filenames: Collection[str],
include: str, include: str,
exclude: str, exclude: str,
) -> Classifier: ) -> 'Classifier':
# on windows we normalize all filenames to use forward slashes # on windows we normalize all filenames to use forward slashes
# this makes it easier to filter using the `files:` regex # this makes it easier to filter using the `files:` regex
# this also makes improperly quoted shell-based hooks work better # this also makes improperly quoted shell-based hooks work better
# see #1173 # see #1173
if os.altsep == '/' and os.sep == '\\': if os.altsep == '/' and os.sep == '\\':
filenames = (f.replace(os.sep, os.altsep) for f in filenames) filenames = [f.replace(os.sep, os.altsep) for f in filenames]
filenames = filter_by_include_exclude(filenames, include, exclude) filenames = filter_by_include_exclude(filenames, include, exclude)
return Classifier(filenames) return Classifier(filenames)
def _get_skips(environ: MutableMapping[str, str]) -> set[str]: def _get_skips(environ: MutableMapping[str, str]) -> Set[str]:
skips = environ.get('SKIP', '') skips = environ.get('SKIP', '')
return {skip.strip() for skip in skips.split(',') if skip.strip()} return {skip.strip() for skip in skips.split(',') if skip.strip()}
@ -142,13 +144,13 @@ def _subtle_line(s: str, use_color: bool) -> None:
def _run_single_hook( def _run_single_hook(
classifier: Classifier, classifier: Classifier,
hook: Hook, hook: Hook,
skips: set[str], skips: Set[str],
cols: int, cols: int,
diff_before: bytes, diff_before: bytes,
verbose: bool, verbose: bool,
use_color: bool, use_color: bool,
) -> tuple[bool, bytes]: ) -> Tuple[bool, bytes]:
filenames = tuple(classifier.filenames_for_hook(hook)) filenames = classifier.filenames_for_hook(hook)
if hook.id in skips or hook.alias in skips: if hook.id in skips or hook.alias in skips:
output.write( output.write(
@ -187,19 +189,10 @@ def _run_single_hook(
if not hook.pass_filenames: if not hook.pass_filenames:
filenames = () filenames = ()
time_before = time.monotonic() time_before = time.time()
language = languages[hook.language] language = languages[hook.language]
with language.in_env(hook.prefix, hook.language_version): retcode, out = language.run_hook(hook, filenames, use_color)
retcode, out = language.run_hook( duration = round(time.time() - time_before, 2) or 0
hook.prefix,
hook.entry,
hook.args,
filenames,
is_local=hook.src == 'local',
require_serial=hook.require_serial,
color=use_color,
)
duration = round(time.monotonic() - time_before, 2) or 0
diff_after = _get_diff() diff_after = _get_diff()
# if the hook makes changes, fail the commit # if the hook makes changes, fail the commit
@ -250,11 +243,10 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
return max(cols, 80) return max(cols, 80)
def _all_filenames(args: argparse.Namespace) -> Iterable[str]: def _all_filenames(args: argparse.Namespace) -> Collection[str]:
# these hooks do not operate on files # these hooks do not operate on files
if args.hook_stage in { if args.hook_stage in {
'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite',
'pre-rebase',
}: }:
return () return ()
elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}:
@ -273,17 +265,17 @@ def _all_filenames(args: argparse.Namespace) -> Iterable[str]:
def _get_diff() -> bytes: def _get_diff() -> bytes:
_, out, _ = cmd_output_b( _, out, _ = cmd_output_b(
'git', 'diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules', 'git', 'diff', '--no-ext-diff', '--ignore-submodules', retcode=None,
check=False,
) )
return out return out
def _run_hooks( def _run_hooks(
config: dict[str, Any], config: Dict[str, Any],
hooks: Sequence[Hook], hooks: Sequence[Hook],
skips: set[str], skips: Set[str],
args: argparse.Namespace, args: argparse.Namespace,
environ: MutableMapping[str, str],
) -> int: ) -> int:
"""Actually run the hooks.""" """Actually run the hooks."""
cols = _compute_cols(hooks) cols = _compute_cols(hooks)
@ -298,8 +290,7 @@ def _run_hooks(
verbose=args.verbose, use_color=args.color, verbose=args.verbose, use_color=args.color,
) )
retval |= current_retval retval |= current_retval
fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast) if retval and config['fail_fast']:
if current_retval and fail_fast:
break break
if retval and args.show_diff_on_failure and prior_diff: if retval and args.show_diff_on_failure and prior_diff:
if args.all_files: if args.all_files:
@ -329,7 +320,8 @@ def _has_unmerged_paths() -> bool:
def _has_unstaged_config(config_file: str) -> bool: def _has_unstaged_config(config_file: str) -> bool:
retcode, _, _ = cmd_output_b( retcode, _, _ = cmd_output_b(
'git', 'diff', '--quiet', '--no-ext-diff', config_file, check=False, 'git', 'diff', '--no-ext-diff', '--exit-code', config_file,
retcode=None,
) )
# be explicit, other git errors don't mean it has an unstaged config. # be explicit, other git errors don't mean it has an unstaged config.
return retcode == 1 return retcode == 1
@ -344,7 +336,7 @@ def run(
stash = not args.all_files and not args.files stash = not args.all_files and not args.files
# Check if we have unresolved merge conflict files and fail fast. # Check if we have unresolved merge conflict files and fail fast.
if stash and _has_unmerged_paths(): if _has_unmerged_paths():
logger.error('Unmerged files. Resolve before committing.') logger.error('Unmerged files. Resolve before committing.')
return 1 return 1
if bool(args.from_ref) != bool(args.to_ref): if bool(args.from_ref) != bool(args.to_ref):
@ -372,16 +364,6 @@ def run(
): ):
return 0 return 0
# Expose prepare_commit_message_source / commit_object_name
# as environment variables for the hooks
if args.prepare_commit_message_source:
environ['PRE_COMMIT_COMMIT_MSG_SOURCE'] = (
args.prepare_commit_message_source
)
if args.commit_object_name:
environ['PRE_COMMIT_COMMIT_OBJECT_NAME'] = args.commit_object_name
# Expose from-ref / to-ref as environment variables for hooks to consume # Expose from-ref / to-ref as environment variables for hooks to consume
if args.from_ref and args.to_ref: if args.from_ref and args.to_ref:
# legacy names # legacy names
@ -391,10 +373,6 @@ def run(
environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_FROM_REF'] = args.from_ref
environ['PRE_COMMIT_TO_REF'] = args.to_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref
if args.pre_rebase_upstream and args.pre_rebase_branch:
environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] = args.pre_rebase_upstream
environ['PRE_COMMIT_PRE_REBASE_BRANCH'] = args.pre_rebase_branch
if ( if (
args.remote_name and args.remote_url and args.remote_name and args.remote_url and
args.remote_branch and args.local_branch args.remote_branch and args.local_branch
@ -435,14 +413,10 @@ def run(
return 1 return 1
skips = _get_skips(environ) skips = _get_skips(environ)
to_install = [ to_install = [hook for hook in hooks if hook.id not in skips]
hook
for hook in hooks
if hook.id not in skips and hook.alias not in skips
]
install_hook_envs(to_install, store) install_hook_envs(to_install, store)
return _run_hooks(config, hooks, skips, args) return _run_hooks(config, hooks, skips, args, environ)
# https://github.com/python/mypy/issues/7726 # https://github.com/python/mypy/issues/7726
raise AssertionError('unreachable') raise AssertionError('unreachable')

View file

@ -1,4 +1,7 @@
from __future__ import annotations # TODO: maybe `git ls-remote git://github.com/pre-commit/pre-commit-hooks` to
# determine the latest revision? This adds ~200ms from my tests (and is
# significantly faster than https:// or http://). For now, periodically
# manually updating the revision is fine.
SAMPLE_CONFIG = '''\ SAMPLE_CONFIG = '''\
# See https://pre-commit.com for more information # See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks

View file

@ -1,9 +1,8 @@
from __future__ import annotations
import argparse import argparse
import logging import logging
import os.path import os.path
import tempfile from typing import Optional
from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import git from pre_commit import git
@ -12,13 +11,14 @@ from pre_commit.clientlib import load_manifest
from pre_commit.commands.run import run from pre_commit.commands.run import run
from pre_commit.store import Store from pre_commit.store import Store
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.util import tmpdir
from pre_commit.util import yaml_dump
from pre_commit.xargs import xargs from pre_commit.xargs import xargs
from pre_commit.yaml import yaml_dump
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]: def _repo_ref(tmpdir: str, repo: str, ref: Optional[str]) -> Tuple[str, str]:
# if `ref` is explicitly passed, use it # if `ref` is explicitly passed, use it
if ref is not None: if ref is not None:
return repo, ref return repo, ref
@ -49,7 +49,7 @@ def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]:
def try_repo(args: argparse.Namespace) -> int: def try_repo(args: argparse.Namespace) -> int:
with tempfile.TemporaryDirectory() as tempdir: with tmpdir() as tempdir:
repo, ref = _repo_ref(tempdir, args.repo, args.ref) repo, ref = _repo_ref(tempdir, args.repo, args.ref)
store = Store(tempdir) store = Store(tempdir)

View file

@ -1,18 +0,0 @@
from __future__ import annotations
from collections.abc import Sequence
from pre_commit import clientlib
def validate_config(filenames: Sequence[str]) -> int:
ret = 0
for filename in filenames:
try:
clientlib.load_config(filename)
except clientlib.InvalidConfigError as e:
print(e)
ret = 1
return ret

View file

@ -1,18 +0,0 @@
from __future__ import annotations
from collections.abc import Sequence
from pre_commit import clientlib
def validate_manifest(filenames: Sequence[str]) -> int:
ret = 0
for filename in filenames:
try:
clientlib.load_manifest(filename)
except clientlib.InvalidManifestError as e:
print(e)
ret = 1
return ret

View file

@ -1,13 +1,25 @@
from __future__ import annotations import sys
import importlib.metadata if sys.version_info < (3, 8): # pragma: no cover (<PY38)
import importlib_metadata
else: # pragma: no cover (PY38+)
import importlib.metadata as importlib_metadata
CONFIG_FILE = '.pre-commit-config.yaml' CONFIG_FILE = '.pre-commit-config.yaml'
MANIFEST_FILE = '.pre-commit-hooks.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml'
# Bump when installation changes in a backwards / forwards incompatible way
INSTALLED_STATE_VERSION = '1'
# Bump when modifying `empty_template` # Bump when modifying `empty_template`
LOCAL_REPO_VERSION = '1' LOCAL_REPO_VERSION = '1'
VERSION = importlib.metadata.version('pre_commit') VERSION = importlib_metadata.version('pre_commit')
# `manual` is not invoked by any installed git hook. See #719
STAGES = (
'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
'post-commit', 'manual', 'post-checkout', 'push', 'post-merge',
'post-rewrite',
)
DEFAULT = 'default' DEFAULT = 'default'

View file

@ -1,11 +1,11 @@
from __future__ import annotations
import contextlib import contextlib
import enum import enum
import os import os
from collections.abc import Generator from typing import Generator
from collections.abc import MutableMapping from typing import MutableMapping
from typing import NamedTuple from typing import NamedTuple
from typing import Optional
from typing import Tuple
from typing import Union from typing import Union
_Unset = enum.Enum('_Unset', 'UNSET') _Unset = enum.Enum('_Unset', 'UNSET')
@ -17,9 +17,9 @@ class Var(NamedTuple):
default: str = '' default: str = ''
SubstitutionT = tuple[Union[str, Var], ...] SubstitutionT = Tuple[Union[str, Var], ...]
ValueT = Union[str, _Unset, SubstitutionT] ValueT = Union[str, _Unset, SubstitutionT]
PatchesT = tuple[tuple[str, ValueT], ...] PatchesT = Tuple[Tuple[str, ValueT], ...]
def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
@ -32,8 +32,8 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
@contextlib.contextmanager @contextlib.contextmanager
def envcontext( def envcontext(
patch: PatchesT, patch: PatchesT,
_env: MutableMapping[str, str] | None = None, _env: Optional[MutableMapping[str, str]] = None,
) -> Generator[None]: ) -> Generator[None, None, None]:
"""In this context, `os.environ` is modified according to `patch`. """In this context, `os.environ` is modified according to `patch`.
`patch` is an iterable of 2-tuples (key, value): `patch` is an iterable of 2-tuples (key, value):

View file

@ -1,18 +1,14 @@
from __future__ import annotations
import contextlib import contextlib
import functools import functools
import os.path import os.path
import sys import sys
import traceback import traceback
from collections.abc import Generator from typing import Generator
from typing import IO
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import output from pre_commit import output
from pre_commit.errors import FatalError from pre_commit.errors import FatalError
from pre_commit.store import Store from pre_commit.store import Store
from pre_commit.util import cmd_output_b
from pre_commit.util import force_bytes from pre_commit.util import force_bytes
@ -25,15 +21,12 @@ def _log_and_exit(
error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc)
output.write_line_b(error_msg) output.write_line_b(error_msg)
_, git_version_b, _ = cmd_output_b('git', '--version', check=False)
git_version = git_version_b.decode(errors='backslashreplace').rstrip()
storedir = Store().directory storedir = Store().directory
log_path = os.path.join(storedir, 'pre-commit.log') log_path = os.path.join(storedir, 'pre-commit.log')
with contextlib.ExitStack() as ctx: with contextlib.ExitStack() as ctx:
if os.access(storedir, os.W_OK): if os.access(storedir, os.W_OK):
output.write_line(f'Check the log at {log_path}') output.write_line(f'Check the log at {log_path}')
log: IO[bytes] = ctx.enter_context(open(log_path, 'wb')) log = ctx.enter_context(open(log_path, 'wb'))
else: # pragma: win32 no cover else: # pragma: win32 no cover
output.write_line(f'Failed to write to log at {log_path}') output.write_line(f'Failed to write to log at {log_path}')
log = sys.stdout.buffer log = sys.stdout.buffer
@ -45,7 +38,6 @@ def _log_and_exit(
_log_line() _log_line()
_log_line('```') _log_line('```')
_log_line(f'pre-commit version: {C.VERSION}') _log_line(f'pre-commit version: {C.VERSION}')
_log_line(f'git --version: {git_version}')
_log_line('sys.version:') _log_line('sys.version:')
for line in sys.version.splitlines(): for line in sys.version.splitlines():
_log_line(f' {line}') _log_line(f' {line}')
@ -68,7 +60,7 @@ def _log_and_exit(
@contextlib.contextmanager @contextlib.contextmanager
def error_handler() -> Generator[None]: def error_handler() -> Generator[None, None, None]:
try: try:
yield yield
except (Exception, KeyboardInterrupt) as e: except (Exception, KeyboardInterrupt) as e:

View file

@ -1,5 +1,2 @@
from __future__ import annotations
class FatalError(RuntimeError): class FatalError(RuntimeError):
pass pass

View file

@ -1,10 +1,8 @@
from __future__ import annotations
import contextlib import contextlib
import errno import errno
import sys import sys
from collections.abc import Callable from typing import Callable
from collections.abc import Generator from typing import Generator
if sys.platform == 'win32': # pragma: no cover (windows) if sys.platform == 'win32': # pragma: no cover (windows)
@ -20,13 +18,15 @@ if sys.platform == 'win32': # pragma: no cover (windows)
def _locked( def _locked(
fileno: int, fileno: int,
blocked_cb: Callable[[], None], blocked_cb: Callable[[], None],
) -> Generator[None]: ) -> Generator[None, None, None]:
try: try:
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
except OSError: except OSError:
blocked_cb() blocked_cb()
while True: while True:
try: try:
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) msvcrt.locking(fileno, msvcrt.LK_LOCK, _region)
except OSError as e: except OSError as e:
# Locking violation. Returned when the _LK_LOCK or _LK_RLCK # Locking violation. Returned when the _LK_LOCK or _LK_RLCK
@ -45,6 +45,7 @@ if sys.platform == 'win32': # pragma: no cover (windows)
# The documentation however states: # The documentation however states:
# "Regions should be locked only briefly and should be unlocked # "Regions should be locked only briefly and should be unlocked
# before closing a file or exiting the program." # before closing a file or exiting the program."
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region)
else: # pragma: win32 no cover else: # pragma: win32 no cover
import fcntl import fcntl
@ -53,7 +54,7 @@ else: # pragma: win32 no cover
def _locked( def _locked(
fileno: int, fileno: int,
blocked_cb: Callable[[], None], blocked_cb: Callable[[], None],
) -> Generator[None]: ) -> Generator[None, None, None]:
try: try:
fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError: # pragma: no cover (tests are single-threaded) except OSError: # pragma: no cover (tests are single-threaded)
@ -69,7 +70,7 @@ else: # pragma: win32 no cover
def lock( def lock(
path: str, path: str,
blocked_cb: Callable[[], None], blocked_cb: Callable[[], None],
) -> Generator[None]: ) -> Generator[None, None, None]:
with open(path, 'a+') as f: with open(path, 'a+') as f:
with _locked(f.fileno(), blocked_cb): with _locked(f.fileno(), blocked_cb):
yield yield

View file

@ -1,22 +1,22 @@
from __future__ import annotations
import logging import logging
import os.path import os.path
import sys import sys
from collections.abc import Mapping from typing import Dict
from typing import List
from typing import MutableMapping
from typing import Optional
from typing import Set
from pre_commit.errors import FatalError from pre_commit.errors import FatalError
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# see #2046
NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false')
def zsplit(s: str) -> List[str]:
def zsplit(s: str) -> list[str]:
s = s.strip('\0') s = s.strip('\0')
if s: if s:
return s.split('\0') return s.split('\0')
@ -24,7 +24,9 @@ def zsplit(s: str) -> list[str]:
return [] return []
def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]: def no_git_env(
_env: Optional[MutableMapping[str, str]] = None,
) -> Dict[str, str]:
# Too many bugs dealing with environment variables and GIT: # Too many bugs dealing with environment variables and GIT:
# https://github.com/pre-commit/pre-commit/issues/300 # https://github.com/pre-commit/pre-commit/issues/300
# In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running
@ -37,13 +39,9 @@ def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]:
return { return {
k: v for k, v in _env.items() k: v for k, v in _env.items()
if not k.startswith('GIT_') or if not k.startswith('GIT_') or
k.startswith(('GIT_CONFIG_KEY_', 'GIT_CONFIG_VALUE_')) or
k in { k in {
'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO',
'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', 'GIT_SSL_NO_VERIFY',
'GIT_HTTP_PROXY_AUTHMETHOD',
'GIT_ALLOW_PROTOCOL',
'GIT_ASKPASS',
} }
} }
@ -57,15 +55,13 @@ def get_root() -> str:
root = os.path.abspath( root = os.path.abspath(
cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(),
) )
inside_git_dir = cmd_output( git_dir = os.path.abspath(get_git_dir())
'git', 'rev-parse', '--is-inside-git-dir',
)[1].strip()
except CalledProcessError: except CalledProcessError:
raise FatalError( raise FatalError(
'git failed. Is it installed, and are you in a Git repository ' 'git failed. Is it installed, and are you in a Git repository '
'directory?', 'directory?',
) )
if inside_git_dir != 'false': if os.path.samefile(root, git_dir):
raise FatalError( raise FatalError(
'git toplevel unexpectedly empty! make sure you are not ' 'git toplevel unexpectedly empty! make sure you are not '
'inside the `.git` directory of your repository.', 'inside the `.git` directory of your repository.',
@ -74,23 +70,18 @@ def get_root() -> str:
def get_git_dir(git_root: str = '.') -> str: def get_git_dir(git_root: str = '.') -> str:
opt = '--git-dir' opts = ('--git-common-dir', '--git-dir')
_, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root) _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root)
git_dir = out.strip() for line, opt in zip(out.splitlines(), opts):
if git_dir != opt: if line != opt: # pragma: no branch (git < 2.5)
return os.path.normpath(os.path.join(git_root, git_dir)) return os.path.normpath(os.path.join(git_root, line))
else: else:
raise AssertionError('unreachable: no git dir') raise AssertionError('unreachable: no git dir')
def get_git_common_dir(git_root: str = '.') -> str: def get_remote_url(git_root: str) -> str:
opt = '--git-common-dir' _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)
_, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root) return out.strip()
git_common_dir = out.strip()
if git_common_dir != opt:
return os.path.normpath(os.path.join(git_root, git_common_dir))
else: # pragma: no cover (git < 2.5)
return get_git_dir(git_root)
def is_in_merge_conflict() -> bool: def is_in_merge_conflict() -> bool:
@ -101,7 +92,7 @@ def is_in_merge_conflict() -> bool:
) )
def parse_merge_msg_for_conflicts(merge_msg: bytes) -> list[str]: def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]:
# Conflicted files start with tabs # Conflicted files start with tabs
return [ return [
line.lstrip(b'#').strip().decode() line.lstrip(b'#').strip().decode()
@ -111,7 +102,7 @@ def parse_merge_msg_for_conflicts(merge_msg: bytes) -> list[str]:
] ]
def get_conflicted_files() -> set[str]: def get_conflicted_files() -> Set[str]:
logger.info('Checking merge-conflict files only.') logger.info('Checking merge-conflict files only.')
# Need to get the conflicted files from the MERGE_MSG because they could # Need to get the conflicted files from the MERGE_MSG because they could
# have resolved the conflict by choosing one side or the other # have resolved the conflict by choosing one side or the other
@ -126,13 +117,13 @@ def get_conflicted_files() -> set[str]:
merge_diff_filenames = zsplit( merge_diff_filenames = zsplit(
cmd_output( cmd_output(
'git', 'diff', '--name-only', '--no-ext-diff', '-z', 'git', 'diff', '--name-only', '--no-ext-diff', '-z',
'-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--', '-m', tree_hash, 'HEAD', 'MERGE_HEAD',
)[1], )[1],
) )
return set(merge_conflict_filenames) | set(merge_diff_filenames) return set(merge_conflict_filenames) | set(merge_diff_filenames)
def get_staged_files(cwd: str | None = None) -> list[str]: def get_staged_files(cwd: Optional[str] = None) -> List[str]:
return zsplit( return zsplit(
cmd_output( cmd_output(
'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z',
@ -143,19 +134,27 @@ def get_staged_files(cwd: str | None = None) -> list[str]:
) )
def intent_to_add_files() -> list[str]: def intent_to_add_files() -> List[str]:
_, stdout, _ = cmd_output( _, stdout, _ = cmd_output(
'git', 'diff', '--no-ext-diff', '--ignore-submodules', 'git', 'status', '--ignore-submodules', '--porcelain', '-z',
'--diff-filter=A', '--name-only', '-z',
) )
return zsplit(stdout) parts = list(reversed(zsplit(stdout)))
intent_to_add = []
while parts:
line = parts.pop()
status, filename = line[:3], line[3:]
if status[0] in {'C', 'R'}: # renames / moves have an additional arg
parts.pop()
if status[1] == 'A':
intent_to_add.append(filename)
return intent_to_add
def get_all_files() -> list[str]: def get_all_files() -> List[str]:
return zsplit(cmd_output('git', 'ls-files', '-z')[1]) return zsplit(cmd_output('git', 'ls-files', '-z')[1])
def get_changed_files(old: str, new: str) -> list[str]: def get_changed_files(old: str, new: str) -> List[str]:
diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z') diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z')
try: try:
_, out, _ = cmd_output(*diff_cmd, f'{old}...{new}') _, out, _ = cmd_output(*diff_cmd, f'{old}...{new}')
@ -174,11 +173,11 @@ def head_rev(remote: str) -> str:
def has_diff(*args: str, repo: str = '.') -> bool: def has_diff(*args: str, repo: str = '.') -> bool:
cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args) cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args)
return cmd_output_b(*cmd, cwd=repo, check=False)[0] == 1 return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1
def has_core_hookpaths_set() -> bool: def has_core_hookpaths_set() -> bool:
_, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', check=False) _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None)
return bool(out.strip()) return bool(out.strip())
@ -186,11 +185,10 @@ def init_repo(path: str, remote: str) -> None:
if os.path.isdir(remote): if os.path.isdir(remote):
remote = os.path.abspath(remote) remote = os.path.abspath(remote)
git = ('git', *NO_FS_MONITOR)
env = no_git_env() env = no_git_env()
# avoid the user's template so that hooks do not recurse # avoid the user's template so that hooks do not recurse
cmd_output_b(*git, 'init', '--template=', path, env=env) cmd_output_b('git', 'init', '--template=', path, env=env)
cmd_output_b(*git, 'remote', 'add', 'origin', remote, cwd=path, env=env) cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env)
def commit(repo: str = '.') -> None: def commit(repo: str = '.') -> None:
@ -219,7 +217,7 @@ def check_for_cygwin_mismatch() -> None:
if is_cygwin_python ^ is_cygwin_git: if is_cygwin_python ^ is_cygwin_git:
exe_type = {True: '(cygwin)', False: '(windows)'} exe_type = {True: '(cygwin)', False: '(windows)'}
logger.warning( logger.warn(
f'pre-commit has detected a mix of cygwin python / git\n' f'pre-commit has detected a mix of cygwin python / git\n'
f'This combination is not supported, it is likely you will ' f'This combination is not supported, it is likely you will '
f'receive an error later in the program.\n' f'receive an error later in the program.\n'
@ -228,18 +226,3 @@ def check_for_cygwin_mismatch() -> None:
f' - python {exe_type[is_cygwin_python]}\n' f' - python {exe_type[is_cygwin_python]}\n'
f' - git {exe_type[is_cygwin_git]}\n', f' - git {exe_type[is_cygwin_git]}\n',
) )
def get_best_candidate_tag(rev: str, git_repo: str) -> str:
"""Get the best tag candidate.
Multiple tags can exist on a SHA. Sometimes a moving tag is attached
to a version tag. Try to pick the tag that looks like a version.
"""
tags = cmd_output(
'git', *NO_FS_MONITOR, 'tag', '--points-at', rev, cwd=git_repo,
)[1].splitlines()
for tag in tags:
if '.' in tag:
return tag
return rev

View file

@ -1,9 +1,10 @@
from __future__ import annotations
import logging import logging
from collections.abc import Sequence import shlex
from typing import Any from typing import Any
from typing import Dict
from typing import NamedTuple from typing import NamedTuple
from typing import Sequence
from typing import Tuple
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
@ -26,7 +27,6 @@ class Hook(NamedTuple):
additional_dependencies: Sequence[str] additional_dependencies: Sequence[str]
args: Sequence[str] args: Sequence[str]
always_run: bool always_run: bool
fail_fast: bool
pass_filenames: bool pass_filenames: bool
description: str description: str
language_version: str language_version: str
@ -37,7 +37,11 @@ class Hook(NamedTuple):
verbose: bool verbose: bool
@property @property
def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]: def cmd(self) -> Tuple[str, ...]:
return (*shlex.split(self.entry), *self.args)
@property
def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]:
return ( return (
self.prefix, self.prefix,
self.language, self.language,
@ -46,7 +50,7 @@ class Hook(NamedTuple):
) )
@classmethod @classmethod
def create(cls, src: str, prefix: Prefix, dct: dict[str, Any]) -> Hook: def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook':
# TODO: have cfgv do this (?) # TODO: have cfgv do this (?)
extra_keys = set(dct) - _KEYS extra_keys = set(dct) - _KEYS
if extra_keys: if extra_keys:

View file

@ -1,196 +0,0 @@
from __future__ import annotations
import contextlib
import os
import random
import re
import shlex
import sys
from collections.abc import Generator
from collections.abc import Sequence
from typing import Any
from typing import ContextManager
from typing import NoReturn
from typing import Protocol
import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit import xargs
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output_b
FIXED_RANDOM_SEED = 1542676187
SHIMS_RE = re.compile(r'[/\\]shims[/\\]')
class Language(Protocol):
# Use `None` for no installation / environment
@property
def ENVIRONMENT_DIR(self) -> str | None: ...
# return a value to replace `'default` for `language_version`
def get_default_version(self) -> str: ...
# return whether the environment is healthy (or should be rebuilt)
def health_check(self, prefix: Prefix, version: str) -> str | None: ...
# install a repository for the given language and language_version
def install_environment(
self,
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
...
# modify the environment for hook execution
def in_env(self, prefix: Prefix, version: str) -> ContextManager[None]: ...
# execute a hook and return the exit code and output
def run_hook(
self,
prefix: Prefix,
entry: str,
args: Sequence[str],
file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool,
) -> tuple[int, bytes]:
...
def exe_exists(exe: str) -> bool:
found = parse_shebang.find_executable(exe)
if found is None: # exe exists
return False
homedir = os.path.expanduser('~')
try:
common: str | None = os.path.commonpath((found, homedir))
except ValueError: # on windows, different drives raises ValueError
common = None
return (
# it is not in a /shims/ directory
not SHIMS_RE.search(found) and
(
# the homedir is / (docker, service user, etc.)
os.path.dirname(homedir) == homedir or
# the exe is not contained in the home directory
common != homedir
)
)
def setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None:
cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs)
def environment_dir(prefix: Prefix, d: str, language_version: str) -> str:
return prefix.path(f'{d}-{language_version}')
def assert_version_default(binary: str, version: str) -> None:
if version != C.DEFAULT:
raise AssertionError(
f'for now, pre-commit requires system-installed {binary} -- '
f'you selected `language_version: {version}`',
)
def assert_no_additional_deps(
lang: str,
additional_deps: Sequence[str],
) -> None:
if additional_deps:
raise AssertionError(
f'for now, pre-commit does not support '
f'additional_dependencies for {lang} -- '
f'you selected `additional_dependencies: {additional_deps}`',
)
def basic_get_default_version() -> str:
return C.DEFAULT
def basic_health_check(prefix: Prefix, language_version: str) -> str | None:
return None
def no_install(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> NoReturn:
raise AssertionError('This language is not installable')
@contextlib.contextmanager
def no_env(prefix: Prefix, version: str) -> Generator[None]:
yield
def target_concurrency() -> int:
if 'PRE_COMMIT_NO_CONCURRENCY' in os.environ:
return 1
else:
# Travis appears to have a bunch of CPUs, but we can't use them all.
if 'TRAVIS' in os.environ:
return 2
else:
return xargs.cpu_count()
def _shuffled(seq: Sequence[str]) -> list[str]:
"""Deterministically shuffle"""
fixed_random = random.Random()
fixed_random.seed(FIXED_RANDOM_SEED, version=1)
seq = list(seq)
fixed_random.shuffle(seq)
return seq
def run_xargs(
cmd: tuple[str, ...],
file_args: Sequence[str],
*,
require_serial: bool,
color: bool,
) -> tuple[int, bytes]:
if require_serial:
jobs = 1
else:
# Shuffle the files so that they more evenly fill out the xargs
# partitions, but do it deterministically in case a hook cares about
# ordering.
file_args = _shuffled(file_args)
jobs = target_concurrency()
return xargs.xargs(cmd, file_args, target_concurrency=jobs, color=color)
def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]:
cmd = shlex.split(entry)
if cmd[:2] == ['pre-commit', 'hazmat']:
cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]]
return (*cmd, *args)
def basic_run_hook(
prefix: Prefix,
entry: str,
args: Sequence[str],
file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool,
) -> tuple[int, bytes]:
return run_xargs(
hook_cmd(entry, args),
file_args,
require_serial=require_serial,
color=color,
)

View file

@ -0,0 +1,68 @@
from typing import Callable
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Tuple
from pre_commit.hook import Hook
from pre_commit.languages import conda
from pre_commit.languages import coursier
from pre_commit.languages import dart
from pre_commit.languages import docker
from pre_commit.languages import docker_image
from pre_commit.languages import dotnet
from pre_commit.languages import fail
from pre_commit.languages import golang
from pre_commit.languages import node
from pre_commit.languages import perl
from pre_commit.languages import pygrep
from pre_commit.languages import python
from pre_commit.languages import r
from pre_commit.languages import ruby
from pre_commit.languages import rust
from pre_commit.languages import script
from pre_commit.languages import swift
from pre_commit.languages import system
from pre_commit.prefix import Prefix
class Language(NamedTuple):
name: str
# Use `None` for no installation / environment
ENVIRONMENT_DIR: Optional[str]
# return a value to replace `'default` for `language_version`
get_default_version: Callable[[], str]
# return whether the environment is healthy (or should be rebuilt)
healthy: Callable[[Prefix, str], bool]
# install a repository for the given language and language_version
install_environment: Callable[[Prefix, str, Sequence[str]], None]
# execute a hook and return the exit code and output
run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]'
# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018
languages = {
# BEGIN GENERATED (testing/gen-languages-all)
'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501
'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501
'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, healthy=dart.healthy, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501
'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501
'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501
'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501
'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501
'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501
'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501
'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501
'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501
'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501
'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, healthy=r.healthy, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501
'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501
'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501
'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501
'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501
'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501
# END GENERATED
}
# TODO: fully deprecate `python_venv`
languages['python_venv'] = languages['python']
all_languages = sorted(languages)

View file

@ -1,24 +1,23 @@
from __future__ import annotations
import contextlib import contextlib
import os import os
import sys from typing import Generator
from collections.abc import Generator from typing import Sequence
from collections.abc import Sequence from typing import Tuple
from pre_commit import lang_base
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import SubstitutionT
from pre_commit.envcontext import UNSET from pre_commit.envcontext import UNSET
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'conda' ENVIRONMENT_DIR = 'conda'
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
run_hook = lang_base.basic_run_hook
def get_env_patch(env: str) -> PatchesT: def get_env_patch(env: str) -> PatchesT:
@ -27,7 +26,7 @@ def get_env_patch(env: str) -> PatchesT:
# $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only
# seems to be used for python.exe. # seems to be used for python.exe.
path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
if sys.platform == 'win32': # pragma: win32 cover if os.name == 'nt': # pragma: no cover (platform specific)
path = (env, os.pathsep, *path) path = (env, os.pathsep, *path)
path = (os.path.join(env, 'Scripts'), os.pathsep, *path) path = (os.path.join(env, 'Scripts'), os.pathsep, *path)
path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path) path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path)
@ -41,37 +40,45 @@ def get_env_patch(env: str) -> PatchesT:
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) prefix: Prefix,
language_version: str,
) -> Generator[None, None, None]:
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
envdir = prefix.path(directory)
with envcontext(get_env_patch(envdir)): with envcontext(get_env_patch(envdir)):
yield yield
def _conda_exe() -> str:
if os.environ.get('PRE_COMMIT_USE_MICROMAMBA'):
return 'micromamba'
elif os.environ.get('PRE_COMMIT_USE_MAMBA'):
return 'mamba'
else:
return 'conda'
def install_environment( def install_environment(
prefix: Prefix, prefix: Prefix,
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
lang_base.assert_version_default('conda', version) helpers.assert_version_default('conda', version)
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
conda_exe = _conda_exe() env_dir = prefix.path(directory)
with clean_path_on_failure(env_dir):
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
cmd_output_b(
conda_exe, 'env', 'create', '-p', env_dir, '--file',
'environment.yml', cwd=prefix.prefix_dir,
)
if additional_dependencies:
cmd_output_b( cmd_output_b(
conda_exe, 'install', '-p', env_dir, *additional_dependencies, 'conda', 'env', 'create', '-p', env_dir, '--file',
cwd=prefix.prefix_dir, 'environment.yml', cwd=prefix.prefix_dir,
) )
if additional_dependencies:
cmd_output_b(
'conda', 'install', '-p', env_dir, *additional_dependencies,
cwd=prefix.prefix_dir,
)
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
# TODO: Some rare commands need to be run using `conda run` but mostly we
# can run them without which is much quicker and produces a better
# output.
# cmd = ('conda', 'run', '-p', env_dir) + hook.cmd
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,76 +1,71 @@
from __future__ import annotations
import contextlib import contextlib
import os.path import os
from collections.abc import Generator from typing import Generator
from collections.abc import Sequence from typing import Sequence
from typing import Tuple
from pre_commit import lang_base
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.errors import FatalError from pre_commit.hook import Hook
from pre_commit.parse_shebang import find_executable from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
ENVIRONMENT_DIR = 'coursier' ENVIRONMENT_DIR = 'coursier'
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
run_hook = lang_base.basic_run_hook
def install_environment( def install_environment(
prefix: Prefix, prefix: Prefix,
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None: # pragma: win32 no cover
lang_base.assert_version_default('coursier', version) helpers.assert_version_default('coursier', version)
helpers.assert_no_additional_deps('coursier', additional_dependencies)
# Support both possible executable names (either "cs" or "coursier") envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
cs = find_executable('cs') or find_executable('coursier') channel = prefix.path('.pre-commit-channel')
if cs is None: with clean_path_on_failure(envdir):
raise AssertionError( for app_descriptor in os.listdir(channel):
'pre-commit requires system-installed "cs" or "coursier" ' _, app_file = os.path.split(app_descriptor)
'executables in the application search path', app, _ = os.path.splitext(app_file)
) helpers.run_setup_cmd(
prefix,
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) (
'cs',
def _install(*opts: str) -> None: 'install',
assert cs is not None
lang_base.setup_cmd(prefix, (cs, 'fetch', *opts))
lang_base.setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts))
with in_env(prefix, version):
channel = prefix.path('.pre-commit-channel')
if os.path.isdir(channel):
for app_descriptor in os.listdir(channel):
_, app_file = os.path.split(app_descriptor)
app, _ = os.path.splitext(app_file)
_install(
'--default-channels=false', '--default-channels=false',
'--channel', channel, f'--channel={channel}',
app, app,
) f'--dir={envdir}',
elif not additional_dependencies: ),
raise FatalError(
'expected .pre-commit-channel dir or additional_dependencies',
) )
if additional_dependencies:
_install(*additional_dependencies)
def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover
def get_env_patch(target_dir: str) -> PatchesT:
return ( return (
('PATH', (target_dir, os.pathsep, Var('PATH'))), ('PATH', (target_dir, os.pathsep, Var('PATH'))),
('COURSIER_CACHE', os.path.join(target_dir, '.cs-cache')),
) )
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) prefix: Prefix,
with envcontext(get_env_patch(envdir)): ) -> Generator[None, None, None]: # pragma: win32 no cover
target_dir = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, get_default_version()),
)
with envcontext(get_env_patch(target_dir)):
yield yield
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]: # pragma: win32 no cover
with in_env(hook.prefix):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,25 +1,26 @@
from __future__ import annotations
import contextlib import contextlib
import os.path import os.path
import shutil import shutil
import tempfile import tempfile
from collections.abc import Generator from typing import Generator
from collections.abc import Sequence from typing import Sequence
from typing import Tuple
from pre_commit import lang_base import pre_commit.constants as C
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
from pre_commit.util import win_exe from pre_commit.util import win_exe
from pre_commit.yaml import yaml_load from pre_commit.util import yaml_load
ENVIRONMENT_DIR = 'dartenv' ENVIRONMENT_DIR = 'dartenv'
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
run_hook = lang_base.basic_run_hook
def get_env_patch(venv: str) -> PatchesT: def get_env_patch(venv: str) -> PatchesT:
@ -29,8 +30,9 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(prefix: Prefix) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT)
envdir = prefix.path(directory)
with envcontext(get_env_patch(envdir)): with envcontext(get_env_patch(envdir)):
yield yield
@ -40,9 +42,9 @@ def install_environment(
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
lang_base.assert_version_default('dart', version) helpers.assert_version_default('dart', version)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
bin_dir = os.path.join(envdir, 'bin') bin_dir = os.path.join(envdir, 'bin')
def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: def _install_dir(prefix_p: Prefix, pub_cache: str) -> None:
@ -51,10 +53,10 @@ def install_environment(
with open(prefix_p.path('pubspec.yaml')) as f: with open(prefix_p.path('pubspec.yaml')) as f:
pubspec_contents = yaml_load(f) pubspec_contents = yaml_load(f)
lang_base.setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env)
for executable in pubspec_contents['executables']: for executable in pubspec_contents['executables']:
lang_base.setup_cmd( helpers.run_setup_cmd(
prefix_p, prefix_p,
( (
'dart', 'compile', 'exe', 'dart', 'compile', 'exe',
@ -64,34 +66,44 @@ def install_environment(
env=dart_env, env=dart_env,
) )
os.makedirs(bin_dir) with clean_path_on_failure(envdir):
os.makedirs(bin_dir)
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
_install_dir(prefix, tmp) _install_dir(prefix, tmp)
for dep_s in additional_dependencies: for dep_s in additional_dependencies:
with tempfile.TemporaryDirectory() as dep_tmp: with tempfile.TemporaryDirectory() as dep_tmp:
dep, _, version = dep_s.partition(':') dep, _, version = dep_s.partition(':')
if version: if version:
dep_cmd: tuple[str, ...] = (dep, '--version', version) dep_cmd: Tuple[str, ...] = (dep, '--version', version)
else: else:
dep_cmd = (dep,) dep_cmd = (dep,)
lang_base.setup_cmd( helpers.run_setup_cmd(
prefix, prefix,
('dart', 'pub', 'cache', 'add', *dep_cmd), ('dart', 'pub', 'cache', 'add', *dep_cmd),
env={**os.environ, 'PUB_CACHE': dep_tmp}, env={**os.environ, 'PUB_CACHE': dep_tmp},
)
# try and find the 'pubspec.yaml' that just got added
for root, _, filenames in os.walk(dep_tmp):
if 'pubspec.yaml' in filenames:
with tempfile.TemporaryDirectory() as copied:
pkg = os.path.join(copied, 'pkg')
shutil.copytree(root, pkg)
_install_dir(Prefix(pkg), dep_tmp)
break
else:
raise AssertionError(
f'could not find pubspec.yaml for {dep_s}',
) )
# try and find the 'pubspec.yaml' that just got added
for root, _, filenames in os.walk(dep_tmp):
if 'pubspec.yaml' in filenames:
with tempfile.TemporaryDirectory() as copied:
pkg = os.path.join(copied, 'pkg')
shutil.copytree(root, pkg)
_install_dir(Prefix(pkg), dep_tmp)
break
else:
raise AssertionError(
f'could not find pubspec.yaml for {dep_s}',
)
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,52 +1,48 @@
from __future__ import annotations
import contextlib
import functools
import hashlib import hashlib
import json import json
import os import os
import re from typing import Sequence
from collections.abc import Sequence from typing import Tuple
from pre_commit import lang_base import pre_commit.constants as C
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'docker' ENVIRONMENT_DIR = 'docker'
PRE_COMMIT_LABEL = 'PRE_COMMIT' PRE_COMMIT_LABEL = 'PRE_COMMIT'
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
in_env = lang_base.no_env # no special environment for docker
_HOSTNAME_MOUNT_RE = re.compile(
rb"""
/containers
(?:/overlay-containers)?
/([a-z0-9]{64})
(?:/userdata)?
/hostname
""",
re.VERBOSE,
)
def _get_container_id() -> str | None: def _is_in_docker() -> bool:
with contextlib.suppress(FileNotFoundError): try:
with open('/proc/1/mountinfo', 'rb') as f: with open('/proc/1/cgroup', 'rb') as f:
for line in f: return b'docker' in f.read()
m = _HOSTNAME_MOUNT_RE.search(line) except FileNotFoundError:
if m: return False
return m[1].decode()
return None
def _get_container_id() -> str:
# It's assumed that we already check /proc/1/cgroup in _is_in_docker. The
# cpuset cgroup controller existed since cgroups were introduced so this
# way of getting the container ID is pretty reliable.
with open('/proc/1/cgroup', 'rb') as f:
for line in f.readlines():
if line.split(b':')[1] == b'cpuset':
return os.path.basename(line.split(b':')[2]).strip().decode()
raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.')
def _get_docker_path(path: str) -> str: def _get_docker_path(path: str) -> str:
container_id = _get_container_id() if not _is_in_docker():
if container_id is None:
return path return path
container_id = _get_container_id()
try: try:
_, out, _ = cmd_output_b('docker', 'inspect', container_id) _, out, _ = cmd_output_b('docker', 'inspect', container_id)
except CalledProcessError: except CalledProcessError:
@ -80,7 +76,7 @@ def build_docker_image(
*, *,
pull: bool, pull: bool,
) -> None: # pragma: win32 no cover ) -> None: # pragma: win32 no cover
cmd: tuple[str, ...] = ( cmd: Tuple[str, ...] = (
'docker', 'build', 'docker', 'build',
'--tag', docker_tag(prefix), '--tag', docker_tag(prefix),
'--label', PRE_COMMIT_LABEL, '--label', PRE_COMMIT_LABEL,
@ -89,64 +85,37 @@ def build_docker_image(
cmd += ('--pull',) cmd += ('--pull',)
# This must come last for old versions of docker. See #477 # This must come last for old versions of docker. See #477
cmd += ('.',) cmd += ('.',)
lang_base.setup_cmd(prefix, cmd) helpers.run_setup_cmd(prefix, cmd)
def install_environment( def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str], prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None: # pragma: win32 no cover ) -> None: # pragma: win32 no cover
lang_base.assert_version_default('docker', version) helpers.assert_version_default('docker', version)
lang_base.assert_no_additional_deps('docker', additional_dependencies) helpers.assert_no_additional_deps('docker', additional_dependencies)
directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) directory = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
# Docker doesn't really have relevant disk environment, but pre-commit # Docker doesn't really have relevant disk environment, but pre-commit
# still needs to cleanup its state files on failure # still needs to cleanup its state files on failure
build_docker_image(prefix, pull=True) with clean_path_on_failure(directory):
os.mkdir(directory) build_docker_image(prefix, pull=True)
os.mkdir(directory)
@functools.lru_cache(maxsize=1) def get_docker_user() -> Tuple[str, ...]: # pragma: win32 no cover
def _is_rootless() -> bool: # pragma: win32 no cover
retcode, out, _ = cmd_output_b(
'docker', 'system', 'info', '--format', '{{ json . }}',
)
if retcode != 0:
return False
info = json.loads(out)
try:
return (
# docker:
# https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo
'name=rootless' in (info.get('SecurityOptions') or ()) or
# podman:
# https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod
info['host']['security']['rootless']
)
except KeyError:
return False
def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover
if _is_rootless():
return ()
try: try:
return ('-u', f'{os.getuid()}:{os.getgid()}') return ('-u', f'{os.getuid()}:{os.getgid()}')
except AttributeError: except AttributeError:
return () return ()
def get_docker_tty(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover # noqa: E501 def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover
return (('--tty',) if color else ())
def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover
return ( return (
'docker', 'run', 'docker', 'run',
'--rm', '--rm',
*get_docker_tty(color=color),
*get_docker_user(), *get_docker_user(),
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
# The `Z` option tells Docker to label the content with a private # The `Z` option tells Docker to label the content with a private
@ -157,25 +126,16 @@ def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover
def run_hook( def run_hook(
prefix: Prefix, hook: Hook,
entry: str,
args: Sequence[str],
file_args: Sequence[str], file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool, color: bool,
) -> tuple[int, bytes]: # pragma: win32 no cover ) -> Tuple[int, bytes]: # pragma: win32 no cover
# Rebuild the docker image in case it has gone missing, as many people do # Rebuild the docker image in case it has gone missing, as many people do
# automated cleanup of docker images. # automated cleanup of docker images.
build_docker_image(prefix, pull=False) build_docker_image(hook.prefix, pull=False)
entry_exe, *cmd_rest = lang_base.hook_cmd(entry, args) entry_exe, *cmd_rest = hook.cmd
entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix))
return lang_base.run_xargs( cmd = (*docker_cmd(), *entry_tag, *cmd_rest)
(*docker_cmd(color=color), *entry_tag, *cmd_rest), return helpers.run_xargs(hook, cmd, file_args, color=color)
file_args,
require_serial=require_serial,
color=color,
)

View file

@ -1,32 +1,20 @@
from __future__ import annotations from typing import Sequence
from typing import Tuple
from collections.abc import Sequence from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit import lang_base
from pre_commit.languages.docker import docker_cmd from pre_commit.languages.docker import docker_cmd
from pre_commit.prefix import Prefix
ENVIRONMENT_DIR = None ENVIRONMENT_DIR = None
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
install_environment = lang_base.no_install install_environment = helpers.no_install
in_env = lang_base.no_env
def run_hook( def run_hook(
prefix: Prefix, hook: Hook,
entry: str,
args: Sequence[str],
file_args: Sequence[str], file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool, color: bool,
) -> tuple[int, bytes]: # pragma: win32 no cover ) -> Tuple[int, bytes]: # pragma: win32 no cover
cmd = docker_cmd(color=color) + lang_base.hook_cmd(entry, args) cmd = docker_cmd() + hook.cmd
return lang_base.run_xargs( return helpers.run_xargs(hook, cmd, file_args, color=color)
cmd,
file_args,
require_serial=require_serial,
color=color,
)

View file

@ -1,26 +1,23 @@
from __future__ import annotations
import contextlib import contextlib
import os.path import os.path
import re from typing import Generator
import tempfile from typing import Sequence
import xml.etree.ElementTree from typing import Tuple
import zipfile
from collections.abc import Generator
from collections.abc import Sequence
from pre_commit import lang_base import pre_commit.constants as C
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
ENVIRONMENT_DIR = 'dotnetenv' ENVIRONMENT_DIR = 'dotnetenv'
BIN_DIR = 'bin' BIN_DIR = 'bin'
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
run_hook = lang_base.basic_run_hook
def get_env_patch(venv: str) -> PatchesT: def get_env_patch(venv: str) -> PatchesT:
@ -30,82 +27,63 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(prefix: Prefix) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT)
envdir = prefix.path(directory)
with envcontext(get_env_patch(envdir)): with envcontext(get_env_patch(envdir)):
yield yield
@contextlib.contextmanager
def _nuget_config_no_sources() -> Generator[str]:
with tempfile.TemporaryDirectory() as tmpdir:
nuget_config = os.path.join(tmpdir, 'nuget.config')
with open(nuget_config, 'w') as f:
f.write(
'<?xml version="1.0" encoding="utf-8"?>'
'<configuration>'
' <packageSources>'
' <clear />'
' </packageSources>'
'</configuration>',
)
yield nuget_config
def install_environment( def install_environment(
prefix: Prefix, prefix: Prefix,
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
lang_base.assert_version_default('dotnet', version) helpers.assert_version_default('dotnet', version)
lang_base.assert_no_additional_deps('dotnet', additional_dependencies) helpers.assert_no_additional_deps('dotnet', additional_dependencies)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
build_dir = prefix.path('pre-commit-build') with clean_path_on_failure(envdir):
build_dir = 'pre-commit-build'
# Build & pack nupkg file # Build & pack nupkg file
lang_base.setup_cmd( helpers.run_setup_cmd(
prefix, prefix,
( (
'dotnet', 'pack', 'dotnet', 'pack',
'--configuration', 'Release', '--configuration', 'Release',
'--property', f'PackageOutputPath={build_dir}', '--output', build_dir,
), ),
) )
nupkg_dir = prefix.path(build_dir) # Determine tool from the packaged file <tool_name>.<version>.nupkg
nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')] build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir))
if len(build_outputs) != 1:
if not nupkgs: raise NotImplementedError(
raise AssertionError('could not find any build outputs to install') f"Can't handle multiple build outputs. Got {build_outputs}",
)
for nupkg in nupkgs: tool_name = build_outputs[0].split('.')[0]
with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f:
nuspec, = (x for x in f.namelist() if x.endswith('.nuspec'))
with f.open(nuspec) as spec:
tree = xml.etree.ElementTree.parse(spec)
namespace = re.match(r'{.*}', tree.getroot().tag)
if not namespace:
raise AssertionError('could not parse namespace from nuspec')
tool_id_element = tree.find(f'.//{namespace[0]}id')
if tool_id_element is None:
raise AssertionError('expected to find an "id" element')
tool_id = tool_id_element.text
if not tool_id:
raise AssertionError('"id" element missing tool name')
# Install to bin dir # Install to bin dir
with _nuget_config_no_sources() as nuget_config: helpers.run_setup_cmd(
lang_base.setup_cmd( prefix,
prefix, (
( 'dotnet', 'tool', 'install',
'dotnet', 'tool', 'install', '--tool-path', os.path.join(envdir, BIN_DIR),
'--configfile', nuget_config, '--add-source', build_dir,
'--tool-path', os.path.join(envdir, BIN_DIR), tool_name,
'--add-source', build_dir, ),
tool_id, )
),
) # Clean the git dir, ignoring the environment dir
clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*')
helpers.run_setup_cmd(prefix, clean_cmd)
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,27 +1,20 @@
from __future__ import annotations from typing import Sequence
from typing import Tuple
from collections.abc import Sequence from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit import lang_base
from pre_commit.prefix import Prefix
ENVIRONMENT_DIR = None ENVIRONMENT_DIR = None
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
install_environment = lang_base.no_install install_environment = helpers.no_install
in_env = lang_base.no_env
def run_hook( def run_hook(
prefix: Prefix, hook: Hook,
entry: str,
args: Sequence[str],
file_args: Sequence[str], file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool, color: bool,
) -> tuple[int, bytes]: ) -> Tuple[int, bytes]:
out = f'{entry}\n\n'.encode() out = f'{hook.entry}\n\n'.encode()
out += b'\n'.join(f.encode() for f in file_args) + b'\n' out += b'\n'.join(f.encode() for f in file_args) + b'\n'
return 1, out return 1, out

View file

@ -1,161 +1,98 @@
from __future__ import annotations
import contextlib import contextlib
import functools
import json
import os.path import os.path
import platform
import shutil
import sys import sys
import tarfile from typing import Generator
import tempfile from typing import Sequence
import urllib.error from typing import Tuple
import urllib.request
import zipfile
from collections.abc import Generator
from collections.abc import Sequence
from typing import ContextManager
from typing import IO
from typing import Protocol
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import lang_base from pre_commit import git
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.git import no_git_env from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
from pre_commit.util import rmtree from pre_commit.util import rmtree
ENVIRONMENT_DIR = 'golangenv' ENVIRONMENT_DIR = 'golangenv'
health_check = lang_base.basic_health_check get_default_version = helpers.basic_get_default_version
run_hook = lang_base.basic_run_hook healthy = helpers.basic_healthy
_ARCH_ALIASES = {
'x86_64': 'amd64',
'i386': '386',
'aarch64': 'arm64',
'armv8': 'arm64',
'armv7l': 'armv6l',
}
_ARCH = platform.machine().lower()
_ARCH = _ARCH_ALIASES.get(_ARCH, _ARCH)
class ExtractAll(Protocol): def get_env_patch(venv: str) -> PatchesT:
def extractall(self, path: str) -> None: ...
if sys.platform == 'win32': # pragma: win32 cover
_EXT = 'zip'
def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]:
return zipfile.ZipFile(bio)
else: # pragma: win32 no cover
_EXT = 'tar.gz'
def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]:
return tarfile.open(fileobj=bio)
@functools.lru_cache(maxsize=1)
def get_default_version() -> str:
if lang_base.exe_exists('go'):
return 'system'
else:
return C.DEFAULT
def get_env_patch(venv: str, version: str) -> PatchesT:
if version == 'system':
return (
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
)
return ( return (
('GOROOT', os.path.join(venv, '.go')), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
('GOTOOLCHAIN', 'local'),
(
'PATH', (
os.path.join(venv, 'bin'), os.pathsep,
os.path.join(venv, '.go', 'bin'), os.pathsep, Var('PATH'),
),
),
) )
@functools.lru_cache
def _infer_go_version(version: str) -> str:
if version != C.DEFAULT:
return version
resp = urllib.request.urlopen('https://go.dev/dl/?mode=json')
return json.load(resp)[0]['version'].removeprefix('go')
def _get_url(version: str) -> str:
os_name = platform.system().lower()
version = _infer_go_version(version)
return f'https://dl.google.com/go/go{version}.{os_name}-{_ARCH}.{_EXT}'
def _install_go(version: str, dest: str) -> None:
try:
resp = urllib.request.urlopen(_get_url(version))
except urllib.error.HTTPError as e: # pragma: no cover
if e.code == 404:
raise ValueError(
f'Could not find a version matching your system requirements '
f'(os={platform.system().lower()}; arch={_ARCH})',
) from e
else:
raise
else:
with tempfile.TemporaryFile() as f:
shutil.copyfileobj(resp, f)
f.seek(0)
with _open_archive(f) as archive:
archive.extractall(dest)
shutil.move(os.path.join(dest, 'go'), os.path.join(dest, '.go'))
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(prefix: Prefix) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) envdir = prefix.path(
with envcontext(get_env_patch(envdir, version)): helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
with envcontext(get_env_patch(envdir)):
yield yield
def guess_go_dir(remote_url: str) -> str:
if remote_url.endswith('.git'):
remote_url = remote_url[:-1 * len('.git')]
looks_like_url = (
not remote_url.startswith('file://') and
('//' in remote_url or '@' in remote_url)
)
remote_url = remote_url.replace(':', '/')
if looks_like_url:
_, _, remote_url = remote_url.rpartition('//')
_, _, remote_url = remote_url.rpartition('@')
return remote_url
else:
return 'unknown_src_dir'
def install_environment( def install_environment(
prefix: Prefix, prefix: Prefix,
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) helpers.assert_version_default('golang', version)
directory = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
if version != 'system': with clean_path_on_failure(directory):
_install_go(version, env_dir) remote = git.get_remote_url(prefix.prefix_dir)
repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote))
if sys.platform == 'cygwin': # pragma: no cover # Clone into the goenv we'll create
gopath = cmd_output('cygpath', '-w', env_dir)[1].strip() cmd = ('git', 'clone', '--recursive', '.', repo_src_dir)
else: helpers.run_setup_cmd(prefix, cmd)
gopath = env_dir
env = no_git_env(dict(os.environ, GOPATH=gopath)) if sys.platform == 'cygwin': # pragma: no cover
env.pop('GOBIN', None) _, gopath, _ = cmd_output('cygpath', '-w', directory)
if version != 'system': gopath = gopath.strip()
env['GOTOOLCHAIN'] = 'local' else:
env['GOROOT'] = os.path.join(env_dir, '.go') gopath = directory
env['PATH'] = os.pathsep.join(( env = dict(os.environ, GOPATH=gopath)
os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], env.pop('GOBIN', None)
)) cmd_output_b('go', 'get', './...', cwd=repo_src_dir, env=env)
for dependency in additional_dependencies:
cmd_output_b('go', 'get', dependency, cwd=repo_src_dir, env=env)
# Same some disk space, we don't need these after installation
rmtree(prefix.path(directory, 'src'))
pkgdir = prefix.path(directory, 'pkg')
if os.path.exists(pkgdir): # pragma: no cover (go<1.10)
rmtree(pkgdir)
lang_base.setup_cmd(prefix, ('go', 'install', './...'), env=env)
for dependency in additional_dependencies:
lang_base.setup_cmd(prefix, ('go', 'install', dependency), env=env)
# save some disk space -- we don't need this after installation def run_hook(
pkgdir = os.path.join(env_dir, 'pkg') hook: Hook,
if os.path.exists(pkgdir): # pragma: no branch (always true on windows?) file_args: Sequence[str],
rmtree(pkgdir) color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,56 +0,0 @@
from __future__ import annotations
import contextlib
import os.path
from collections.abc import Generator
from collections.abc import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var
from pre_commit.errors import FatalError
from pre_commit.prefix import Prefix
ENVIRONMENT_DIR = 'hs_env'
get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check
run_hook = lang_base.basic_run_hook
def get_env_patch(target_dir: str) -> PatchesT:
bin_path = os.path.join(target_dir, 'bin')
return (('PATH', (bin_path, os.pathsep, Var('PATH'))),)
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
lang_base.assert_version_default('haskell', version)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
pkgs = [*prefix.star('.cabal'), *additional_dependencies]
if not pkgs:
raise FatalError('Expected .cabal files or additional_dependencies')
bindir = os.path.join(envdir, 'bin')
os.makedirs(bindir, exist_ok=True)
lang_base.setup_cmd(prefix, ('cabal', 'update'))
lang_base.setup_cmd(
prefix,
(
'cabal', 'install',
'--install-method', 'copy',
'--installdir', bindir,
*pkgs,
),
)

View file

@ -0,0 +1,136 @@
import multiprocessing
import os
import random
import re
from typing import Any
from typing import List
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit.hook import Hook
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output_b
from pre_commit.xargs import xargs
if TYPE_CHECKING:
from typing import NoReturn
FIXED_RANDOM_SEED = 1542676187
SHIMS_RE = re.compile(r'[/\\]shims[/\\]')
def exe_exists(exe: str) -> bool:
found = parse_shebang.find_executable(exe)
if found is None: # exe exists
return False
homedir = os.path.expanduser('~')
try:
common: Optional[str] = os.path.commonpath((found, homedir))
except ValueError: # on windows, different drives raises ValueError
common = None
return (
# it is not in a /shims/ directory
not SHIMS_RE.search(found) and
(
# the homedir is / (docker, service user, etc.)
os.path.dirname(homedir) == homedir or
# the exe is not contained in the home directory
common != homedir
)
)
def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...], **kwargs: Any) -> None:
cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs)
@overload
def environment_dir(d: None, language_version: str) -> None: ...
@overload
def environment_dir(d: str, language_version: str) -> str: ...
def environment_dir(d: Optional[str], language_version: str) -> Optional[str]:
if d is None:
return None
else:
return f'{d}-{language_version}'
def assert_version_default(binary: str, version: str) -> None:
if version != C.DEFAULT:
raise AssertionError(
f'For now, pre-commit requires system-installed {binary}',
)
def assert_no_additional_deps(
lang: str,
additional_deps: Sequence[str],
) -> None:
if additional_deps:
raise AssertionError(
f'For now, pre-commit does not support '
f'additional_dependencies for {lang}',
)
def basic_get_default_version() -> str:
return C.DEFAULT
def basic_healthy(prefix: Prefix, language_version: str) -> bool:
return True
def no_install(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> 'NoReturn':
raise AssertionError('This type is not installable')
def target_concurrency(hook: Hook) -> int:
if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ:
return 1
else:
# Travis appears to have a bunch of CPUs, but we can't use them all.
if 'TRAVIS' in os.environ:
return 2
else:
try:
return multiprocessing.cpu_count()
except NotImplementedError:
return 1
def _shuffled(seq: Sequence[str]) -> List[str]:
"""Deterministically shuffle"""
fixed_random = random.Random()
fixed_random.seed(FIXED_RANDOM_SEED, version=1)
seq = list(seq)
fixed_random.shuffle(seq)
return seq
def run_xargs(
hook: Hook,
cmd: Tuple[str, ...],
file_args: Sequence[str],
**kwargs: Any,
) -> Tuple[int, bytes]:
# Shuffle the files so that they more evenly fill out the xargs partitions,
# but do it deterministically in case a hook cares about ordering.
file_args = _shuffled(file_args)
kwargs['target_concurrency'] = target_concurrency(hook)
return xargs(cmd, file_args, **kwargs)

View file

@ -1,133 +0,0 @@
from __future__ import annotations
import contextlib
import os
import shutil
from collections.abc import Generator
from collections.abc import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'juliaenv'
health_check = lang_base.basic_health_check
get_default_version = lang_base.basic_get_default_version
def run_hook(
prefix: Prefix,
entry: str,
args: Sequence[str],
file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool,
) -> tuple[int, bytes]:
# `entry` is a (hook-repo relative) file followed by (optional) args, e.g.
# `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we
# 1) shell parse it and join with args with hook_cmd
# 2) prepend the hooks prefix path to the first argument (the file), unless
# it is a local script
# 3) prepend `julia` as the interpreter
cmd = lang_base.hook_cmd(entry, args)
script = cmd[0] if is_local else prefix.path(cmd[0])
cmd = ('julia', '--startup-file=no', script, *cmd[1:])
return lang_base.run_xargs(
cmd,
file_args,
require_serial=require_serial,
color=color,
)
def get_env_patch(target_dir: str, version: str) -> PatchesT:
return (
('JULIA_LOAD_PATH', target_dir),
# May be set, remove it to not interfer with LOAD_PATH
('JULIA_PROJECT', UNSET),
)
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir, version)):
yield
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with in_env(prefix, version):
# TODO: Support language_version with juliaup similar to rust via
# rustup
# if version != 'system':
# ...
# Copy Project.toml to hook env if it exist
os.makedirs(envdir, exist_ok=True)
project_names = ('JuliaProject.toml', 'Project.toml')
project_found = False
for project_name in project_names:
project_file = prefix.path(project_name)
if not os.path.isfile(project_file):
continue
shutil.copy(project_file, envdir)
project_found = True
break
# If no project file was found we create an empty one so that the
# package manager doesn't error
if not project_found:
open(os.path.join(envdir, 'Project.toml'), 'a').close()
# Copy Manifest.toml to hook env if it exists
manifest_names = ('JuliaManifest.toml', 'Manifest.toml')
for manifest_name in manifest_names:
manifest_file = prefix.path(manifest_name)
if not os.path.isfile(manifest_file):
continue
shutil.copy(manifest_file, envdir)
break
# Julia code to instantiate the hook environment
julia_code = """
@assert length(ARGS) > 0
hook_env = ARGS[1]
deps = join(ARGS[2:end], " ")
# We prepend @stdlib here so that we can load the package manager even
# though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env.
pushfirst!(LOAD_PATH, "@stdlib")
using Pkg
popfirst!(LOAD_PATH)
# Instantiate the environment shipped with the hook repo. If we have
# additional dependencies we disable precompilation in this step to
# avoid double work.
precompile = isempty(deps) ? "1" : "0"
withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do
Pkg.instantiate()
end
# Add additional dependencies (with precompilation)
if !isempty(deps)
withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do
Pkg.REPLMode.pkgstr("add " * deps)
end
end
"""
cmd_output_b(
'julia', '--startup-file=no', '-e', julia_code, '--', envdir,
*additional_dependencies,
cwd=prefix.prefix_dir,
)

View file

@ -1,75 +0,0 @@
from __future__ import annotations
import contextlib
import os
import sys
from collections.abc import Generator
from collections.abc import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output
ENVIRONMENT_DIR = 'lua_env'
get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check
run_hook = lang_base.basic_run_hook
def _get_lua_version() -> str: # pragma: win32 no cover
"""Get the Lua version used in file paths."""
_, stdout, _ = cmd_output('luarocks', 'config', '--lua-ver')
return stdout.strip()
def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover
version = _get_lua_version()
so_ext = 'dll' if sys.platform == 'win32' else 'so'
return (
('PATH', (os.path.join(d, 'bin'), os.pathsep, Var('PATH'))),
(
'LUA_PATH', (
os.path.join(d, 'share', 'lua', version, '?.lua;'),
os.path.join(d, 'share', 'lua', version, '?', 'init.lua;;'),
),
),
(
'LUA_CPATH',
(os.path.join(d, 'lib', 'lua', version, f'?.{so_ext};;'),),
),
)
@contextlib.contextmanager # pragma: win32 no cover
def in_env(prefix: Prefix, version: str) -> Generator[None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None: # pragma: win32 no cover
lang_base.assert_version_default('lua', version)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with in_env(prefix, version):
# luarocks doesn't bootstrap a tree prior to installing
# so ensure the directory exists.
os.makedirs(envdir, exist_ok=True)
# Older luarocks (e.g., 2.4.2) expect the rockspec as an arg
for rockspec in prefix.star('.rockspec'):
make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec)
lang_base.setup_cmd(prefix, make_cmd)
# luarocks can't install multiple packages at once
# so install them individually.
for dependency in additional_dependencies:
cmd = ('luarocks', '--tree', envdir, 'install', dependency)
lang_base.setup_cmd(prefix, cmd)

View file

@ -1,26 +1,26 @@
from __future__ import annotations
import contextlib import contextlib
import functools import functools
import os import os
import sys import sys
from collections.abc import Generator from typing import Generator
from collections.abc import Sequence from typing import Sequence
from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import lang_base
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET from pre_commit.envcontext import UNSET
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.languages.python import bin_dir from pre_commit.languages.python import bin_dir
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.util import rmtree from pre_commit.util import rmtree
ENVIRONMENT_DIR = 'node_env' ENVIRONMENT_DIR = 'node_env'
run_hook = lang_base.basic_run_hook
@functools.lru_cache(maxsize=1) @functools.lru_cache(maxsize=1)
@ -30,12 +30,17 @@ def get_default_version() -> str:
return C.DEFAULT return C.DEFAULT
# if node is already installed, we can save a bunch of setup time by # if node is already installed, we can save a bunch of setup time by
# using the installed version # using the installed version
elif all(lang_base.exe_exists(exe) for exe in ('node', 'npm')): elif all(helpers.exe_exists(exe) for exe in ('node', 'npm')):
return 'system' return 'system'
else: else:
return C.DEFAULT return C.DEFAULT
def _envdir(prefix: Prefix, version: str) -> str:
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
return prefix.path(directory)
def get_env_patch(venv: str) -> PatchesT: def get_env_patch(venv: str) -> PatchesT:
if sys.platform == 'cygwin': # pragma: no cover if sys.platform == 'cygwin': # pragma: no cover
_, win_venv, _ = cmd_output('cygpath', '-w', venv) _, win_venv, _ = cmd_output('cygpath', '-w', venv)
@ -59,52 +64,64 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) prefix: Prefix,
with envcontext(get_env_patch(envdir)): language_version: str,
) -> Generator[None, None, None]:
with envcontext(get_env_patch(_envdir(prefix, language_version))):
yield yield
def health_check(prefix: Prefix, version: str) -> str | None: def healthy(prefix: Prefix, language_version: str) -> bool:
with in_env(prefix, version): with in_env(prefix, language_version):
retcode, _, _ = cmd_output_b('node', '--version', check=False) retcode, _, _ = cmd_output_b('node', '--version', retcode=None)
if retcode != 0: # pragma: win32 no cover return retcode == 0
return f'`node --version` returned {retcode}'
else:
return None
def install_environment( def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str], prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None: ) -> None:
additional_dependencies = tuple(additional_dependencies)
assert prefix.exists('package.json') assert prefix.exists('package.json')
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) envdir = _envdir(prefix, version)
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath
if sys.platform == 'win32': # pragma: no cover if sys.platform == 'win32': # pragma: no cover
envdir = fr'\\?\{os.path.normpath(envdir)}' envdir = fr'\\?\{os.path.normpath(envdir)}'
cmd = [sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir] with clean_path_on_failure(envdir):
if version != C.DEFAULT: cmd = [
cmd.extend(['-n', version]) sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir,
cmd_output_b(*cmd) ]
if version != C.DEFAULT:
cmd.extend(['-n', version])
cmd_output_b(*cmd)
with in_env(prefix, version): with in_env(prefix, version):
# https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449
# install as if we installed from git # install as if we installed from git
local_install_cmd = ( local_install_cmd = (
'npm', 'install', '--include=dev', '--include=prod', 'npm', 'install', '--dev', '--prod',
'--ignore-prepublish', '--no-progress', '--no-save', '--ignore-prepublish', '--no-progress', '--no-save',
) )
lang_base.setup_cmd(prefix, local_install_cmd) helpers.run_setup_cmd(prefix, local_install_cmd)
_, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir)
pkg = prefix.path(pkg.strip()) pkg = prefix.path(pkg.strip())
install = ('npm', 'install', '-g', pkg, *additional_dependencies) install = ('npm', 'install', '-g', pkg, *additional_dependencies)
lang_base.setup_cmd(prefix, install) helpers.run_setup_cmd(prefix, install)
# clean these up after installation # clean these up after installation
if prefix.exists('node_modules'): # pragma: win32 no cover if prefix.exists('node_modules'): # pragma: win32 no cover
rmtree(prefix.path('node_modules')) rmtree(prefix.path('node_modules'))
os.remove(pkg) os.remove(pkg)
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,21 +1,26 @@
from __future__ import annotations
import contextlib import contextlib
import os import os
import shlex import shlex
from collections.abc import Generator from typing import Generator
from collections.abc import Sequence from typing import Sequence
from typing import Tuple
from pre_commit import lang_base
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
ENVIRONMENT_DIR = 'perl_env' ENVIRONMENT_DIR = 'perl_env'
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
run_hook = lang_base.basic_run_hook
def _envdir(prefix: Prefix, version: str) -> str:
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
return prefix.path(directory)
def get_env_patch(venv: str) -> PatchesT: def get_env_patch(venv: str) -> PatchesT:
@ -33,18 +38,30 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) prefix: Prefix,
with envcontext(get_env_patch(envdir)): language_version: str,
) -> Generator[None, None, None]:
with envcontext(get_env_patch(_envdir(prefix, language_version))):
yield yield
def install_environment( def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str], prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None: ) -> None:
lang_base.assert_version_default('perl', version) helpers.assert_version_default('perl', version)
with in_env(prefix, version): with clean_path_on_failure(_envdir(prefix, version)):
lang_base.setup_cmd( with in_env(prefix, version):
prefix, ('cpan', '-T', '.', *additional_dependencies), helpers.run_setup_cmd(
) prefix, ('cpan', '-T', '.', *additional_dependencies),
)
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,22 +1,21 @@
from __future__ import annotations
import argparse import argparse
import re import re
import sys import sys
from collections.abc import Sequence
from re import Pattern
from typing import NamedTuple from typing import NamedTuple
from typing import Optional
from typing import Pattern
from typing import Sequence
from typing import Tuple
from pre_commit import lang_base
from pre_commit import output from pre_commit import output
from pre_commit.prefix import Prefix from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.xargs import xargs from pre_commit.xargs import xargs
ENVIRONMENT_DIR = None ENVIRONMENT_DIR = None
get_default_version = lang_base.basic_get_default_version get_default_version = helpers.basic_get_default_version
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
install_environment = lang_base.no_install install_environment = helpers.no_install
in_env = lang_base.no_env
def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int:
@ -88,20 +87,15 @@ FNS = {
def run_hook( def run_hook(
prefix: Prefix, hook: Hook,
entry: str,
args: Sequence[str],
file_args: Sequence[str], file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool, color: bool,
) -> tuple[int, bytes]: ) -> Tuple[int, bytes]:
cmd = (sys.executable, '-m', __name__, *args, entry) exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,)
return xargs(cmd, file_args, color=color) return xargs(exe, file_args, color=color)
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=( description=(
'grep-like finder using python regexes. Unlike grep, this tool ' 'grep-like finder using python regexes. Unlike grep, this tool '
@ -130,4 +124,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,30 +1,32 @@
from __future__ import annotations
import contextlib import contextlib
import functools import functools
import os import os
import sys import sys
from collections.abc import Generator from typing import Dict
from collections.abc import Sequence from typing import Generator
from typing import Optional
from typing import Sequence
from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import lang_base
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET from pre_commit.envcontext import UNSET
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.parse_shebang import find_executable from pre_commit.parse_shebang import find_executable
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.util import win_exe from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'py_env' ENVIRONMENT_DIR = 'py_env'
run_hook = lang_base.basic_run_hook
@functools.cache @functools.lru_cache(maxsize=None)
def _version_info(exe: str) -> str: def _version_info(exe: str) -> str:
prog = 'import sys;print(".".join(str(p) for p in sys.version_info))' prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
try: try:
@ -33,7 +35,7 @@ def _version_info(exe: str) -> str:
return f'<<error retrieving version from {exe}>>' return f'<<error retrieving version from {exe}>>'
def _read_pyvenv_cfg(filename: str) -> dict[str, str]: def _read_pyvenv_cfg(filename: str) -> Dict[str, str]:
ret = {} ret = {}
with open(filename, encoding='UTF-8') as f: with open(filename, encoding='UTF-8') as f:
for line in f: for line in f:
@ -48,7 +50,7 @@ def _read_pyvenv_cfg(filename: str) -> dict[str, str]:
def bin_dir(venv: str) -> str: def bin_dir(venv: str) -> str:
"""On windows there's a different directory for the virtualenv""" """On windows there's a different directory for the virtualenv"""
bin_part = 'Scripts' if sys.platform == 'win32' else 'bin' bin_part = 'Scripts' if os.name == 'nt' else 'bin'
return os.path.join(venv, bin_part) return os.path.join(venv, bin_part)
@ -63,9 +65,9 @@ def get_env_patch(venv: str) -> PatchesT:
def _find_by_py_launcher( def _find_by_py_launcher(
version: str, version: str,
) -> str | None: # pragma: no cover (windows only) ) -> Optional[str]: # pragma: no cover (windows only)
if version.startswith('python'): if version.startswith('python'):
num = version.removeprefix('python') num = version[len('python'):]
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
env = dict(os.environ, PYTHONIOENCODING='UTF-8') env = dict(os.environ, PYTHONIOENCODING='UTF-8')
try: try:
@ -75,15 +77,8 @@ def _find_by_py_launcher(
return None return None
def _impl_exe_name() -> str: def _find_by_sys_executable() -> Optional[str]:
if sys.implementation.name == 'cpython': # pragma: cpython cover def _norm(path: str) -> Optional[str]:
return 'python'
else: # pragma: cpython no cover
return sys.implementation.name # pypy mostly
def _find_by_sys_executable() -> str | None:
def _norm(path: str) -> str | None:
_, exe = os.path.split(path.lower()) _, exe = os.path.split(path.lower())
exe, _, _ = exe.partition('.exe') exe, _, _ = exe.partition('.exe')
if exe not in {'python', 'pythonw'} and find_executable(exe): if exe not in {'python', 'pythonw'} and find_executable(exe):
@ -107,25 +102,18 @@ def _find_by_sys_executable() -> str | None:
@functools.lru_cache(maxsize=1) @functools.lru_cache(maxsize=1)
def get_default_version() -> str: # pragma: no cover (platform dependent) def get_default_version() -> str: # pragma: no cover (platform dependent)
v_major = f'{sys.version_info[0]}' # First attempt from `sys.executable` (or the realpath)
v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}' exe = _find_by_sys_executable()
if exe:
return exe
# attempt the likely implementation exe # Next try the `pythonX.X` executable
for potential in (v_minor, v_major): exe = f'python{sys.version_info[0]}.{sys.version_info[1]}'
exe = f'{_impl_exe_name()}{potential}' if find_executable(exe):
if find_executable(exe): return exe
return exe
# next try `sys.executable` (or the realpath) if _find_by_py_launcher(exe):
maybe_exe = _find_by_sys_executable() return exe
if maybe_exe:
return maybe_exe
# maybe on windows we can find it via py launcher?
if sys.platform == 'win32': # pragma: win32 cover
exe = f'python{v_minor}'
if _find_by_py_launcher(exe):
return exe
# We tried! # We tried!
return C.DEFAULT return C.DEFAULT
@ -138,20 +126,20 @@ def _sys_executable_matches(version: str) -> bool:
return False return False
try: try:
info = tuple(int(p) for p in version.removeprefix('python').split('.')) info = tuple(int(p) for p in version[len('python'):].split('.'))
except ValueError: except ValueError:
return False return False
return sys.version_info[:len(info)] == info return sys.version_info[:len(info)] == info
def norm_version(version: str) -> str | None: def norm_version(version: str) -> Optional[str]:
if version == C.DEFAULT: # use virtualenv's default if version == C.DEFAULT: # use virtualenv's default
return None return None
elif _sys_executable_matches(version): # virtualenv defaults to our exe elif _sys_executable_matches(version): # virtualenv defaults to our exe
return None return None
if sys.platform == 'win32': # pragma: no cover (windows) if os.name == 'nt': # pragma: no cover (windows)
version_exec = _find_by_py_launcher(version) version_exec = _find_by_py_launcher(version)
if version_exec: if version_exec:
return version_exec return version_exec
@ -166,49 +154,37 @@ def norm_version(version: str) -> str | None:
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) prefix: Prefix,
language_version: str,
) -> Generator[None, None, None]:
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
envdir = prefix.path(directory)
with envcontext(get_env_patch(envdir)): with envcontext(get_env_patch(envdir)):
yield yield
def health_check(prefix: Prefix, version: str) -> str | None: def healthy(prefix: Prefix, language_version: str) -> bool:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
envdir = prefix.path(directory)
pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg')
# created with "old" virtualenv # created with "old" virtualenv
if not os.path.exists(pyvenv_cfg): if not os.path.exists(pyvenv_cfg):
return 'pyvenv.cfg does not exist (old virtualenv?)' return False
exe_name = win_exe('python') exe_name = win_exe('python')
py_exe = prefix.path(bin_dir(envdir), exe_name) py_exe = prefix.path(bin_dir(envdir), exe_name)
cfg = _read_pyvenv_cfg(pyvenv_cfg) cfg = _read_pyvenv_cfg(pyvenv_cfg)
if 'version_info' not in cfg: return (
return "created virtualenv's pyvenv.cfg is missing `version_info`" 'version_info' in cfg and
# always use uncached lookup here in case we replaced an unhealthy env
# always use uncached lookup here in case we replaced an unhealthy env _version_info.__wrapped__(py_exe) == cfg['version_info'] and (
virtualenv_version = _version_info.__wrapped__(py_exe) 'base-executable' not in cfg or
if virtualenv_version != cfg['version_info']: _version_info(cfg['base-executable']) == cfg['version_info']
return (
f'virtualenv python version did not match created version:\n'
f'- actual version: {virtualenv_version}\n'
f'- expected version: {cfg["version_info"]}\n'
) )
)
# made with an older version of virtualenv? skip `base-executable` check
if 'base-executable' not in cfg:
return None
base_exe_version = _version_info(cfg['base-executable'])
if base_exe_version != cfg['version_info']:
return (
f'base executable python version does not match created version:\n'
f'- base-executable version: {base_exe_version}\n'
f'- expected version: {cfg["version_info"]}\n'
)
else:
return None
def install_environment( def install_environment(
@ -216,13 +192,23 @@ def install_environment(
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
venv_cmd = [sys.executable, '-mvirtualenv', envdir] venv_cmd = [sys.executable, '-mvirtualenv', envdir]
python = norm_version(version) python = norm_version(version)
if python is not None: if python is not None:
venv_cmd.extend(('-p', python)) venv_cmd.extend(('-p', python))
install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies) install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies)
cmd_output_b(*venv_cmd, cwd='/') with clean_path_on_failure(envdir):
with in_env(prefix, version): cmd_output_b(*venv_cmd, cwd='/')
lang_base.setup_cmd(prefix, install_cmd) with in_env(prefix, version):
helpers.run_setup_cmd(prefix, install_cmd)
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,160 +1,61 @@
from __future__ import annotations
import contextlib import contextlib
import os import os
import shlex import shlex
import shutil import shutil
import tempfile from typing import Generator
import textwrap from typing import Sequence
from collections.abc import Generator from typing import Tuple
from collections.abc import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output from pre_commit.util import clean_path_on_failure
from pre_commit.util import win_exe from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'renv' ENVIRONMENT_DIR = 'renv'
get_default_version = lang_base.basic_get_default_version RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
get_default_version = helpers.basic_get_default_version
_RENV_ACTIVATED_OPTS = ( healthy = helpers.basic_healthy
'--no-save', '--no-restore', '--no-site-file', '--no-environ',
)
def _execute_r(
code: str, *,
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
cli_opts: Sequence[str],
) -> str:
with in_env(prefix, version), _r_code_in_tempfile(code) as f:
_, out, _ = cmd_output(
_rscript_exec(), *cli_opts, f, *args, cwd=cwd,
)
return out.rstrip('\n')
def _execute_r_in_renv(
code: str, *,
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
) -> str:
return _execute_r(
code=code, prefix=prefix, version=version, args=args, cwd=cwd,
cli_opts=_RENV_ACTIVATED_OPTS,
)
def _execute_vanilla_r(
code: str, *,
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
) -> str:
return _execute_r(
code=code, prefix=prefix, version=version, args=args, cwd=cwd,
cli_opts=('--vanilla',),
)
def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
return _execute_r_in_renv(
'cat(renv::settings$r.version())',
prefix=prefix, version=version,
cwd=envdir,
)
def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
return _execute_r_in_renv(
'cat(as.character(getRversion()))',
prefix=prefix, version=version,
cwd=envdir,
)
def _write_current_r_version(
envdir: str, prefix: Prefix, version: str,
) -> None:
_execute_r_in_renv(
'renv::settings$r.version(as.character(getRversion()))',
prefix=prefix, version=version,
cwd=envdir,
)
def health_check(prefix: Prefix, version: str) -> str | None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
r_version_installation = _read_installed_version(
envdir=envdir, prefix=prefix, version=version,
)
r_version_current_executable = _read_executable_version(
envdir=envdir, prefix=prefix, version=version,
)
if r_version_installation in {'NULL', ''}:
return (
f'Hooks were installed with an unknown R version. R version for '
f'hook repo now set to {r_version_current_executable}'
)
elif r_version_installation != r_version_current_executable:
return (
f'Hooks were installed for R version {r_version_installation}, '
f'but current R executable has version '
f'{r_version_current_executable}'
)
return None
@contextlib.contextmanager
def _r_code_in_tempfile(code: str) -> Generator[str]:
"""
To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}`
but use `Rscript [options] path/to/file_with_expr.R`
"""
with tempfile.TemporaryDirectory() as tmpdir:
fname = os.path.join(tmpdir, 'script.R')
with open(fname, 'w') as f:
f.write(_inline_r_setup(textwrap.dedent(code)))
yield fname
def get_env_patch(venv: str) -> PatchesT: def get_env_patch(venv: str) -> PatchesT:
return ( return (
('R_PROFILE_USER', os.path.join(venv, 'activate.R')), ('R_PROFILE_USER', os.path.join(venv, 'activate.R')),
('RENV_PROJECT', UNSET),
) )
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) prefix: Prefix,
language_version: str,
) -> Generator[None, None, None]:
envdir = _get_env_dir(prefix, language_version)
with envcontext(get_env_patch(envdir)): with envcontext(get_env_patch(envdir)):
yield yield
def _prefix_if_file_entry( def _get_env_dir(prefix: Prefix, version: str) -> str:
entry: list[str], return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
prefix: Prefix,
*,
is_local: bool, def _prefix_if_non_local_file_entry(
entry: Sequence[str],
prefix: Prefix,
src: str,
) -> Sequence[str]: ) -> Sequence[str]:
if entry[1] == '-e' or is_local: if entry[1] == '-e':
return entry[1:] return entry[1:]
else: else:
return (prefix.path(entry[1]),) if src == 'local':
path = entry[1]
else:
path = prefix.path(entry[1])
return (path,)
def _rscript_exec() -> str: def _entry_validate(entry: Sequence[str]) -> None:
r_home = os.environ.get('R_HOME')
if r_home is None:
return 'Rscript'
else:
return os.path.join(r_home, 'bin', win_exe('Rscript'))
def _entry_validate(entry: list[str]) -> None:
""" """
Allowed entries: Allowed entries:
# Rscript -e expr # Rscript -e expr
@ -168,23 +69,20 @@ def _entry_validate(entry: list[str]) -> None:
raise ValueError('You can supply at most one expression.') raise ValueError('You can supply at most one expression.')
elif len(entry) > 2: elif len(entry) > 2:
raise ValueError( raise ValueError(
'The only valid syntax is `Rscript -e {expr}`' 'The only valid syntax is `Rscript -e {expr}`',
'or `Rscript path/to/hook/script`', 'or `Rscript path/to/hook/script`',
) )
def _cmd_from_hook( def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]:
prefix: Prefix, entry = shlex.split(hook.entry)
entry: str, _entry_validate(entry)
args: Sequence[str],
*,
is_local: bool,
) -> tuple[str, ...]:
cmd = shlex.split(entry)
_entry_validate(cmd)
cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local) return (
return (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args) *entry[:1], *RSCRIPT_OPTS,
*_prefix_if_non_local_file_entry(entry, hook.prefix, hook.src),
*hook.args,
)
def install_environment( def install_environment(
@ -192,87 +90,59 @@ def install_environment(
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
lang_base.assert_version_default('r', version) env_dir = _get_env_dir(prefix, version)
with clean_path_on_failure(env_dir):
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True)
os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir)
shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv'))
shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) cmd_output_b(
'Rscript', '--vanilla', '-e',
r_code_inst_environment = f"""\ f"""\
prefix_dir <- {prefix.prefix_dir!r} prefix_dir <- {prefix.prefix_dir!r}
options( options(
repos = c(CRAN = "https://cran.rstudio.com"), repos = c(CRAN = "https://cran.rstudio.com"),
renv.consent = TRUE renv.consent = TRUE
) )
source("renv/activate.R") source("renv/activate.R")
renv::restore() renv::restore()
activate_statement <- paste0( activate_statement <- paste0(
'suppressWarnings({{', 'suppressWarnings({{',
'old <- setwd("', getwd(), '"); ', 'old <- setwd("', getwd(), '"); ',
'source("renv/activate.R"); ', 'source("renv/activate.R"); ',
'setwd(old); ', 'setwd(old); ',
'renv::load("', getwd(), '");}})' 'renv::load("', getwd(), '");}})'
) )
writeLines(activate_statement, 'activate.R') writeLines(activate_statement, 'activate.R')
is_package <- tryCatch( is_package <- tryCatch(
{{ {{
path_desc <- file.path(prefix_dir, 'DESCRIPTION') path_desc <- file.path(prefix_dir, 'DESCRIPTION')
suppressWarnings(desc <- read.dcf(path_desc)) suppressWarnings(desc <- read.dcf(path_desc))
"Package" %in% colnames(desc) "Package" %in% colnames(desc)
}}, }},
error = function(...) FALSE error = function(...) FALSE
) )
if (is_package) {{ if (is_package) {{
renv::install(prefix_dir) renv::install(prefix_dir)
}} }}
""" """,
_execute_vanilla_r(
r_code_inst_environment,
prefix=prefix, version=version, cwd=env_dir,
)
_write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
if additional_dependencies:
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
_execute_r_in_renv(
code=r_code_inst_add, prefix=prefix, version=version,
args=additional_dependencies,
cwd=env_dir, cwd=env_dir,
) )
if additional_dependencies:
with in_env(prefix, version):
def _inline_r_setup(code: str) -> str: cmd_output_b(
""" 'Rscript', *RSCRIPT_OPTS, '-e',
Some behaviour of R cannot be configured via env variables, but can 'renv::install(commandArgs(trailingOnly = TRUE))',
only be configured via R options once R has started. These are set here. *additional_dependencies,
""" cwd=env_dir,
with_option = [ )
textwrap.dedent("""\
options(
install.packages.compile.from.source = "never",
pkgType = "binary"
)
"""),
code,
]
return '\n'.join(with_option)
def run_hook( def run_hook(
prefix: Prefix, hook: Hook,
entry: str,
args: Sequence[str],
file_args: Sequence[str], file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool, color: bool,
) -> tuple[int, bytes]: ) -> Tuple[int, bytes]:
cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) with in_env(hook.prefix, hook.language_version):
return lang_base.run_xargs( return helpers.run_xargs(
cmd, hook, _cmd_from_hook(hook), file_args, color=color,
file_args, )
require_serial=require_serial,
color=color,
)

View file

@ -1,37 +1,31 @@
from __future__ import annotations
import contextlib import contextlib
import functools import functools
import importlib.resources
import os.path import os.path
import shutil import shutil
import tarfile import tarfile
from collections.abc import Generator from typing import Generator
from collections.abc import Sequence from typing import Sequence
from typing import IO from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import lang_base
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET from pre_commit.envcontext import UNSET
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
from pre_commit.util import clean_path_on_failure
from pre_commit.util import resource_bytesio
ENVIRONMENT_DIR = 'rbenv' ENVIRONMENT_DIR = 'rbenv'
health_check = lang_base.basic_health_check healthy = helpers.basic_healthy
run_hook = lang_base.basic_run_hook
def _resource_bytesio(filename: str) -> IO[bytes]:
files = importlib.resources.files('pre_commit.resources')
return files.joinpath(filename).open('rb')
@functools.lru_cache(maxsize=1) @functools.lru_cache(maxsize=1)
def get_default_version() -> str: def get_default_version() -> str:
if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')): if all(helpers.exe_exists(exe) for exe in ('ruby', 'gem')):
return 'system' return 'system'
else: else:
return C.DEFAULT return C.DEFAULT
@ -73,14 +67,19 @@ def get_env_patch(
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) prefix: Prefix,
with envcontext(get_env_patch(envdir, version)): language_version: str,
) -> Generator[None, None, None]:
envdir = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, language_version),
)
with envcontext(get_env_patch(envdir, language_version)):
yield yield
def _extract_resource(filename: str, dest: str) -> None: def _extract_resource(filename: str, dest: str) -> None:
with _resource_bytesio(filename) as bio: with resource_bytesio(filename) as bio:
with tarfile.open(fileobj=bio) as tf: with tarfile.open(fileobj=bio) as tf:
tf.extractall(dest) tf.extractall(dest)
@ -89,14 +88,14 @@ def _install_rbenv(
prefix: Prefix, prefix: Prefix,
version: str, version: str,
) -> None: # pragma: win32 no cover ) -> None: # pragma: win32 no cover
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
_extract_resource('rbenv.tar.gz', prefix.path('.')) _extract_resource('rbenv.tar.gz', prefix.path('.'))
shutil.move(prefix.path('rbenv'), envdir) shutil.move(prefix.path('rbenv'), prefix.path(directory))
# Only install ruby-build if the version is specified # Only install ruby-build if the version is specified
if version != C.DEFAULT: if version != C.DEFAULT:
plugins_dir = os.path.join(envdir, 'plugins') plugins_dir = prefix.path(directory, 'plugins')
_extract_resource('ruby-download.tar.gz', plugins_dir) _extract_resource('ruby-download.tar.gz', plugins_dir)
_extract_resource('ruby-build.tar.gz', plugins_dir) _extract_resource('ruby-build.tar.gz', plugins_dir)
@ -106,40 +105,47 @@ def _install_ruby(
version: str, version: str,
) -> None: # pragma: win32 no cover ) -> None: # pragma: win32 no cover
try: try:
lang_base.setup_cmd(prefix, ('rbenv', 'download', version)) helpers.run_setup_cmd(prefix, ('rbenv', 'download', version))
except CalledProcessError: # pragma: no cover (usually find with download) except CalledProcessError: # pragma: no cover (usually find with download)
# Failed to download from mirror for some reason, build it instead # Failed to download from mirror for some reason, build it instead
lang_base.setup_cmd(prefix, ('rbenv', 'install', version)) helpers.run_setup_cmd(prefix, ('rbenv', 'install', version))
def install_environment( def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str], prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None: ) -> None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) additional_dependencies = tuple(additional_dependencies)
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
with clean_path_on_failure(prefix.path(directory)):
if version != 'system': # pragma: win32 no cover
_install_rbenv(prefix, version)
with in_env(prefix, version):
# Need to call this before installing so rbenv's directories
# are set up
helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-'))
if version != C.DEFAULT:
_install_ruby(prefix, version)
# Need to call this after installing to set up the shims
helpers.run_setup_cmd(prefix, ('rbenv', 'rehash'))
if version != 'system': # pragma: win32 no cover
_install_rbenv(prefix, version)
with in_env(prefix, version): with in_env(prefix, version):
# Need to call this before installing so rbenv's directories helpers.run_setup_cmd(
# are set up prefix, ('gem', 'build', *prefix.star('.gemspec')),
lang_base.setup_cmd(prefix, ('rbenv', 'init', '-')) )
if version != C.DEFAULT: helpers.run_setup_cmd(
_install_ruby(prefix, version) prefix,
# Need to call this after installing to set up the shims (
lang_base.setup_cmd(prefix, ('rbenv', 'rehash')) 'gem', 'install',
'--no-document', '--no-format-executable',
*prefix.star('.gem'), *additional_dependencies,
),
)
with in_env(prefix, version):
lang_base.setup_cmd( def run_hook(
prefix, ('gem', 'build', *prefix.star('.gemspec')), hook: Hook,
) file_args: Sequence[str],
lang_base.setup_cmd( color: bool,
prefix, ) -> Tuple[int, bytes]:
( with in_env(hook.prefix, hook.language_version):
'gem', 'install', return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
'--no-document', '--no-format-executable',
'--no-user-install',
'--install-dir', os.path.join(envdir, 'gems'),
'--bindir', os.path.join(envdir, 'gems', 'bin'),
*prefix.star('.gem'), *additional_dependencies,
),
)

View file

@ -1,113 +1,55 @@
from __future__ import annotations
import contextlib import contextlib
import functools
import os.path import os.path
import shutil from typing import Generator
import sys from typing import Sequence
import tempfile from typing import Set
import urllib.request from typing import Tuple
from collections.abc import Generator
from collections.abc import Sequence import toml
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import lang_base
from pre_commit import parse_shebang
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.util import make_executable
from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'rustenv' ENVIRONMENT_DIR = 'rustenv'
health_check = lang_base.basic_health_check get_default_version = helpers.basic_get_default_version
run_hook = lang_base.basic_run_hook healthy = helpers.basic_healthy
@functools.lru_cache(maxsize=1) def get_env_patch(target_dir: str) -> PatchesT:
def get_default_version() -> str:
# If rust is already installed, we can save a bunch of setup time by
# using the installed version.
#
# Just detecting the executable does not suffice, because if rustup is
# installed but no toolchain is available, then `cargo` exists but
# cannot be used without installing a toolchain first.
if cmd_output_b('cargo', '--version', check=False, cwd='/')[0] == 0:
return 'system'
else:
return C.DEFAULT
def _rust_toolchain(language_version: str) -> str:
"""Transform the language version into a rust toolchain version."""
if language_version == C.DEFAULT:
return 'stable'
else:
return language_version
def get_env_patch(target_dir: str, version: str) -> PatchesT:
return ( return (
('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))),
# Only set RUSTUP_TOOLCHAIN if we don't want use the system's default
# toolchain
*(
(('RUSTUP_TOOLCHAIN', _rust_toolchain(version)),)
if version != 'system' else ()
),
) )
@contextlib.contextmanager @contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(prefix: Prefix) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) target_dir = prefix.path(
with envcontext(get_env_patch(envdir, version)): helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
with envcontext(get_env_patch(target_dir)):
yield yield
def _add_dependencies( def _add_dependencies(
prefix: Prefix, cargo_toml_path: str,
additional_dependencies: set[str], additional_dependencies: Set[str],
) -> None: ) -> None:
crates = [] with open(cargo_toml_path, 'r+') as f:
for dep in additional_dependencies: cargo_toml = toml.load(f)
name, _, spec = dep.partition(':') cargo_toml.setdefault('dependencies', {})
crate = f'{name}@{spec or "*"}' for dep in additional_dependencies:
crates.append(crate) name, _, spec = dep.partition(':')
cargo_toml['dependencies'][name] = spec or '*'
lang_base.setup_cmd(prefix, ('cargo', 'add', *crates)) f.seek(0)
toml.dump(cargo_toml, f)
f.truncate()
def install_rust_with_toolchain(toolchain: str, envdir: str) -> None:
with tempfile.TemporaryDirectory() as rustup_dir:
with envcontext((('CARGO_HOME', envdir), ('RUSTUP_HOME', rustup_dir))):
# acquire `rustup` if not present
if parse_shebang.find_executable('rustup') is None:
# We did not detect rustup and need to download it first.
if sys.platform == 'win32': # pragma: win32 cover
url = 'https://win.rustup.rs/x86_64'
else: # pragma: win32 no cover
url = 'https://sh.rustup.rs'
resp = urllib.request.urlopen(url)
rustup_init = os.path.join(rustup_dir, win_exe('rustup-init'))
with open(rustup_init, 'wb') as f:
shutil.copyfileobj(resp, f)
make_executable(rustup_init)
# install rustup into `$CARGO_HOME/bin`
cmd_output_b(
rustup_init, '-y', '--quiet', '--no-modify-path',
'--default-toolchain', 'none',
)
cmd_output_b(
'rustup', 'toolchain', 'install', '--no-self-update',
toolchain,
)
def install_environment( def install_environment(
@ -115,7 +57,10 @@ def install_environment(
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) helpers.assert_version_default('rust', version)
directory = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
# There are two cases where we might want to specify more dependencies: # There are two cases where we might want to specify more dependencies:
# as dependencies for the library being built, and as binary packages # as dependencies for the library being built, and as binary packages
@ -132,29 +77,30 @@ def install_environment(
} }
lib_deps = set(additional_dependencies) - cli_deps lib_deps = set(additional_dependencies) - cli_deps
packages_to_install: set[tuple[str, ...]] = {('--path', '.')} if len(lib_deps) > 0:
for cli_dep in cli_deps: _add_dependencies(prefix.path('Cargo.toml'), lib_deps)
cli_dep = cli_dep.removeprefix('cli:')
package, _, crate_version = cli_dep.partition(':')
if crate_version != '':
packages_to_install.add((package, '--version', crate_version))
else:
packages_to_install.add((package,))
with contextlib.ExitStack() as ctx: with clean_path_on_failure(directory):
ctx.enter_context(in_env(prefix, version)) packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')}
for cli_dep in cli_deps:
if version != 'system': cli_dep = cli_dep[len('cli:'):]
install_rust_with_toolchain(_rust_toolchain(version), envdir) package, _, version = cli_dep.partition(':')
if version != '':
tmpdir = ctx.enter_context(tempfile.TemporaryDirectory()) packages_to_install.add((package, '--version', version))
ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),))) else:
packages_to_install.add((package,))
if len(lib_deps) > 0:
_add_dependencies(prefix, lib_deps)
for args in packages_to_install: for args in packages_to_install:
cmd_output_b( cmd_output_b(
'cargo', 'install', '--bins', '--root', envdir, *args, 'cargo', 'install', '--bins', '--root', directory, *args,
cwd=prefix.prefix_dir, cwd=prefix.prefix_dir,
) )
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -0,0 +1,19 @@
from typing import Sequence
from typing import Tuple
from pre_commit.hook import Hook
from pre_commit.languages import helpers
ENVIRONMENT_DIR = None
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
install_environment = helpers.no_install
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:])
return helpers.run_xargs(hook, cmd, file_args, color=color)

View file

@ -1,25 +1,25 @@
from __future__ import annotations
import contextlib import contextlib
import os import os
from collections.abc import Generator from typing import Generator
from collections.abc import Sequence from typing import Sequence
from typing import Tuple
from pre_commit import lang_base import pre_commit.constants as C
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'swift_env'
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
BUILD_DIR = '.build' BUILD_DIR = '.build'
BUILD_CONFIG = 'release' BUILD_CONFIG = 'release'
ENVIRONMENT_DIR = 'swift_env'
get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check
run_hook = lang_base.basic_run_hook
def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover
bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG) bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG)
@ -27,8 +27,10 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover
@contextlib.contextmanager # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover
def in_env(prefix: Prefix, version: str) -> Generator[None]: def in_env(prefix: Prefix) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) envdir = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
with envcontext(get_env_patch(envdir)): with envcontext(get_env_patch(envdir)):
yield yield
@ -36,15 +38,27 @@ def in_env(prefix: Prefix, version: str) -> Generator[None]:
def install_environment( def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str], prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None: # pragma: win32 no cover ) -> None: # pragma: win32 no cover
lang_base.assert_version_default('swift', version) helpers.assert_version_default('swift', version)
lang_base.assert_no_additional_deps('swift', additional_dependencies) helpers.assert_no_additional_deps('swift', additional_dependencies)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) directory = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
# Build the swift package # Build the swift package
os.mkdir(envdir) with clean_path_on_failure(directory):
cmd_output_b( os.mkdir(directory)
'swift', 'build', cmd_output_b(
'--package-path', prefix.prefix_dir, 'swift', 'build',
'-c', BUILD_CONFIG, '-C', prefix.prefix_dir,
'--build-path', os.path.join(envdir, BUILD_DIR), '-c', BUILD_CONFIG,
) '--build-path', os.path.join(directory, BUILD_DIR),
)
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]: # pragma: win32 no cover
with in_env(hook.prefix):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -0,0 +1,19 @@
from typing import Sequence
from typing import Tuple
from pre_commit.hook import Hook
from pre_commit.languages import helpers
ENVIRONMENT_DIR = None
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
install_environment = helpers.no_install
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,10 +0,0 @@
from __future__ import annotations
from pre_commit import lang_base
ENVIRONMENT_DIR = None
get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check
install_environment = lang_base.no_install
in_env = lang_base.no_env
run_hook = lang_base.basic_run_hook

View file

@ -1,32 +0,0 @@
from __future__ import annotations
from collections.abc import Sequence
from pre_commit import lang_base
from pre_commit.prefix import Prefix
ENVIRONMENT_DIR = None
get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check
install_environment = lang_base.no_install
in_env = lang_base.no_env
def run_hook(
prefix: Prefix,
entry: str,
args: Sequence[str],
file_args: Sequence[str],
*,
is_local: bool,
require_serial: bool,
color: bool,
) -> tuple[int, bytes]:
cmd = lang_base.hook_cmd(entry, args)
cmd = (prefix.path(cmd[0]), *cmd[1:])
return lang_base.run_xargs(
cmd,
file_args,
require_serial=require_serial,
color=color,
)

View file

@ -1,8 +1,6 @@
from __future__ import annotations
import contextlib import contextlib
import logging import logging
from collections.abc import Generator from typing import Generator
from pre_commit import color from pre_commit import color
from pre_commit import output from pre_commit import output
@ -32,7 +30,7 @@ class LoggingHandler(logging.Handler):
@contextlib.contextmanager @contextlib.contextmanager
def logging_handler(use_color: bool) -> Generator[None]: def logging_handler(use_color: bool) -> Generator[None, None, None]:
handler = LoggingHandler(use_color) handler = LoggingHandler(use_color)
logger.addHandler(handler) logger.addHandler(handler)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)

View file

@ -1,16 +1,15 @@
from __future__ import annotations
import argparse import argparse
import logging import logging
import os import os
import sys import sys
from collections.abc import Sequence from typing import Any
from typing import Optional
from typing import Sequence
from typing import Union
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import clientlib
from pre_commit import git from pre_commit import git
from pre_commit.color import add_color_option from pre_commit.color import add_color_option
from pre_commit.commands import hazmat
from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import autoupdate
from pre_commit.commands.clean import clean from pre_commit.commands.clean import clean
from pre_commit.commands.gc import gc from pre_commit.commands.gc import gc
@ -23,8 +22,6 @@ from pre_commit.commands.migrate_config import migrate_config
from pre_commit.commands.run import run from pre_commit.commands.run import run
from pre_commit.commands.sample_config import sample_config from pre_commit.commands.sample_config import sample_config
from pre_commit.commands.try_repo import try_repo from pre_commit.commands.try_repo import try_repo
from pre_commit.commands.validate_config import validate_config
from pre_commit.commands.validate_manifest import validate_manifest
from pre_commit.error_handler import error_handler from pre_commit.error_handler import error_handler
from pre_commit.logging_handler import logging_handler from pre_commit.logging_handler import logging_handler
from pre_commit.store import Store from pre_commit.store import Store
@ -38,13 +35,8 @@ logger = logging.getLogger('pre_commit')
# pyvenv # pyvenv
os.environ.pop('__PYVENV_LAUNCHER__', None) os.environ.pop('__PYVENV_LAUNCHER__', None)
# https://github.com/getsentry/snuba/pull/5388
os.environ.pop('PYTHONEXECUTABLE', None)
COMMANDS_NO_GIT = { COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'}
'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config',
'validate-config', 'validate-manifest',
}
def _add_config_option(parser: argparse.ArgumentParser) -> None: def _add_config_option(parser: argparse.ArgumentParser) -> None:
@ -54,19 +46,43 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None:
) )
class AppendReplaceDefault(argparse.Action):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.appended = False
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: Union[str, Sequence[str], None],
option_string: Optional[str] = None,
) -> None:
if not self.appended:
setattr(namespace, self.dest, [])
self.appended = True
getattr(namespace, self.dest).append(values)
def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
'-t', '--hook-type', '-t', '--hook-type', choices=(
choices=clientlib.HOOK_TYPES, action='append', dest='hook_types', 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg',
'commit-msg', 'post-commit', 'post-checkout', 'post-merge',
'post-rewrite',
),
action=AppendReplaceDefault,
default=['pre-commit'],
dest='hook_types',
) )
def _add_run_options(parser: argparse.ArgumentParser) -> None: def _add_run_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument('hook', nargs='?', help='A single hook-id to run') parser.add_argument('hook', nargs='?', help='A single hook-id to run')
parser.add_argument('--verbose', '-v', action='store_true') parser.add_argument('--verbose', '-v', action='store_true', default=False)
mutex_group = parser.add_mutually_exclusive_group(required=False) mutex_group = parser.add_mutually_exclusive_group(required=False)
mutex_group.add_argument( mutex_group.add_argument(
'--all-files', '-a', action='store_true', '--all-files', '-a', action='store_true', default=False,
help='Run on all the files in the repo.', help='Run on all the files in the repo.',
) )
mutex_group.add_argument( mutex_group.add_argument(
@ -78,14 +94,7 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
help='When hooks fail, run `git diff` directly afterward.', help='When hooks fail, run `git diff` directly afterward.',
) )
parser.add_argument( parser.add_argument(
'--fail-fast', action='store_true', '--hook-stage', choices=C.STAGES, default='commit',
help='Stop after the first failing hook.',
)
parser.add_argument(
'--hook-stage',
choices=clientlib.STAGES,
type=clientlib.transform_stage,
default='pre-commit',
help='The stage during which the hook is fired. One of %(choices)s', help='The stage during which the hook is fired. One of %(choices)s',
) )
parser.add_argument( parser.add_argument(
@ -97,7 +106,7 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
'--from-ref', '--source', '-s', '--from-ref', '--source', '-s',
help=( help=(
'(for usage with `--to-ref`) -- this option represents the ' '(for usage with `--from-ref`) -- this option represents the '
'original ref in a `from_ref...to_ref` diff expression. ' 'original ref in a `from_ref...to_ref` diff expression. '
'For `pre-push` hooks, this represents the branch you are pushing ' 'For `pre-push` hooks, this represents the branch you are pushing '
'to. ' 'to. '
@ -108,42 +117,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
'--to-ref', '--origin', '-o', '--to-ref', '--origin', '-o',
help=( help=(
'(for usage with `--from-ref`) -- this option represents the ' '(for usage with `--to-ref`) -- this option represents the '
'destination ref in a `from_ref...to_ref` diff expression. ' 'destination ref in a `from_ref...to_ref` diff expression. '
'For `pre-push` hooks, this represents the branch being pushed. ' 'For `pre-push` hooks, this represents the branch being pushed. '
'For `post-checkout` hooks, this represents the branch that is ' 'For `post-checkout` hooks, this represents the branch that is '
'now checked out.' 'now checked out.'
), ),
) )
parser.add_argument(
'--pre-rebase-upstream', help=(
'The upstream from which the series was forked.'
),
)
parser.add_argument(
'--pre-rebase-branch', help=(
'The branch being rebased, and is not set when '
'rebasing the current branch.'
),
)
parser.add_argument( parser.add_argument(
'--commit-msg-filename', '--commit-msg-filename',
help='Filename to check when running during `commit-msg`', help='Filename to check when running during `commit-msg`',
) )
parser.add_argument(
'--prepare-commit-message-source',
help=(
'Source of the commit message '
'(typically the second argument to .git/hooks/prepare-commit-msg)'
),
)
parser.add_argument(
'--commit-object-name',
help=(
'Commit object name '
'(typically the third argument to .git/hooks/prepare-commit-msg)'
),
)
parser.add_argument( parser.add_argument(
'--remote-name', help='Remote name used by `git push`.', '--remote-name', help='Remote name used by `git push`.',
) )
@ -178,10 +162,6 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
args.config = os.path.abspath(args.config) args.config = os.path.abspath(args.config)
if args.command in {'run', 'try-repo'}: if args.command in {'run', 'try-repo'}:
args.files = [os.path.abspath(filename) for filename in args.files] args.files = [os.path.abspath(filename) for filename in args.files]
if args.commit_msg_filename is not None:
args.commit_msg_filename = os.path.abspath(
args.commit_msg_filename,
)
if args.command == 'try-repo' and os.path.exists(args.repo): if args.command == 'try-repo' and os.path.exists(args.repo):
args.repo = os.path.abspath(args.repo) args.repo = os.path.abspath(args.repo)
@ -191,15 +171,11 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
args.config = os.path.relpath(args.config) args.config = os.path.relpath(args.config)
if args.command in {'run', 'try-repo'}: if args.command in {'run', 'try-repo'}:
args.files = [os.path.relpath(filename) for filename in args.files] args.files = [os.path.relpath(filename) for filename in args.files]
if args.commit_msg_filename is not None:
args.commit_msg_filename = os.path.relpath(
args.commit_msg_filename,
)
if args.command == 'try-repo' and os.path.exists(args.repo): if args.command == 'try-repo' and os.path.exists(args.repo):
args.repo = os.path.relpath(args.repo) args.repo = os.path.relpath(args.repo)
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Optional[Sequence[str]] = None) -> int:
argv = argv if argv is not None else sys.argv[1:] argv = argv if argv is not None else sys.argv[1:]
parser = argparse.ArgumentParser(prog='pre-commit') parser = argparse.ArgumentParser(prog='pre-commit')
@ -212,20 +188,16 @@ def main(argv: Sequence[str] | None = None) -> int:
subparsers = parser.add_subparsers(dest='command') subparsers = parser.add_subparsers(dest='command')
def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: autoupdate_parser = subparsers.add_parser(
parser = subparsers.add_parser(name, help=help)
add_color_option(parser)
return parser
autoupdate_parser = _add_cmd(
'autoupdate', 'autoupdate',
help="Auto-update pre-commit config to the latest repos' versions.", help="Auto-update pre-commit config to the latest repos' versions.",
) )
add_color_option(autoupdate_parser)
_add_config_option(autoupdate_parser) _add_config_option(autoupdate_parser)
autoupdate_parser.add_argument( autoupdate_parser.add_argument(
'--bleeding-edge', action='store_true', '--bleeding-edge', action='store_true',
help=( help=(
'Update to the bleeding edge of `HEAD` instead of the latest ' 'Update to the bleeding edge of `master` instead of the latest '
'tagged version (the default behavior).' 'tagged version (the default behavior).'
), ),
) )
@ -234,30 +206,38 @@ def main(argv: Sequence[str] | None = None) -> int:
help='Store "frozen" hashes in `rev` instead of tag names', help='Store "frozen" hashes in `rev` instead of tag names',
) )
autoupdate_parser.add_argument( autoupdate_parser.add_argument(
'--repo', dest='repos', action='append', metavar='REPO', default=[], '--repo', dest='repos', action='append', metavar='REPO',
help='Only update this repository -- may be specified multiple times.', help='Only update this repository -- may be specified multiple times.',
) )
autoupdate_parser.add_argument(
'-j', '--jobs', type=int, default=1, clean_parser = subparsers.add_parser(
help='Number of threads to use. (default %(default)s).', 'clean', help='Clean out pre-commit files.',
) )
add_color_option(clean_parser)
_add_config_option(clean_parser)
_add_cmd('clean', help='Clean out pre-commit files.') hook_impl_parser = subparsers.add_parser('hook-impl')
add_color_option(hook_impl_parser)
_add_cmd('gc', help='Clean unused cached repos.') _add_config_option(hook_impl_parser)
hook_impl_parser.add_argument('--hook-type')
hazmat_parser = _add_cmd( hook_impl_parser.add_argument('--hook-dir')
'hazmat', help='Composable tools for rare use in hook `entry`.', hook_impl_parser.add_argument(
'--skip-on-missing-config', action='store_true',
) )
hazmat.add_parsers(hazmat_parser) hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER)
init_templatedir_parser = _add_cmd( gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.')
add_color_option(gc_parser)
_add_config_option(gc_parser)
init_templatedir_parser = subparsers.add_parser(
'init-templatedir', 'init-templatedir',
help=( help=(
'Install hook script in a directory intended for use with ' 'Install hook script in a directory intended for use with '
'`git config init.templateDir`.' '`git config init.templateDir`.'
), ),
) )
add_color_option(init_templatedir_parser)
_add_config_option(init_templatedir_parser) _add_config_option(init_templatedir_parser)
init_templatedir_parser.add_argument( init_templatedir_parser.add_argument(
'directory', help='The directory in which to write the hook script.', 'directory', help='The directory in which to write the hook script.',
@ -270,7 +250,10 @@ def main(argv: Sequence[str] | None = None) -> int:
) )
_add_hook_type_option(init_templatedir_parser) _add_hook_type_option(init_templatedir_parser)
install_parser = _add_cmd('install', help='Install the pre-commit script.') install_parser = subparsers.add_parser(
'install', help='Install the pre-commit script.',
)
add_color_option(install_parser)
_add_config_option(install_parser) _add_config_option(install_parser)
install_parser.add_argument( install_parser.add_argument(
'-f', '--overwrite', action='store_true', '-f', '--overwrite', action='store_true',
@ -285,14 +268,14 @@ def main(argv: Sequence[str] | None = None) -> int:
) )
_add_hook_type_option(install_parser) _add_hook_type_option(install_parser)
install_parser.add_argument( install_parser.add_argument(
'--allow-missing-config', action='store_true', '--allow-missing-config', action='store_true', default=False,
help=( help=(
'Whether to allow a missing `pre-commit` configuration file ' 'Whether to allow a missing `pre-commit` configuration file '
'or exit with a failure code.' 'or exit with a failure code.'
), ),
) )
install_hooks_parser = _add_cmd( install_hooks_parser = subparsers.add_parser(
'install-hooks', 'install-hooks',
help=( help=(
'Install hook environments for all environments in the config ' 'Install hook environments for all environments in the config '
@ -300,24 +283,32 @@ def main(argv: Sequence[str] | None = None) -> int:
'useful.' 'useful.'
), ),
) )
add_color_option(install_hooks_parser)
_add_config_option(install_hooks_parser) _add_config_option(install_hooks_parser)
migrate_config_parser = _add_cmd( migrate_config_parser = subparsers.add_parser(
'migrate-config', 'migrate-config',
help='Migrate list configuration to new map configuration.', help='Migrate list configuration to new map configuration.',
) )
add_color_option(migrate_config_parser)
_add_config_option(migrate_config_parser) _add_config_option(migrate_config_parser)
run_parser = _add_cmd('run', help='Run hooks.') run_parser = subparsers.add_parser('run', help='Run hooks.')
add_color_option(run_parser)
_add_config_option(run_parser) _add_config_option(run_parser)
_add_run_options(run_parser) _add_run_options(run_parser)
_add_cmd('sample-config', help=f'Produce a sample {C.CONFIG_FILE} file') sample_config_parser = subparsers.add_parser(
'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file',
)
add_color_option(sample_config_parser)
_add_config_option(sample_config_parser)
try_repo_parser = _add_cmd( try_repo_parser = subparsers.add_parser(
'try-repo', 'try-repo',
help='Try the hooks in a repository, useful for developing new hooks.', help='Try the hooks in a repository, useful for developing new hooks.',
) )
add_color_option(try_repo_parser)
_add_config_option(try_repo_parser) _add_config_option(try_repo_parser)
try_repo_parser.add_argument( try_repo_parser.add_argument(
'repo', help='Repository to source hooks from.', 'repo', help='Repository to source hooks from.',
@ -331,39 +322,18 @@ def main(argv: Sequence[str] | None = None) -> int:
) )
_add_run_options(try_repo_parser) _add_run_options(try_repo_parser)
uninstall_parser = _add_cmd( uninstall_parser = subparsers.add_parser(
'uninstall', help='Uninstall the pre-commit script.', 'uninstall', help='Uninstall the pre-commit script.',
) )
add_color_option(uninstall_parser)
_add_config_option(uninstall_parser) _add_config_option(uninstall_parser)
_add_hook_type_option(uninstall_parser) _add_hook_type_option(uninstall_parser)
validate_config_parser = _add_cmd(
'validate-config', help='Validate .pre-commit-config.yaml files',
)
validate_config_parser.add_argument('filenames', nargs='*')
validate_manifest_parser = _add_cmd(
'validate-manifest', help='Validate .pre-commit-hooks.yaml files',
)
validate_manifest_parser.add_argument('filenames', nargs='*')
# does not use `_add_cmd` because it doesn't use `--color`
help = subparsers.add_parser( help = subparsers.add_parser(
'help', help='Show help for a specific command.', 'help', help='Show help for a specific command.',
) )
help.add_argument('help_cmd', nargs='?', help='Command to show help for.') help.add_argument('help_cmd', nargs='?', help='Command to show help for.')
# not intended for users to call this directly
hook_impl_parser = subparsers.add_parser('hook-impl')
add_color_option(hook_impl_parser)
_add_config_option(hook_impl_parser)
hook_impl_parser.add_argument('--hook-type')
hook_impl_parser.add_argument('--hook-dir')
hook_impl_parser.add_argument(
'--skip-on-missing-config', action='store_true',
)
hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER)
# argparse doesn't really provide a way to use a `default` subparser # argparse doesn't really provide a way to use a `default` subparser
if len(argv) == 0: if len(argv) == 0:
argv = ['run'] argv = ['run']
@ -377,26 +347,23 @@ def main(argv: Sequence[str] | None = None) -> int:
with error_handler(), logging_handler(args.color): with error_handler(), logging_handler(args.color):
git.check_for_cygwin_mismatch() git.check_for_cygwin_mismatch()
store = Store()
if args.command not in COMMANDS_NO_GIT: if args.command not in COMMANDS_NO_GIT:
_adjust_args_and_chdir(args) _adjust_args_and_chdir(args)
store.mark_config_used(args.config)
store = Store()
store.mark_config_used(args.config)
if args.command == 'autoupdate': if args.command == 'autoupdate':
return autoupdate( return autoupdate(
args.config, args.config, store,
tags_only=not args.bleeding_edge, tags_only=not args.bleeding_edge,
freeze=args.freeze, freeze=args.freeze,
repos=args.repos, repos=args.repos,
jobs=args.jobs,
) )
elif args.command == 'clean': elif args.command == 'clean':
return clean(store) return clean(store)
elif args.command == 'gc': elif args.command == 'gc':
return gc(store) return gc(store)
elif args.command == 'hazmat':
return hazmat.impl(args)
elif args.command == 'hook-impl': elif args.command == 'hook-impl':
return hook_impl( return hook_impl(
store, store,
@ -432,14 +399,7 @@ def main(argv: Sequence[str] | None = None) -> int:
elif args.command == 'try-repo': elif args.command == 'try-repo':
return try_repo(args) return try_repo(args)
elif args.command == 'uninstall': elif args.command == 'uninstall':
return uninstall( return uninstall(hook_types=args.hook_types)
config_file=args.config,
hook_types=args.hook_types,
)
elif args.command == 'validate-config':
return validate_config(args.filenames)
elif args.command == 'validate-manifest':
return validate_manifest(args.filenames)
else: else:
raise NotImplementedError( raise NotImplementedError(
f'Command {args.command} not implemented.', f'Command {args.command} not implemented.',
@ -451,4 +411,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,7 +1,6 @@
from __future__ import annotations
import argparse import argparse
from collections.abc import Sequence from typing import Optional
from typing import Sequence
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import git from pre_commit import git
@ -21,14 +20,14 @@ def check_all_hooks_match_files(config_file: str) -> int:
for hook in all_hooks(config, Store()): for hook in all_hooks(config, Store()):
if hook.always_run or hook.language == 'fail': if hook.always_run or hook.language == 'fail':
continue continue
elif not any(classifier.filenames_for_hook(hook)): elif not classifier.filenames_for_hook(hook):
print(f'{hook.id} does not apply to this repository') print(f'{hook.id} does not apply to this repository')
retv = 1 retv = 1
return retv return retv
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -40,4 +39,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,9 +1,7 @@
from __future__ import annotations
import argparse import argparse
import re import re
from collections.abc import Iterable from typing import Optional
from collections.abc import Sequence from typing import Sequence
from cfgv import apply_defaults from cfgv import apply_defaults
@ -15,7 +13,7 @@ from pre_commit.commands.run import Classifier
def exclude_matches_any( def exclude_matches_any(
filenames: Iterable[str], filenames: Sequence[str],
include: str, include: str,
exclude: str, exclude: str,
) -> bool: ) -> bool:
@ -51,12 +49,11 @@ def check_useless_excludes(config_file: str) -> int:
# Not actually a manifest dict, but this more accurately reflects # Not actually a manifest dict, but this more accurately reflects
# the defaults applied during runtime # the defaults applied during runtime
hook = apply_defaults(hook, MANIFEST_HOOK_DICT) hook = apply_defaults(hook, MANIFEST_HOOK_DICT)
names = classifier.by_types( names = classifier.filenames
classifier.filenames, types = hook['types']
hook['types'], types_or = hook['types_or']
hook['types_or'], exclude_types = hook['exclude_types']
hook['exclude_types'], names = classifier.by_types(names, types, types_or, exclude_types)
)
include, exclude = hook['files'], hook['exclude'] include, exclude = hook['files'], hook['exclude']
if not exclude_matches_any(names, include, exclude): if not exclude_matches_any(names, include, exclude):
print( print(
@ -68,7 +65,7 @@ def check_useless_excludes(config_file: str) -> int:
return retv return retv
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -80,4 +77,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,12 +1,11 @@
from __future__ import annotations
import sys import sys
from collections.abc import Sequence from typing import Optional
from typing import Sequence
from pre_commit import output from pre_commit import output
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Optional[Sequence[str]] = None) -> int:
argv = argv if argv is not None else sys.argv[1:] argv = argv if argv is not None else sys.argv[1:]
for arg in argv: for arg in argv:
output.write_line(arg) output.write_line(arg)
@ -14,4 +13,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,9 +1,8 @@
from __future__ import annotations
import contextlib import contextlib
import sys import sys
from typing import Any from typing import Any
from typing import IO from typing import IO
from typing import Optional
def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None:
@ -12,9 +11,9 @@ def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None:
def write_line_b( def write_line_b(
s: bytes | None = None, s: Optional[bytes] = None,
stream: IO[bytes] = sys.stdout.buffer, stream: IO[bytes] = sys.stdout.buffer,
logfile_name: str | None = None, logfile_name: Optional[str] = None,
) -> None: ) -> None:
with contextlib.ExitStack() as exit_stack: with contextlib.ExitStack() as exit_stack:
output_streams = [stream] output_streams = [stream]
@ -29,5 +28,5 @@ def write_line_b(
output_stream.flush() output_stream.flush()
def write_line(s: str | None = None, **kwargs: Any) -> None: def write_line(s: Optional[str] = None, **kwargs: Any) -> None:
write_line_b(s.encode() if s is not None else s, **kwargs) write_line_b(s.encode() if s is not None else s, **kwargs)

View file

@ -1,18 +1,21 @@
from __future__ import annotations
import os.path import os.path
from collections.abc import Mapping from typing import Mapping
from typing import NoReturn from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from identify.identify import parse_shebang_from_file from identify.identify import parse_shebang_from_file
if TYPE_CHECKING:
from typing import NoReturn
class ExecutableNotFoundError(OSError): class ExecutableNotFoundError(OSError):
def to_output(self) -> tuple[int, bytes, None]: def to_output(self) -> Tuple[int, bytes, None]:
return (1, self.args[0].encode(), None) return (1, self.args[0].encode(), None)
def parse_filename(filename: str) -> tuple[str, ...]: def parse_filename(filename: str) -> Tuple[str, ...]:
if not os.path.exists(filename): if not os.path.exists(filename):
return () return ()
else: else:
@ -20,13 +23,13 @@ def parse_filename(filename: str) -> tuple[str, ...]:
def find_executable( def find_executable(
exe: str, *, env: Mapping[str, str] | None = None, exe: str, _environ: Optional[Mapping[str, str]] = None,
) -> str | None: ) -> Optional[str]:
exe = os.path.normpath(exe) exe = os.path.normpath(exe)
if os.sep in exe: if os.sep in exe:
return exe return exe
environ = env if env is not None else os.environ environ = _environ if _environ is not None else os.environ
if 'PATHEXT' in environ: if 'PATHEXT' in environ:
exts = environ['PATHEXT'].split(os.pathsep) exts = environ['PATHEXT'].split(os.pathsep)
@ -43,12 +46,12 @@ def find_executable(
return None return None
def normexe(orig: str, *, env: Mapping[str, str] | None = None) -> str: def normexe(orig: str) -> str:
def _error(msg: str) -> NoReturn: def _error(msg: str) -> 'NoReturn':
raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') raise ExecutableNotFoundError(f'Executable `{orig}` {msg}')
if os.sep not in orig and (not os.altsep or os.altsep not in orig): if os.sep not in orig and (not os.altsep or os.altsep not in orig):
exe = find_executable(orig, env=env) exe = find_executable(orig)
if exe is None: if exe is None:
_error('not found') _error('not found')
return exe return exe
@ -62,11 +65,7 @@ def normexe(orig: str, *, env: Mapping[str, str] | None = None) -> str:
return orig return orig
def normalize_cmd( def normalize_cmd(cmd: Tuple[str, ...]) -> Tuple[str, ...]:
cmd: tuple[str, ...],
*,
env: Mapping[str, str] | None = None,
) -> tuple[str, ...]:
"""Fixes for the following issues on windows """Fixes for the following issues on windows
- https://bugs.python.org/issue8557 - https://bugs.python.org/issue8557
- windows does not parse shebangs - windows does not parse shebangs
@ -74,12 +73,12 @@ def normalize_cmd(
This function also makes deep-path shebangs work just fine This function also makes deep-path shebangs work just fine
""" """
# Use PATH to determine the executable # Use PATH to determine the executable
exe = normexe(cmd[0], env=env) exe = normexe(cmd[0])
# Figure out the shebang from the resulting command # Figure out the shebang from the resulting command
cmd = parse_filename(exe) + (exe,) + cmd[1:] cmd = parse_filename(exe) + (exe,) + cmd[1:]
# This could have given us back another bare executable # This could have given us back another bare executable
exe = normexe(cmd[0], env=env) exe = normexe(cmd[0])
return (exe,) + cmd[1:] return (exe,) + cmd[1:]

View file

@ -1,7 +1,6 @@
from __future__ import annotations
import os.path import os.path
from typing import NamedTuple from typing import NamedTuple
from typing import Tuple
class Prefix(NamedTuple): class Prefix(NamedTuple):
@ -13,6 +12,6 @@ class Prefix(NamedTuple):
def exists(self, *parts: str) -> bool: def exists(self, *parts: str) -> bool:
return os.path.exists(self.path(*parts)) return os.path.exists(self.path(*parts))
def star(self, end: str) -> tuple[str, ...]: def star(self, end: str) -> Tuple[str, ...]:
paths = os.listdir(self.prefix_dir) paths = os.listdir(self.prefix_dir)
return tuple(path for path in paths if path.endswith(end)) return tuple(path for path in paths if path.endswith(end))

View file

@ -1,41 +1,40 @@
from __future__ import annotations
import json import json
import logging import logging
import os import os
from collections.abc import Sequence
from typing import Any from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit.all_languages import languages
from pre_commit.clientlib import load_manifest from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META from pre_commit.clientlib import META
from pre_commit.hook import Hook from pre_commit.hook import Hook
from pre_commit.lang_base import environment_dir from pre_commit.languages.all import languages
from pre_commit.languages.helpers import environment_dir
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.store import Store from pre_commit.store import Store
from pre_commit.util import clean_path_on_failure from pre_commit.util import parse_version
from pre_commit.util import rmtree from pre_commit.util import rmtree
logger = logging.getLogger('pre_commit') logger = logging.getLogger('pre_commit')
def _state_filename_v1(venv: str) -> str:
return os.path.join(venv, '.install_state_v1')
def _state_filename_v2(venv: str) -> str:
return os.path.join(venv, '.install_state_v2')
def _state(additional_deps: Sequence[str]) -> object: def _state(additional_deps: Sequence[str]) -> object:
return {'additional_dependencies': additional_deps} return {'additional_dependencies': sorted(additional_deps)}
def _read_state(venv: str) -> object | None: def _state_filename(prefix: Prefix, venv: str) -> str:
filename = _state_filename_v1(venv) return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}')
def _read_state(prefix: Prefix, venv: str) -> Optional[object]:
filename = _state_filename(prefix, venv)
if not os.path.exists(filename): if not os.path.exists(filename):
return None return None
else: else:
@ -43,22 +42,26 @@ def _read_state(venv: str) -> object | None:
return json.load(f) return json.load(f)
def _write_state(prefix: Prefix, venv: str, state: object) -> None:
state_filename = _state_filename(prefix, venv)
staging = f'{state_filename}staging'
with open(staging, 'w') as state_file:
state_file.write(json.dumps(state))
# Move the file into place atomically to indicate we've installed
os.replace(staging, state_filename)
def _hook_installed(hook: Hook) -> bool: def _hook_installed(hook: Hook) -> bool:
lang = languages[hook.language] lang = languages[hook.language]
if lang.ENVIRONMENT_DIR is None: venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version)
return True
venv = environment_dir(
hook.prefix,
lang.ENVIRONMENT_DIR,
hook.language_version,
)
return ( return (
( venv is None or (
os.path.exists(_state_filename_v2(venv)) or (
_read_state(venv) == _state(hook.additional_dependencies) _read_state(hook.prefix, venv) ==
) and _state(hook.additional_dependencies)
not lang.health_check(hook.prefix, hook.language_version) ) and
lang.healthy(hook.prefix, hook.language_version)
)
) )
@ -69,51 +72,43 @@ def _hook_install(hook: Hook) -> None:
lang = languages[hook.language] lang = languages[hook.language]
assert lang.ENVIRONMENT_DIR is not None assert lang.ENVIRONMENT_DIR is not None
venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version)
venv = environment_dir(
hook.prefix,
lang.ENVIRONMENT_DIR,
hook.language_version,
)
# There's potentially incomplete cleanup from previous runs # There's potentially incomplete cleanup from previous runs
# Clean it up! # Clean it up!
if os.path.exists(venv): if hook.prefix.exists(venv):
rmtree(venv) rmtree(hook.prefix.path(venv))
with clean_path_on_failure(venv): lang.install_environment(
lang.install_environment( hook.prefix, hook.language_version, hook.additional_dependencies,
hook.prefix, hook.language_version, hook.additional_dependencies, )
if not lang.healthy(hook.prefix, hook.language_version):
raise AssertionError(
f'BUG: expected environment for {hook.language} to be healthy() '
f'immediately after install, please open an issue describing '
f'your environment',
) )
health_error = lang.health_check(hook.prefix, hook.language_version) # Write our state to indicate we're installed
if health_error: _write_state(hook.prefix, venv, _state(hook.additional_dependencies))
raise AssertionError(
f'BUG: expected environment for {hook.language} to be healthy '
f'immediately after install, please open an issue describing '
f'your environment\n\n'
f'more info:\n\n{health_error}',
)
# TODO: remove v1 state writing, no longer needed after pre-commit 3.0
# Write our state to indicate we're installed
state_filename = _state_filename_v1(venv)
staging = f'{state_filename}staging'
with open(staging, 'w') as state_file:
state_file.write(json.dumps(_state(hook.additional_dependencies)))
# Move the file into place atomically to indicate we've installed
os.replace(staging, state_filename)
open(_state_filename_v2(venv), 'a+').close()
def _hook( def _hook(
*hook_dicts: dict[str, Any], *hook_dicts: Dict[str, Any],
root_config: dict[str, Any], root_config: Dict[str, Any],
) -> dict[str, Any]: ) -> Dict[str, Any]:
ret, rest = dict(hook_dicts[0]), hook_dicts[1:] ret, rest = dict(hook_dicts[0]), hook_dicts[1:]
for dct in rest: for dct in rest:
ret.update(dct) ret.update(dct)
version = ret['minimum_pre_commit_version']
if parse_version(version) > parse_version(C.VERSION):
logger.error(
f'The hook `{ret["id"]}` requires pre-commit version {version} '
f'but version {C.VERSION} is installed. '
f'Perhaps run `pip install --upgrade pre-commit`.',
)
exit(1)
lang = ret['language'] lang = ret['language']
if ret['language_version'] == C.DEFAULT: if ret['language_version'] == C.DEFAULT:
ret['language_version'] = root_config['default_language_version'][lang] ret['language_version'] = root_config['default_language_version'][lang]
@ -145,10 +140,10 @@ def _hook(
def _non_cloned_repository_hooks( def _non_cloned_repository_hooks(
repo_config: dict[str, Any], repo_config: Dict[str, Any],
store: Store, store: Store,
root_config: dict[str, Any], root_config: Dict[str, Any],
) -> tuple[Hook, ...]: ) -> Tuple[Hook, ...]:
def _prefix(language_name: str, deps: Sequence[str]) -> Prefix: def _prefix(language_name: str, deps: Sequence[str]) -> Prefix:
language = languages[language_name] language = languages[language_name]
# pygrep / script / system / docker_image do not have # pygrep / script / system / docker_image do not have
@ -169,10 +164,10 @@ def _non_cloned_repository_hooks(
def _cloned_repository_hooks( def _cloned_repository_hooks(
repo_config: dict[str, Any], repo_config: Dict[str, Any],
store: Store, store: Store,
root_config: dict[str, Any], root_config: Dict[str, Any],
) -> tuple[Hook, ...]: ) -> Tuple[Hook, ...]:
repo, rev = repo_config['repo'], repo_config['rev'] repo, rev = repo_config['repo'], repo_config['rev']
manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE)
by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} by_id = {hook['id']: hook for hook in load_manifest(manifest_path)}
@ -201,10 +196,10 @@ def _cloned_repository_hooks(
def _repository_hooks( def _repository_hooks(
repo_config: dict[str, Any], repo_config: Dict[str, Any],
store: Store, store: Store,
root_config: dict[str, Any], root_config: Dict[str, Any],
) -> tuple[Hook, ...]: ) -> Tuple[Hook, ...]:
if repo_config['repo'] in {LOCAL, META}: if repo_config['repo'] in {LOCAL, META}:
return _non_cloned_repository_hooks(repo_config, store, root_config) return _non_cloned_repository_hooks(repo_config, store, root_config)
else: else:
@ -212,8 +207,8 @@ def _repository_hooks(
def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None:
def _need_installed() -> list[Hook]: def _need_installed() -> List[Hook]:
seen: set[tuple[Prefix, str, str, tuple[str, ...]]] = set() seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set()
ret = [] ret = []
for hook in hooks: for hook in hooks:
if hook.install_key not in seen and not _hook_installed(hook): if hook.install_key not in seen and not _hook_installed(hook):
@ -229,7 +224,7 @@ def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None:
_hook_install(hook) _hook_install(hook)
def all_hooks(root_config: dict[str, Any], store: Store) -> tuple[Hook, ...]: def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]:
return tuple( return tuple(
hook hook
for repo in root_config['repos'] for repo in root_config['repos']

View file

@ -1,12 +0,0 @@
package = "pre-commit-package"
version = "dev-1"
source = {
url = "git+ssh://git@github.com/pre-commit/pre-commit.git"
}
description = {}
dependencies = {}
build = {
type = "builtin",
modules = {},
}

View file

@ -1,4 +1,4 @@
name: pre_commit_empty_pubspec name: pre_commit_empty_pubspec
environment: environment:
sdk: '>=2.12.0' sdk: '>=2.10.0'
executables: {} executables: {}

View file

@ -1,4 +1,4 @@
from setuptools import setup from setuptools import setup
setup(name='pre-commit-placeholder-package', version='0.0.0', py_modules=[]) setup(name='pre-commit-placeholder-package', version='0.0.0')

View file

@ -1,20 +1,44 @@
#!/usr/bin/env bash #!/usr/bin/env python3
# File generated by pre-commit: https://pre-commit.com # File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03 # ID: 138fd403232d2ddd5efb44317e38bf03
import os
import sys
# we try our best, but the shebang of this script is difficult to determine:
# - macos doesn't ship with python3
# - windows executables are almost always `python.exe`
# therefore we continue to support python2 for this small script
if sys.version_info < (3, 3):
from distutils.spawn import find_executable as which
else:
from shutil import which
# work around https://github.com/Homebrew/homebrew-core/issues/30445
os.environ.pop('__PYVENV_LAUNCHER__', None)
# start templated # start templated
INSTALL_PYTHON='' INSTALL_PYTHON = ''
ARGS=(hook-impl) ARGS = ['hook-impl']
# end templated # end templated
ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__))))
ARGS.append('--')
ARGS.extend(sys.argv[1:])
HERE="$(cd "$(dirname "$0")" && pwd)" DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?'
ARGS+=(--hook-dir "$HERE" -- "$@") if os.access(INSTALL_PYTHON, os.X_OK):
CMD = [INSTALL_PYTHON, '-mpre_commit']
elif which('pre-commit'):
CMD = ['pre-commit']
else:
raise SystemExit(DNE)
if [ -x "$INSTALL_PYTHON" ]; then CMD.extend(ARGS)
exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}" if sys.platform == 'win32': # https://bugs.python.org/issue19124
elif command -v pre-commit > /dev/null; then import subprocess
exec pre-commit "${ARGS[@]}"
else if sys.version_info < (3, 7): # https://bugs.python.org/issue25942
echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 raise SystemExit(subprocess.Popen(CMD).wait())
exit 1 else:
fi raise SystemExit(subprocess.call(CMD))
else:
os.execvp(CMD[0], CMD)

Binary file not shown.

View file

@ -1,13 +1,10 @@
from __future__ import annotations
import contextlib import contextlib
import logging import logging
import os.path import os.path
import time import time
from collections.abc import Generator from typing import Generator
from pre_commit import git from pre_commit import git
from pre_commit.errors import FatalError
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
@ -16,12 +13,6 @@ from pre_commit.xargs import xargs
logger = logging.getLogger('pre_commit') logger = logging.getLogger('pre_commit')
# without forcing submodule.recurse=0, changes in nested submodules will be
# discarded if `submodule.recurse=1` is configured
# we choose this instead of `--no-recurse-submodules` because it works on
# versions of git before that option was added to `git checkout`
_CHECKOUT_CMD = ('git', '-c', 'submodule.recurse=0', 'checkout', '--', '.')
def _git_apply(patch: str) -> None: def _git_apply(patch: str) -> None:
args = ('apply', '--whitespace=nowarn', patch) args = ('apply', '--whitespace=nowarn', patch)
@ -33,7 +24,7 @@ def _git_apply(patch: str) -> None:
@contextlib.contextmanager @contextlib.contextmanager
def _intent_to_add_cleared() -> Generator[None]: def _intent_to_add_cleared() -> Generator[None, None, None]:
intent_to_add = git.intent_to_add_files() intent_to_add = git.intent_to_add_files()
if intent_to_add: if intent_to_add:
logger.warning('Unstaged intent-to-add files detected.') logger.warning('Unstaged intent-to-add files detected.')
@ -48,23 +39,14 @@ def _intent_to_add_cleared() -> Generator[None]:
@contextlib.contextmanager @contextlib.contextmanager
def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]: def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
tree = cmd_output('git', 'write-tree')[1].strip() tree = cmd_output('git', 'write-tree')[1].strip()
diff_cmd = ( retcode, diff_stdout_binary, _ = cmd_output_b(
'git', 'diff-index', '--ignore-submodules', '--binary', 'git', 'diff-index', '--ignore-submodules', '--binary',
'--exit-code', '--no-color', '--no-ext-diff', tree, '--', '--exit-code', '--no-color', '--no-ext-diff', tree, '--',
retcode=None,
) )
retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False) if retcode and diff_stdout_binary.strip():
if retcode == 0:
# There weren't any staged files so we don't need to do anything
# special
yield
elif retcode == 1 and not diff_stdout.strip():
# due to behaviour (probably a bug?) in git with crlf endings and
# autocrlf set to either `true` or `input` sometimes git will refuse
# to show a crlf-only diff to us :(
yield
elif retcode == 1 and diff_stdout.strip():
patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = f'patch{int(time.time())}-{os.getpid()}'
patch_filename = os.path.join(patch_dir, patch_filename) patch_filename = os.path.join(patch_dir, patch_filename)
logger.warning('Unstaged files detected.') logger.warning('Unstaged files detected.')
@ -72,13 +54,13 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
# Save the current unstaged changes as a patch # Save the current unstaged changes as a patch
os.makedirs(patch_dir, exist_ok=True) os.makedirs(patch_dir, exist_ok=True)
with open(patch_filename, 'wb') as patch_file: with open(patch_filename, 'wb') as patch_file:
patch_file.write(diff_stdout) patch_file.write(diff_stdout_binary)
# prevent recursive post-checkout hooks (#1418) # prevent recursive post-checkout hooks (#1418)
no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')
cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env)
try: try:
cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)
yield yield
finally: finally:
# Try to apply the patch we saved # Try to apply the patch we saved
@ -92,20 +74,18 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
# We failed to apply the patch, presumably due to fixes made # We failed to apply the patch, presumably due to fixes made
# by hooks. # by hooks.
# Roll back the changes made by hooks. # Roll back the changes made by hooks.
cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env)
_git_apply(patch_filename) _git_apply(patch_filename)
logger.info(f'Restored changes from {patch_filename}.') logger.info(f'Restored changes from {patch_filename}.')
else: # pragma: win32 no cover else:
# some error occurred while requesting the diff # There weren't any staged files so we don't need to do anything
e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr) # special
raise FatalError( yield
f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}',
)
@contextlib.contextmanager @contextlib.contextmanager
def staged_files_only(patch_dir: str) -> Generator[None]: def staged_files_only(patch_dir: str) -> Generator[None, None, None]:
"""Clear any unstaged changes from the git working directory inside this """Clear any unstaged changes from the git working directory inside this
context. context.
""" """

View file

@ -1,22 +1,23 @@
from __future__ import annotations
import contextlib import contextlib
import logging import logging
import os.path import os.path
import sqlite3 import sqlite3
import tempfile import tempfile
from collections.abc import Callable from typing import Callable
from collections.abc import Generator from typing import Generator
from collections.abc import Sequence from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import clientlib
from pre_commit import file_lock from pre_commit import file_lock
from pre_commit import git from pre_commit import git
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
from pre_commit.util import clean_path_on_failure from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.util import resource_text from pre_commit.util import resource_text
from pre_commit.util import rmtree
logger = logging.getLogger('pre_commit') logger = logging.getLogger('pre_commit')
@ -36,30 +37,10 @@ def _get_default_directory() -> str:
return os.path.realpath(ret) return os.path.realpath(ret)
_LOCAL_RESOURCES = (
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
'package.json', 'pre-commit-package-dev-1.rockspec',
'pre_commit_placeholder_package.gemspec', 'setup.py',
'environment.yml', 'Makefile.PL', 'pubspec.yaml',
'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv',
)
def _make_local_repo(directory: str) -> None:
for resource in _LOCAL_RESOURCES:
resource_dirname, resource_basename = os.path.split(resource)
contents = resource_text(f'empty_template_{resource_basename}')
target_dir = os.path.join(directory, resource_dirname)
target_file = os.path.join(target_dir, resource_basename)
os.makedirs(target_dir, exist_ok=True)
with open(target_file, 'w') as f:
f.write(contents)
class Store: class Store:
get_default_directory = staticmethod(_get_default_directory) get_default_directory = staticmethod(_get_default_directory)
def __init__(self, directory: str | None = None) -> None: def __init__(self, directory: Optional[str] = None) -> None:
self.directory = directory or Store.get_default_directory() self.directory = directory or Store.get_default_directory()
self.db_path = os.path.join(self.directory, 'db.db') self.db_path = os.path.join(self.directory, 'db.db')
self.readonly = ( self.readonly = (
@ -95,13 +76,13 @@ class Store:
' PRIMARY KEY (repo, ref)' ' PRIMARY KEY (repo, ref)'
');', ');',
) )
self._create_configs_table(db) self._create_config_table(db)
# Atomic file move # Atomic file move
os.replace(tmpfile, self.db_path) os.replace(tmpfile, self.db_path)
@contextlib.contextmanager @contextlib.contextmanager
def exclusive_lock(self) -> Generator[None]: def exclusive_lock(self) -> Generator[None, None, None]:
def blocked_cb() -> None: # pragma: no cover (tests are in-process) def blocked_cb() -> None: # pragma: no cover (tests are in-process)
logger.info('Locking pre-commit directory') logger.info('Locking pre-commit directory')
@ -111,8 +92,8 @@ class Store:
@contextlib.contextmanager @contextlib.contextmanager
def connect( def connect(
self, self,
db_path: str | None = None, db_path: Optional[str] = None,
) -> Generator[sqlite3.Connection]: ) -> Generator[sqlite3.Connection, None, None]:
db_path = db_path or self.db_path db_path = db_path or self.db_path
# sqlite doesn't close its fd with its contextmanager >.< # sqlite doesn't close its fd with its contextmanager >.<
# contextlib.closing fixes this. # contextlib.closing fixes this.
@ -125,7 +106,7 @@ class Store:
@classmethod @classmethod
def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str:
if deps: if deps:
return f'{repo}:{",".join(deps)}' return f'{repo}:{",".join(sorted(deps))}'
else: else:
return repo return repo
@ -136,10 +117,9 @@ class Store:
deps: Sequence[str], deps: Sequence[str],
make_strategy: Callable[[str], None], make_strategy: Callable[[str], None],
) -> str: ) -> str:
original_repo = repo
repo = self.db_repo_name(repo, deps) repo = self.db_repo_name(repo, deps)
def _get_result() -> str | None: def _get_result() -> Optional[str]:
# Check if we already exist # Check if we already exist
with self.connect() as db: with self.connect() as db:
result = db.execute( result = db.execute(
@ -169,9 +149,6 @@ class Store:
'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',
[repo, ref, directory], [repo, ref, directory],
) )
clientlib.warn_for_stages_on_repo_init(original_repo, directory)
return directory return directory
def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None: def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
@ -209,12 +186,39 @@ class Store:
return self._new_repo(repo, ref, deps, clone_strategy) return self._new_repo(repo, ref, deps, clone_strategy)
LOCAL_RESOURCES = (
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py',
'environment.yml', 'Makefile.PL', 'pubspec.yaml',
'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv',
)
def make_local(self, deps: Sequence[str]) -> str: def make_local(self, deps: Sequence[str]) -> str:
def make_local_strategy(directory: str) -> None:
for resource in self.LOCAL_RESOURCES:
resource_dirname, resource_basename = os.path.split(resource)
contents = resource_text(f'empty_template_{resource_basename}')
target_dir = os.path.join(directory, resource_dirname)
target_file = os.path.join(target_dir, resource_basename)
os.makedirs(target_dir, exist_ok=True)
with open(target_file, 'w') as f:
f.write(contents)
env = git.no_git_env()
# initialize the git repository so it looks more like cloned repos
def _git_cmd(*args: str) -> None:
cmd_output_b('git', *args, cwd=directory, env=env)
git.init_repo(directory, '<<unknown>>')
_git_cmd('add', '.')
git.commit(repo=directory)
return self._new_repo( return self._new_repo(
'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo, 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy,
) )
def _create_configs_table(self, db: sqlite3.Connection) -> None: def _create_config_table(self, db: sqlite3.Connection) -> None:
db.executescript( db.executescript(
'CREATE TABLE IF NOT EXISTS configs (' 'CREATE TABLE IF NOT EXISTS configs ('
' path TEXT NOT NULL,' ' path TEXT NOT NULL,'
@ -231,5 +235,28 @@ class Store:
return return
with self.connect() as db: with self.connect() as db:
# TODO: eventually remove this and only create in _create # TODO: eventually remove this and only create in _create
self._create_configs_table(db) self._create_config_table(db)
db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
def select_all_configs(self) -> List[str]:
with self.connect() as db:
self._create_config_table(db)
rows = db.execute('SELECT path FROM configs').fetchall()
return [path for path, in rows]
def delete_configs(self, configs: List[str]) -> None:
with self.connect() as db:
rows = [(path,) for path in configs]
db.executemany('DELETE FROM configs WHERE path = ?', rows)
def select_all_repos(self) -> List[Tuple[str, str, str]]:
with self.connect() as db:
return db.execute('SELECT repo, ref, path from repos').fetchall()
def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None:
with self.connect() as db:
db.execute(
'DELETE FROM repos WHERE repo = ? and ref = ?',
(db_repo_name, ref),
)
rmtree(path)

View file

@ -1,20 +1,45 @@
from __future__ import annotations
import contextlib import contextlib
import errno import errno
import importlib.resources import functools
import os.path import os.path
import shutil import shutil
import stat import stat
import subprocess import subprocess
import sys import sys
from collections.abc import Callable import tempfile
from collections.abc import Generator
from types import TracebackType from types import TracebackType
from typing import Any from typing import Any
from typing import Callable
from typing import Dict
from typing import Generator
from typing import IO
from typing import Optional
from typing import Tuple
from typing import Type
import yaml
from pre_commit import parse_shebang from pre_commit import parse_shebang
if sys.version_info >= (3, 7): # pragma: no cover (PY37+)
from importlib.resources import open_binary
from importlib.resources import read_text
else: # pragma: no cover (<PY37)
from importlib_resources import open_binary
from importlib_resources import read_text
Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader)
yaml_load = functools.partial(yaml.load, Loader=Loader)
Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper)
def yaml_dump(o: Any, **kwargs: Any) -> str:
# when python/mypy#1484 is solved, this can be `functools.partial`
return yaml.dump(
o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False,
**kwargs,
)
def force_bytes(exc: Any) -> bytes: def force_bytes(exc: Any) -> bytes:
with contextlib.suppress(TypeError): with contextlib.suppress(TypeError):
@ -25,7 +50,7 @@ def force_bytes(exc: Any) -> bytes:
@contextlib.contextmanager @contextlib.contextmanager
def clean_path_on_failure(path: str) -> Generator[None]: def clean_path_on_failure(path: str) -> Generator[None, None, None]:
"""Cleans up the directory on an exceptional failure.""" """Cleans up the directory on an exceptional failure."""
try: try:
yield yield
@ -35,9 +60,24 @@ def clean_path_on_failure(path: str) -> Generator[None]:
raise raise
@contextlib.contextmanager
def tmpdir() -> Generator[str, None, None]:
"""Contextmanager to create a temporary directory. It will be cleaned up
afterwards.
"""
tempdir = tempfile.mkdtemp()
try:
yield tempdir
finally:
rmtree(tempdir)
def resource_bytesio(filename: str) -> IO[bytes]:
return open_binary('pre_commit.resources', filename)
def resource_text(filename: str) -> str: def resource_text(filename: str) -> str:
files = importlib.resources.files('pre_commit.resources') return read_text('pre_commit.resources', filename)
return files.joinpath(filename).read_text()
def make_executable(filename: str) -> None: def make_executable(filename: str) -> None:
@ -50,26 +90,29 @@ class CalledProcessError(RuntimeError):
def __init__( def __init__(
self, self,
returncode: int, returncode: int,
cmd: tuple[str, ...], cmd: Tuple[str, ...],
expected_returncode: int,
stdout: bytes, stdout: bytes,
stderr: bytes | None, stderr: Optional[bytes],
) -> None: ) -> None:
super().__init__(returncode, cmd, stdout, stderr) super().__init__(returncode, cmd, expected_returncode, stdout, stderr)
self.returncode = returncode self.returncode = returncode
self.cmd = cmd self.cmd = cmd
self.expected_returncode = expected_returncode
self.stdout = stdout self.stdout = stdout
self.stderr = stderr self.stderr = stderr
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
def _indent_or_none(part: bytes | None) -> bytes: def _indent_or_none(part: Optional[bytes]) -> bytes:
if part: if part:
return b'\n ' + part.replace(b'\n', b'\n ').rstrip() return b'\n ' + part.replace(b'\n', b'\n ')
else: else:
return b' (none)' return b' (none)'
return b''.join(( return b''.join((
f'command: {self.cmd!r}\n'.encode(), f'command: {self.cmd!r}\n'.encode(),
f'return code: {self.returncode}\n'.encode(), f'return code: {self.returncode}\n'.encode(),
f'expected return code: {self.expected_returncode}\n'.encode(),
b'stdout:', _indent_or_none(self.stdout), b'\n', b'stdout:', _indent_or_none(self.stdout), b'\n',
b'stderr:', _indent_or_none(self.stderr), b'stderr:', _indent_or_none(self.stderr),
)) ))
@ -78,24 +121,24 @@ class CalledProcessError(RuntimeError):
return self.__bytes__().decode() return self.__bytes__().decode()
def _setdefault_kwargs(kwargs: dict[str, Any]) -> None: def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None:
for arg in ('stdin', 'stdout', 'stderr'): for arg in ('stdin', 'stdout', 'stderr'):
kwargs.setdefault(arg, subprocess.PIPE) kwargs.setdefault(arg, subprocess.PIPE)
def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]: def _oserror_to_output(e: OSError) -> Tuple[int, bytes, None]:
return 1, force_bytes(e).rstrip(b'\n') + b'\n', None return 1, force_bytes(e).rstrip(b'\n') + b'\n', None
def cmd_output_b( def cmd_output_b(
*cmd: str, *cmd: str,
check: bool = True, retcode: Optional[int] = 0,
**kwargs: Any, **kwargs: Any,
) -> tuple[int, bytes, bytes | None]: ) -> Tuple[int, bytes, Optional[bytes]]:
_setdefault_kwargs(kwargs) _setdefault_kwargs(kwargs)
try: try:
cmd = parse_shebang.normalize_cmd(cmd, env=kwargs.get('env')) cmd = parse_shebang.normalize_cmd(cmd)
except parse_shebang.ExecutableNotFoundError as e: except parse_shebang.ExecutableNotFoundError as e:
returncode, stdout_b, stderr_b = e.to_output() returncode, stdout_b, stderr_b = e.to_output()
else: else:
@ -107,36 +150,36 @@ def cmd_output_b(
stdout_b, stderr_b = proc.communicate() stdout_b, stderr_b = proc.communicate()
returncode = proc.returncode returncode = proc.returncode
if check and returncode: if retcode is not None and retcode != returncode:
raise CalledProcessError(returncode, cmd, stdout_b, stderr_b) raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b)
return returncode, stdout_b, stderr_b return returncode, stdout_b, stderr_b
def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]: def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]:
returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs)
stdout = stdout_b.decode() if stdout_b is not None else None stdout = stdout_b.decode() if stdout_b is not None else None
stderr = stderr_b.decode() if stderr_b is not None else None stderr = stderr_b.decode() if stderr_b is not None else None
return returncode, stdout, stderr return returncode, stdout, stderr
if sys.platform != 'win32': # pragma: win32 no cover if os.name != 'nt': # pragma: win32 no cover
from os import openpty from os import openpty
import termios import termios
class Pty: class Pty:
def __init__(self) -> None: def __init__(self) -> None:
self.r: int | None = None self.r: Optional[int] = None
self.w: int | None = None self.w: Optional[int] = None
def __enter__(self) -> Pty: def __enter__(self) -> 'Pty':
self.r, self.w = openpty() self.r, self.w = openpty()
# tty flags normally change \n to \r\n # tty flags normally change \n to \r\n
attrs = termios.tcgetattr(self.w) attrs = termios.tcgetattr(self.r)
assert isinstance(attrs[1], int) assert isinstance(attrs[1], int)
attrs[1] &= ~(termios.ONLCR | termios.OPOST) attrs[1] &= ~(termios.ONLCR | termios.OPOST)
termios.tcsetattr(self.w, termios.TCSANOW, attrs) termios.tcsetattr(self.r, termios.TCSANOW, attrs)
return self return self
@ -152,19 +195,19 @@ if sys.platform != 'win32': # pragma: win32 no cover
def __exit__( def __exit__(
self, self,
exc_type: type[BaseException] | None, exc_type: Optional[Type[BaseException]],
exc_value: BaseException | None, exc_value: Optional[BaseException],
traceback: TracebackType | None, traceback: Optional[TracebackType],
) -> None: ) -> None:
self.close_w() self.close_w()
self.close_r() self.close_r()
def cmd_output_p( def cmd_output_p(
*cmd: str, *cmd: str,
check: bool = True, retcode: Optional[int] = 0,
**kwargs: Any, **kwargs: Any,
) -> tuple[int, bytes, bytes | None]: ) -> Tuple[int, bytes, Optional[bytes]]:
assert check is False assert retcode is None
assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr']
_setdefault_kwargs(kwargs) _setdefault_kwargs(kwargs)
@ -202,37 +245,29 @@ else: # pragma: no cover
cmd_output_p = cmd_output_b cmd_output_p = cmd_output_b
def _handle_readonly( def rmtree(path: str) -> None:
func: Callable[[str], object], """On windows, rmtree fails for readonly dirs."""
path: str, def handle_remove_readonly(
exc: BaseException, func: Callable[..., Any],
) -> None: path: str,
if ( exc: Tuple[Type[OSError], OSError, TracebackType],
func in (os.rmdir, os.remove, os.unlink) and
isinstance(exc, OSError) and
exc.errno in {errno.EACCES, errno.EPERM}
):
for p in (path, os.path.dirname(path)):
os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR)
func(path)
else:
raise
if sys.version_info < (3, 12): # pragma: <3.12 cover
def _handle_readonly_old(
func: Callable[[str], object],
path: str,
excinfo: tuple[type[BaseException], BaseException, TracebackType],
) -> None: ) -> None:
return _handle_readonly(func, path, excinfo[1]) excvalue = exc[1]
if (
func in (os.rmdir, os.remove, os.unlink) and
excvalue.errno in {errno.EACCES, errno.EPERM}
):
for p in (path, os.path.dirname(path)):
os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR)
func(path)
else:
raise
shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly)
def rmtree(path: str) -> None:
shutil.rmtree(path, ignore_errors=False, onerror=_handle_readonly_old) def parse_version(s: str) -> Tuple[int, ...]:
else: # pragma: >=3.12 cover """poor man's version comparison"""
def rmtree(path: str) -> None: return tuple(int(p) for p in s.split('.'))
"""On windows, rmtree fails for readonly dirs."""
shutil.rmtree(path, ignore_errors=False, onexc=_handle_readonly)
def win_exe(s: str) -> str: def win_exe(s: str) -> str:

View file

@ -1,18 +1,18 @@
from __future__ import annotations
import concurrent.futures import concurrent.futures
import contextlib import contextlib
import math import math
import multiprocessing
import os import os
import subprocess import subprocess
import sys import sys
from collections.abc import Callable
from collections.abc import Generator
from collections.abc import Iterable
from collections.abc import MutableMapping
from collections.abc import Sequence
from typing import Any from typing import Any
from typing import Callable
from typing import Generator
from typing import Iterable
from typing import List
from typing import MutableMapping
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TypeVar from typing import TypeVar
from pre_commit import parse_shebang from pre_commit import parse_shebang
@ -23,22 +23,7 @@ TArg = TypeVar('TArg')
TRet = TypeVar('TRet') TRet = TypeVar('TRet')
def cpu_count() -> int: def _environ_size(_env: Optional[MutableMapping[str, str]] = None) -> int:
try:
# On systems that support it, this will return a more accurate count of
# usable CPUs for the current process, which will take into account
# cgroup limits
return len(os.sched_getaffinity(0))
except AttributeError:
pass
try:
return multiprocessing.cpu_count()
except NotImplementedError:
return 1
def _environ_size(_env: MutableMapping[str, str] | None = None) -> int:
environ = _env if _env is not None else getattr(os, 'environb', os.environ) environ = _env if _env is not None else getattr(os, 'environb', os.environ)
size = 8 * len(environ) # number of pointers in `envp` size = 8 * len(environ) # number of pointers in `envp`
for k, v in environ.items(): for k, v in environ.items():
@ -77,8 +62,8 @@ def partition(
cmd: Sequence[str], cmd: Sequence[str],
varargs: Sequence[str], varargs: Sequence[str],
target_concurrency: int, target_concurrency: int,
_max_length: int | None = None, _max_length: Optional[int] = None,
) -> tuple[tuple[str, ...], ...]: ) -> Tuple[Tuple[str, ...], ...]:
_max_length = _max_length or _get_platform_max_length() _max_length = _max_length or _get_platform_max_length()
# Generally, we try to partition evenly into at least `target_concurrency` # Generally, we try to partition evenly into at least `target_concurrency`
@ -88,7 +73,7 @@ def partition(
cmd = tuple(cmd) cmd = tuple(cmd)
ret = [] ret = []
ret_cmd: list[str] = [] ret_cmd: List[str] = []
# Reversed so arguments are in order # Reversed so arguments are in order
varargs = list(reversed(varargs)) varargs = list(reversed(varargs))
@ -120,6 +105,7 @@ def partition(
@contextlib.contextmanager @contextlib.contextmanager
def _thread_mapper(maxsize: int) -> Generator[ def _thread_mapper(maxsize: int) -> Generator[
Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]], Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]],
None, None,
]: ]:
if maxsize == 1: if maxsize == 1:
yield map yield map
@ -129,14 +115,14 @@ def _thread_mapper(maxsize: int) -> Generator[
def xargs( def xargs(
cmd: tuple[str, ...], cmd: Tuple[str, ...],
varargs: Sequence[str], varargs: Sequence[str],
*, *,
color: bool = False, color: bool = False,
target_concurrency: int = 1, target_concurrency: int = 1,
_max_length: int = _get_platform_max_length(), _max_length: int = _get_platform_max_length(),
**kwargs: Any, **kwargs: Any,
) -> tuple[int, bytes]: ) -> Tuple[int, bytes]:
"""A simplified implementation of xargs. """A simplified implementation of xargs.
color: Make a pty if on a platform that supports it color: Make a pty if on a platform that supports it
@ -166,10 +152,10 @@ def xargs(
partitions = partition(cmd, varargs, target_concurrency, _max_length) partitions = partition(cmd, varargs, target_concurrency, _max_length)
def run_cmd_partition( def run_cmd_partition(
run_cmd: tuple[str, ...], run_cmd: Tuple[str, ...],
) -> tuple[int, bytes, bytes | None]: ) -> Tuple[int, bytes, Optional[bytes]]:
return cmd_fn( return cmd_fn(
*run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs, *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs,
) )
threads = min(len(partitions), target_concurrency) threads = min(len(partitions), target_concurrency)
@ -177,8 +163,7 @@ def xargs(
results = thread_map(run_cmd_partition, partitions) results = thread_map(run_cmd_partition, partitions)
for proc_retcode, proc_out, _ in results: for proc_retcode, proc_out, _ in results:
if abs(proc_retcode) > abs(retcode): retcode = max(retcode, proc_retcode)
retcode = proc_retcode
stdout += proc_out stdout += proc_out
return retcode, stdout return retcode, stdout

View file

@ -1,19 +0,0 @@
from __future__ import annotations
import functools
from typing import Any
import yaml
Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader)
yaml_compose = functools.partial(yaml.compose, Loader=Loader)
yaml_load = functools.partial(yaml.load, Loader=Loader)
Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper)
def yaml_dump(o: Any, **kwargs: Any) -> str:
# when python/mypy#1484 is solved, this can be `functools.partial`
return yaml.dump(
o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False,
**kwargs,
)

View file

@ -1,52 +0,0 @@
from __future__ import annotations
from collections.abc import Generator
from collections.abc import Iterable
from typing import NamedTuple
from typing import Protocol
from yaml.nodes import MappingNode
from yaml.nodes import Node
from yaml.nodes import ScalarNode
from yaml.nodes import SequenceNode
class _Matcher(Protocol):
def match(self, n: Node) -> Generator[Node]: ...
class MappingKey(NamedTuple):
k: str
def match(self, n: Node) -> Generator[Node]:
if isinstance(n, MappingNode):
for k, _ in n.value:
if k.value == self.k:
yield k
class MappingValue(NamedTuple):
k: str
def match(self, n: Node) -> Generator[Node]:
if isinstance(n, MappingNode):
for k, v in n.value:
if k.value == self.k:
yield v
class SequenceItem(NamedTuple):
def match(self, n: Node) -> Generator[Node]:
if isinstance(n, SequenceNode):
yield from n.value
def _match(gen: Iterable[Node], m: _Matcher) -> Iterable[Node]:
return (n for src in gen for n in m.match(src))
def match(n: Node, matcher: tuple[_Matcher, ...]) -> Generator[ScalarNode]:
gen: Iterable[Node] = (n,)
for m in matcher:
gen = _match(gen, m)
return (n for n in gen if isinstance(n, ScalarNode))

View file

@ -1,4 +1,4 @@
covdefaults>=2.2 covdefaults
coverage coverage
distlib distlib
pytest pytest

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = pre_commit name = pre_commit
version = 4.5.1 version = 2.15.0
description = A framework for managing and maintaining multi-language pre-commit hooks. description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown
@ -8,10 +8,15 @@ url = https://github.com/pre-commit/pre-commit
author = Anthony Sottile author = Anthony Sottile
author_email = asottile@umich.edu author_email = asottile@umich.edu
license = MIT license = MIT
license_files = LICENSE license_file = LICENSE
classifiers = classifiers =
License :: OSI Approved :: MIT License
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python :: Implementation :: PyPy
@ -22,8 +27,11 @@ install_requires =
identify>=1.0.0 identify>=1.0.0
nodeenv>=0.11.1 nodeenv>=0.11.1
pyyaml>=5.1 pyyaml>=5.1
virtualenv>=20.10.0 toml
python_requires = >=3.10 virtualenv>=20.0.8
importlib-metadata;python_version<"3.8"
importlib-resources;python_version<"3.7"
python_requires = >=3.6.1
[options.packages.find] [options.packages.find]
exclude = exclude =
@ -33,6 +41,8 @@ exclude =
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =
pre-commit = pre_commit.main:main pre-commit = pre_commit.main:main
pre-commit-validate-config = pre_commit.clientlib:validate_config_main
pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main
[options.package_data] [options.package_data]
pre_commit.resources = pre_commit.resources =
@ -52,7 +62,7 @@ check_untyped_defs = true
disallow_any_generics = true disallow_any_generics = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
disallow_untyped_defs = true disallow_untyped_defs = true
enable_error_code = deprecated no_implicit_optional = true
warn_redundant_casts = true warn_redundant_casts = true
warn_unused_ignores = true warn_unused_ignores = true

View file

@ -1,4 +1,2 @@
from __future__ import annotations
from setuptools import setup from setuptools import setup
setup() setup()

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import collections import collections

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import contextlib import contextlib
import os.path import os.path
import shutil import shutil
@ -12,8 +10,8 @@ from pre_commit import git
from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import CONFIG_SCHEMA
from pre_commit.clientlib import load_manifest from pre_commit.clientlib import load_manifest
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.yaml import yaml_dump from pre_commit.util import yaml_dump
from pre_commit.yaml import yaml_load from pre_commit.util import yaml_load
from testing.util import get_resource_path from testing.util import get_resource_path
from testing.util import git_commit from testing.util import git_commit
@ -38,7 +36,7 @@ def copy_tree_to_path(src_dir, dest_dir):
def git_dir(tempdir_factory): def git_dir(tempdir_factory):
path = tempdir_factory.get() path = tempdir_factory.get()
cmd_output('git', '-c', 'init.defaultBranch=master', 'init', path) cmd_output('git', 'init', path)
return path return path

28
testing/gen-languages-all Executable file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env python3
import sys
LANGUAGES = [
'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail',
'golang', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust',
'script', 'swift', 'system',
]
FIELDS = [
'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment',
'run_hook',
]
def main() -> int:
print(f' # BEGIN GENERATED ({sys.argv[0]})')
for lang in LANGUAGES:
parts = [f' {lang!r}: Language(name={lang!r}']
for k in FIELDS:
parts.append(f', {k}={lang}.{k}')
parts.append('), # noqa: E501')
print(''.join(parts))
print(' # END GENERATED')
return 0
if __name__ == '__main__':
exit(main())

11
testing/get-coursier.ps1 Executable file
View file

@ -0,0 +1,11 @@
$wc = New-Object System.Net.WebClient
$coursier_url = "https://github.com/coursier/coursier/releases/download/v2.0.5/cs-x86_64-pc-win32.exe"
$coursier_dest = "C:\coursier\cs.exe"
$coursier_hash ="d63d497f7805261e1cd657b8aaa626f6b8f7264cdb68219b2e6be9dd882033a9"
New-Item -Path "C:\" -Name "coursier" -ItemType "directory"
$wc.DownloadFile($coursier_url, $coursier_dest)
if ((Get-FileHash $coursier_dest -Algorithm SHA256).Hash -ne $coursier_hash) {
throw "Invalid coursier file"
}

View file

@ -1,29 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# This is a script used in CI to install coursier
set -euo pipefail set -euo pipefail
if [ "$OSTYPE" = msys ]; then COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux"
URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-win32.zip' COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f"
SHA256='0d07386ff0f337e3e6264f7dde29d137dda6eaa2385f29741435e0b93ccdb49d' ARTIFACT="/tmp/coursier/cs"
TARGET='/tmp/coursier/cs.zip'
unpack() {
unzip "$TARGET" -d /tmp/coursier
mv /tmp/coursier/cs-*.exe /tmp/coursier/cs.exe
cygpath -w /tmp/coursier >> "$GITHUB_PATH"
}
else
URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-linux.gz'
SHA256='176e92e08ab292531aa0c4993dbc9f2c99dec79578752f3b9285f54f306db572'
TARGET=/tmp/coursier/cs.gz
unpack() {
gunzip "$TARGET"
chmod +x /tmp/coursier/cs
echo /tmp/coursier >> "$GITHUB_PATH"
}
fi
mkdir -p /tmp/coursier mkdir -p /tmp/coursier
curl --location --silent --output "$TARGET" "$URL" rm -f "$ARTIFACT"
echo "$SHA256 $TARGET" | sha256sum --check curl --location --silent --output "$ARTIFACT" "$COURSIER_URL"
unpack echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check
chmod ugo+x /tmp/coursier/cs
echo '##vso[task.prependpath]/tmp/coursier'

View file

@ -1,14 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
VERSION=2.19.6 VERSION=2.13.4
if [ "$OSTYPE" = msys ]; then if [ "$OSTYPE" = msys ]; then
URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip"
cygpath -w /tmp/dart-sdk/bin >> "$GITHUB_PATH" echo "##vso[task.prependpath]$(cygpath -w /tmp/dart-sdk/bin)"
else else
URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip" URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip"
echo '/tmp/dart-sdk/bin' >> "$GITHUB_PATH" echo '##vso[task.prependpath]/tmp/dart-sdk/bin'
fi fi
curl --silent --location --output /tmp/dart.zip "$URL" curl --silent --location --output /tmp/dart.zip "$URL"

6
testing/get-r.ps1 Normal file
View file

@ -0,0 +1,6 @@
$dir = $Env:Temp
$urlR = "https://cran.r-project.org/bin/windows/base/old/4.0.4/R-4.0.4-win.exe"
$outputR = "$dir\R-win.exe"
$wcR = New-Object System.Net.WebClient
$wcR.DownloadFile($urlR, $outputR)
Start-Process -FilePath $outputR -ArgumentList "/S /v/qn"

9
testing/get-r.sh Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
sudo apt install r-base
# create empty folder for user library.
# necessary for non-root users who have
# never installed an R package before.
# Alternatively, we require the renv
# package to be installed already, then we can
# omit that.
Rscript -e 'dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)'

29
testing/get-swift.sh Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# This is a script used in CI to install swift
set -euo pipefail
. /etc/lsb-release
if [ "$DISTRIB_CODENAME" = "bionic" ]; then
SWIFT_URL='https://swift.org/builds/swift-5.1.3-release/ubuntu1804/swift-5.1.3-RELEASE/swift-5.1.3-RELEASE-ubuntu18.04.tar.gz'
SWIFT_HASH='ac82ccd773fe3d586fc340814e31e120da1ff695c6a712f6634e9cc720769610'
else
echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2
exit 1
fi
check() {
echo "$SWIFT_HASH $TGZ" | sha256sum --check
}
TGZ="$HOME/.swift/swift.tar.gz"
mkdir -p "$(dirname "$TGZ")"
if ! check >& /dev/null; then
rm -f "$TGZ"
curl --location --silent --output "$TGZ" "$SWIFT_URL"
check
fi
mkdir -p /tmp/swift
tar -xf "$TGZ" --strip 1 --directory /tmp/swift
echo '##vso[task.prependpath]/tmp/swift/usr/bin'

View file

@ -1,40 +0,0 @@
from __future__ import annotations
import os
from collections.abc import Sequence
from pre_commit.lang_base import Language
from pre_commit.prefix import Prefix
def run_language(
path: os.PathLike[str],
language: Language,
exe: str,
args: Sequence[str] = (),
file_args: Sequence[str] = (),
version: str | None = None,
deps: Sequence[str] = (),
is_local: bool = False,
require_serial: bool = True,
color: bool = False,
) -> tuple[int, bytes]:
prefix = Prefix(str(path))
version = version or language.get_default_version()
if language.ENVIRONMENT_DIR is not None:
language.install_environment(prefix, version, deps)
health_error = language.health_check(prefix, version)
assert health_error is None, health_error
with language.in_env(prefix, version):
ret, out = language.run_hook(
prefix,
exe,
args,
file_args,
is_local=is_local,
require_serial=require_serial,
color=color,
)
out = out.replace(b'\r\n', b'\n')
return ret, out

View file

@ -1,92 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import concurrent.futures
import json
import os.path
import subprocess
import sys
EXCLUDED = frozenset((
('windows-latest', 'docker'),
('windows-latest', 'docker_image'),
('windows-latest', 'lua'),
('windows-latest', 'swift'),
))
def _always_run() -> frozenset[str]:
ret = ['.github/workflows/languages.yaml', 'testing/languages']
ret.extend(
os.path.join('pre_commit/resources', fname)
for fname in os.listdir('pre_commit/resources')
)
return frozenset(ret)
def _lang_files(lang: str) -> frozenset[str]:
prog = f'''\
import json
import os.path
import sys
import pre_commit.languages.{lang}
import tests.languages.{lang}_test
modules = sorted(
os.path.relpath(v.__file__)
for k, v in sys.modules.items()
if k.startswith(('pre_commit.', 'tests.', 'testing.'))
)
print(json.dumps(modules))
'''
out = json.loads(subprocess.check_output((sys.executable, '-c', prog)))
return frozenset(out)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--all', action='store_true')
args = parser.parse_args()
langs = [
os.path.splitext(fname)[0]
for fname in sorted(os.listdir('pre_commit/languages'))
if fname.endswith('.py') and fname != '__init__.py'
]
triggers_all = _always_run()
for fname in triggers_all:
assert os.path.exists(fname), fname
if not args.all:
with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe:
by_lang = {
lang: files | triggers_all
for lang, files in zip(langs, exe.map(_lang_files, langs))
}
diff_cmd = ('git', 'diff', '--name-only', 'origin/main...HEAD')
files = set(subprocess.check_output(diff_cmd).decode().splitlines())
langs = [
lang
for lang, lang_files in by_lang.items()
if lang_files & files
]
matched = [
{'os': os, 'language': lang}
for os in ('windows-latest', 'ubuntu-latest')
for lang in langs
if (os, lang) not in EXCLUDED
]
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f'languages={json.dumps(matched)}\n')
return 0
if __name__ == '__main__':
raise SystemExit(main())

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