Compare commits

..

No commits in common. "main" and "v1.20.0" have entirely different histories.

228 changed files with 4932 additions and 13527 deletions

34
.coveragerc Normal file
View file

@ -0,0 +1,34 @@
[run]
branch = True
source = .
omit =
.tox/*
/usr/*
setup.py
# Don't complain if non-runnable code isn't run
*/__main__.py
pre_commit/color_windows.py
pre_commit/resources/*
[report]
show_missing = True
skip_covered = True
exclude_lines =
# Have to re-enable the standard pragma
\#\s*pragma: no cover
# We optionally substitute this
${COVERAGE_IGNORE_WINDOWS}
# Don't complain if tests don't hit defensive assertion code:
^\s*raise AssertionError\b
^\s*raise NotImplementedError\b
^\s*return NotImplemented\b
^\s*raise$
# Don't complain if non-runnable code isn't run:
^if __name__ == ['"]__main__['"]:$
[html]
directory = coverage-html
# vim:ft=dosini

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

16
.gitignore vendored
View file

@ -1,6 +1,14 @@
*.egg-info
*.iml
*.py[co]
/.coverage
/.tox
/dist
.vscode/
.*.sw[a-z]
.coverage
.idea
.project
.pydevproject
.tox
.venv.touch
/venv*
coverage-html
dist
.pytest_cache

View file

@ -1,44 +1,42 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v2.1.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-docstring-first
- id: check-json
- id: check-yaml
- id: debug-statements
- id: double-quote-string-fixer
- id: name-tests-test
- id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt
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
rev: 7.3.0
- id: double-quote-string-fixer
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.7
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.4.3
hooks:
- id: mypy
additional_dependencies: [types-pyyaml]
exclude: ^testing/resources/
- id: autopep8
- repo: https://github.com/pre-commit/pre-commit
rev: v1.14.4
hooks:
- id: validate_manifest
- repo: https://github.com/asottile/pyupgrade
rev: v1.12.0
hooks:
- id: pyupgrade
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.4.0
hooks:
- id: reorder-python-imports
language_version: python3
- repo: https://github.com/asottile/add-trailing-comma
rev: v1.0.0
hooks:
- id: add-trailing-comma
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes

View file

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

File diff suppressed because it is too large Load diff

View file

@ -4,15 +4,12 @@
- The complete test suite depends on having at least the following installed
(possibly not a complete list)
- git (Version 2.24.0 or above is required to run pre-merge-commit tests)
- git (A sufficiently newer version is required to run pre-push tests)
- python2 (Required by a test which checks different python versions)
- python3 (Required by a test which checks different python versions)
- tox (or virtualenv)
- ruby + gem
- docker
- conda
- cargo (required by tests for rust dependencies)
- go (required by tests for go dependencies)
- swift
### Setting up an environment
@ -20,8 +17,7 @@ This is useful for running specific tests. The easiest way to set this up
is to run:
1. `tox --devenv venv` (note: requires tox>=3.13)
2. `. venv/bin/activate` (or follow the [activation instructions] for your
platform)
2. `. venv/bin/activate`
This will create and put you into a virtualenv which has an editable
installation of pre-commit. Hack away! Running `pre-commit` will reflect
@ -64,14 +60,14 @@ to implement. The current implemented languages are at varying levels:
- 0th class - pre-commit does not require any dependencies for these languages
as they're not actually languages (current examples: fail, pygrep)
- 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
will install tools in an isolated fashion (current examples: python, swift,
docker).
will install tools in an isolated fashion (current examples: python, go, rust,
swift, docker).
- 3rd class - pre-commit requires the user to install both the tool and the
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.
Ideally the language works on the supported platforms for pre-commit (linux,
@ -92,7 +88,7 @@ language, for example:
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`
@ -111,21 +107,20 @@ one cannot be determined, return `'default'`.
You generally don't need to implement this on a first pass and can just use:
```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
#### `health_check`
#### `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
healthy.
This function should return `True` or `False`.
You generally don't need to implement this on a first pass and can just use:
```python
health_check = lang_base.basic_health_check
healthy = helpers.basic_healthy
```
`python` is currently the only language which implements this api, for python
@ -137,7 +132,7 @@ this is the trickiest one to implement and where all the smart parts happen.
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
- (2nd class): install the package at `.` into the `ENVIRONMENT_DIR`
- (2nd class, optional): install packages listed in `additional_dependencies`
@ -149,5 +144,3 @@ This is usually the easiest to implement, most of them look the same as the
`node` hook implementation:
https://github.com/pre-commit/pre-commit/blob/160238220f022035c8ef869c9a8642f622c02118/pre_commit/languages/node.py#L72-L74
[activation instructions]: https://virtualenv.pypa.io/en/latest/user_guide.html#activators

View file

@ -1,5 +1,5 @@
[![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)
[![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)
[![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)
[![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

46
azure-pipelines.yml Normal file
View file

@ -0,0 +1,46 @@
trigger:
branches:
include: [master, test-me-*]
tags:
include: ['*']
resources:
repositories:
- repository: asottile
type: github
endpoint: github
name: asottile/azure-pipeline-templates
ref: refs/tags/v0.0.15
jobs:
- template: job--pre-commit.yml@asottile
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [py27, py37]
os: windows
additional_variables:
COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover'
TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS
TEMP: C:\Temp # remove when dropping python2
- 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-swift.sh
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
displayName: install swift
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [pypy, pypy3, py27, py36, py37, py38]
os: linux
pre_test:
- task: UseRubyVersion@0
- bash: |
testing/get-swift.sh
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
displayName: install swift

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from __future__ import absolute_import
from pre_commit.main import 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,252 +1,65 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import argparse
import functools
import logging
import os.path
import re
import shlex
import pipes
import sys
from collections.abc import Callable
from collections.abc import Sequence
from typing import Any
from typing import NamedTuple
import cfgv
from aspy.yaml import ordered_load
from identify.identify import ALL_TAGS
import pre_commit.constants as C
from pre_commit.all_languages import language_names
from pre_commit.errors import FatalError
from pre_commit.yaml import yaml_load
from pre_commit.error_handler import FatalError
from pre_commit.languages.all import all_languages
from pre_commit.util import parse_version
logger = logging.getLogger('pre_commit')
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):
if tag not in ALL_TAGS:
raise cfgv.ValidationError(
f'Type tag {tag!r} is not recognized. '
f'Try upgrading identify and pre-commit?',
'Type tag {!r} is not recognized. '
'Try upgrading identify and pre-commit?'.format(tag),
)
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):
if parse_version(version) > parse_version(C.VERSION):
raise cfgv.ValidationError(
f'pre-commit version {version} is required but version '
f'{C.VERSION} is installed. '
f'Perhaps run `pip install --upgrade pre-commit`.',
'pre-commit version {} is required but version {} is installed. '
'Perhaps run `pip install --upgrade pre-commit`.'.format(
version, C.VERSION,
),
)
_STAGES = {
'commit': 'pre-commit',
'merge-commit': 'pre-merge-commit',
'push': 'pre-push',
}
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)
def _make_argparser(filenames_help):
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help=filenames_help)
parser.add_argument('-V', '--version', action='version', version=C.VERSION)
return parser
MANIFEST_HOOK_DICT = cfgv.Map(
'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('name', 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('files', check_string_regex, ''),
cfgv.Optional('exclude', check_string_regex, '^$'),
cfgv.Optional(
'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '',
),
cfgv.Optional(
'exclude', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '^$',
),
cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']),
cfgv.Optional('types_or', cfgv.check_array(check_type_tag), []),
cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []),
cfgv.Optional(
@ -254,13 +67,13 @@ MANIFEST_HOOK_DICT = cfgv.Map(
),
cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []),
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('description', cfgv.check_string, ''),
cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT),
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),
StagesMigration('stages', []),
cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []),
cfgv.Optional('verbose', cfgv.check_bool, False),
)
MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)
@ -270,106 +83,83 @@ class InvalidManifestError(FatalError):
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(
cfgv.load_from_filename,
schema=MANIFEST_SCHEMA,
load_strategy=_load_manifest_forward_compat,
load_strategy=ordered_load,
exc_tp=InvalidManifestError,
)
def validate_manifest_main(argv=None):
parser = _make_argparser('Manifest filenames.')
args = parser.parse_args(argv)
ret = 0
for filename in args.filenames:
try:
load_manifest(filename)
except InvalidManifestError as e:
print(e)
ret = 1
return ret
LOCAL = 'local'
META = 'meta'
class WarnMutableRev(cfgv.Conditional):
def check(self, dct: dict[str, Any]) -> None:
super().check(dct)
class MigrateShaToRev(object):
key = 'rev'
if self.key in dct:
rev = dct[self.key]
@staticmethod
def _cond(key):
return cfgv.Conditional(
key, cfgv.check_string,
condition_key='repo',
condition_value=cfgv.NotIn(LOCAL, META),
ensure_absent=True,
)
if '.' not in rev and not re.match(r'^[a-fA-F0-9]+$', rev):
logger.warning(
f'The {self.key!r} field of repo {dct["repo"]!r} '
f'appears to be a mutable reference '
f'(moving tag / branch). Mutable references are never '
f'updated after first install and are not supported. '
f'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501
f'for more details. '
f'Hint: `pre-commit autoupdate` often fixes this.',
)
def check(self, dct):
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):
if 'sha' in dct:
dct['rev'] = dct.pop('sha')
def remove_default(self, dct):
pass
class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault):
def check(self, dct: dict[str, Any]) -> None:
super().check(dct)
if '/*' in dct.get(self.key, ''):
logger.warning(
f'The {self.key!r} field in hook {dct.get("id")!r} is a '
f"regex, not a glob -- matching '/*' probably isn't what you "
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):
def check(self, dct: dict[str, Any]) -> None:
super().check(dct)
if '/*' in dct.get(self.key, ''):
logger.warning(
f'The top-level {self.key!r} field is a regex, not a glob -- '
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(
fr'pre-commit normalizes the slashes in the top-level '
fr'{self.key!r} field to forward slashes, so you '
fr'can use / instead of {fwd_slash_re}',
)
def _entry(modname: str) -> str:
def _entry(modname):
"""the hook `entry` is passed through `shlex.split()` by the command
runner, so to prevent issues with spaces and backslashes (on Windows)
it must be quoted here.
"""
return f'{shlex.quote(sys.executable)} -m pre_commit.meta_hooks.{modname}'
return '{} -m pre_commit.meta_hooks.{}'.format(
pipes.quote(sys.executable), modname,
)
def warn_unknown_keys_root(
extra: Sequence[str],
orig_keys: Sequence[str],
dct: dict[str, str],
) -> None:
logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}')
def warn_unknown_keys_repo(
extra: Sequence[str],
orig_keys: Sequence[str],
dct: dict[str, str],
) -> None:
def warn_unknown_keys_root(extra, orig_keys, dct):
logger.warning(
f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}',
'Unexpected key(s) present at root: {}'.format(', '.join(extra)),
)
def warn_unknown_keys_repo(extra, orig_keys, dct):
logger.warning(
'Unexpected key(s) present on {}: {}'.format(
dct['repo'], ', '.join(extra),
),
)
@ -377,14 +167,14 @@ _meta = (
(
'check-hooks-apply', (
('name', 'Check hooks apply to the repository'),
('files', f'^{re.escape(C.CONFIG_FILE)}$'),
('files', C.CONFIG_FILE),
('entry', _entry('check_hooks_apply')),
),
),
(
'check-useless-excludes', (
('name', 'Check for useless excludes'),
('files', f'^{re.escape(C.CONFIG_FILE)}$'),
('files', C.CONFIG_FILE),
('entry', _entry('check_useless_excludes')),
),
),
@ -397,44 +187,25 @@ _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(
'Hook', 'id',
cfgv.Required('id', cfgv.check_string),
cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))),
# language must be `unsupported`
cfgv.Optional(
'language', cfgv.check_one_of({'unsupported'}), 'unsupported',
),
# entry cannot be overridden
NotAllowed('entry', cfgv.check_any),
*(
# language must be system
cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'),
*([
# default to the hook definition for the meta hooks
cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id)
for hook_id, values in _meta
for key, value in values
),
*(
] + [
# default to the "manifest" parsing
cfgv.OptionalNoDefault(item.key, item.check_fn)
# these will always be defaulted above
if item.key in {'name', 'language', 'entry'} else
item
for item in MANIFEST_HOOK_DICT.items
),
*_COMMON_HOOK_WARNINGS,
])
)
CONFIG_HOOK_DICT = cfgv.Map(
'Hook', 'id',
@ -445,22 +216,11 @@ CONFIG_HOOK_DICT = cfgv.Map(
# are optional.
# No defaults are provided here as the config is merged on top of the
# manifest.
*(
*[
cfgv.OptionalNoDefault(item.key, item.check_fn)
for item in MANIFEST_HOOK_DICT.items
if item.key != 'id'
if item.key != 'stages'
if item.key != 'language' # remove
),
StagesMigrationNoDefault('stages', []),
LanguageMigration('language', cfgv.check_one_of(language_names)), # remove
*_COMMON_HOOK_WARNINGS,
)
LOCAL_HOOK_DICT = cfgv.Map(
'Hook', 'id',
*MANIFEST_HOOK_DICT.items,
*_COMMON_HOOK_WARNINGS,
]
)
CONFIG_REPO_DICT = cfgv.Map(
'Repository', 'repo',
@ -472,7 +232,7 @@ CONFIG_REPO_DICT = cfgv.Map(
'repo', cfgv.NotIn(LOCAL, META),
),
cfgv.ConditionalRecurse(
'hooks', cfgv.Array(LOCAL_HOOK_DICT),
'hooks', cfgv.Array(MANIFEST_HOOK_DICT),
'repo', LOCAL,
),
cfgv.ConditionalRecurse(
@ -480,62 +240,44 @@ CONFIG_REPO_DICT = cfgv.Map(
'repo', META,
),
WarnMutableRev(
'rev', cfgv.check_string,
condition_key='repo',
condition_value=cfgv.NotIn(LOCAL, META),
ensure_absent=True,
),
MigrateShaToRev(),
cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo),
)
DEFAULT_LANGUAGE_VERSION = cfgv.Map(
'DefaultLanguageVersion', None,
cfgv.NoAdditionalKeys(language_names),
*(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in language_names),
cfgv.NoAdditionalKeys(all_languages),
*[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages]
)
CONFIG_SCHEMA = cfgv.Map(
'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('exclude', cfgv.check_regex, '^$'),
cfgv.Optional('fail_fast', cfgv.check_bool, False),
cfgv.Optional(
'minimum_pre_commit_version',
cfgv.check_and(cfgv.check_string, check_min_version),
'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(
(
'repos',
'default_install_hook_types',
'default_language_version',
'default_stages',
'files',
'exclude',
'fail_fast',
'minimum_pre_commit_version',
'ci',
),
warn_unknown_keys_root,
),
OptionalSensibleRegexAtTop('files', cfgv.check_string),
OptionalSensibleRegexAtTop('exclude', cfgv.check_string),
# do not warn about configuration for pre-commit.ci
cfgv.OptionalNoDefault('ci', cfgv.check_type(dict)),
)
@ -543,9 +285,31 @@ class InvalidConfigError(FatalError):
pass
def ordered_load_normalize_legacy_config(contents):
data = ordered_load(contents)
if isinstance(data, list):
# TODO: Once happy, issue a deprecation warning and instructions
return {'repos': data}
else:
return data
load_config = functools.partial(
cfgv.load_from_filename,
schema=CONFIG_SCHEMA,
load_strategy=yaml_load,
load_strategy=ordered_load_normalize_legacy_config,
exc_tp=InvalidConfigError,
)
def validate_config_main(argv=None):
parser = _make_argparser('Config filenames.')
args = parser.parse_args(argv)
ret = 0
for filename in args.filenames:
try:
load_config(filename)
except InvalidConfigError as e:
print(e)
ret = 1
return ret

View file

@ -1,70 +1,28 @@
from __future__ import annotations
from __future__ import unicode_literals
import argparse
import os
import sys
if sys.platform == 'win32': # pragma: no cover (windows)
def _enable() -> None:
from ctypes import POINTER
from ctypes import windll
from ctypes import WinError
from ctypes import WINFUNCTYPE
from ctypes.wintypes import BOOL
from ctypes.wintypes import DWORD
from ctypes.wintypes import HANDLE
STD_ERROR_HANDLE = -12
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
def bool_errcheck(result, func, args):
if not result:
raise WinError()
return args
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(
('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),),
)
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
('GetConsoleMode', windll.kernel32),
((1, 'hConsoleHandle'), (2, 'lpMode')),
)
GetConsoleMode.errcheck = bool_errcheck
SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
('SetConsoleMode', windll.kernel32),
((1, 'hConsoleHandle'), (1, 'dwMode')),
)
SetConsoleMode.errcheck = bool_errcheck
# As of Windows 10, the Windows console supports (some) ANSI escape
# sequences, but it needs to be enabled using `SetConsoleMode` first.
#
# More info on the escape sequences supported:
# https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx
stderr = GetStdHandle(STD_ERROR_HANDLE)
flags = GetConsoleMode(stderr)
SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
terminal_supports_color = True
if os.name == 'nt': # pragma: no cover (windows)
from pre_commit.color_windows import enable_virtual_terminal_processing
try:
_enable()
except OSError:
enable_virtual_terminal_processing()
except WindowsError:
terminal_supports_color = False
else:
terminal_supports_color = True
else: # pragma: win32 no cover
terminal_supports_color = True
RED = '\033[41m'
GREEN = '\033[42m'
YELLOW = '\033[43;30m'
TURQUOISE = '\033[46;30m'
SUBTLE = '\033[2m'
NORMAL = '\033[m'
NORMAL = '\033[0m'
def format_color(text: str, color: str, use_color_setting: bool) -> str:
class InvalidColorSetting(ValueError):
pass
def format_color(text, color, use_color_setting):
"""Format text with color.
Args:
@ -72,38 +30,29 @@ def format_color(text: str, color: str, use_color_setting: bool) -> str:
color - The color start string
use_color_setting - Whether or not to color
"""
if use_color_setting:
return f'{color}{text}{NORMAL}'
else:
if not use_color_setting:
return text
else:
return '{}{}{}'.format(color, text, NORMAL)
COLOR_CHOICES = ('auto', 'always', 'never')
def use_color(setting: str) -> bool:
def use_color(setting):
"""Choose whether to use color based on the command argument.
Args:
setting - Either `auto`, `always`, or `never`
"""
if setting not in COLOR_CHOICES:
raise ValueError(setting)
raise InvalidColorSetting(setting)
return (
setting == 'always' or (
setting == 'auto' and
sys.stderr.isatty() and
sys.stdout.isatty() and
terminal_supports_color and
os.getenv('TERM') != 'dumb'
)
)
def add_color_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'),
type=use_color,
metavar='{' + ','.join(COLOR_CHOICES) + '}',
help='Whether to use color in output. Defaults to `%(default)s`.',
)

View file

@ -0,0 +1,48 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from ctypes import POINTER
from ctypes import windll
from ctypes import WinError
from ctypes import WINFUNCTYPE
from ctypes.wintypes import BOOL
from ctypes.wintypes import DWORD
from ctypes.wintypes import HANDLE
STD_OUTPUT_HANDLE = -11
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
def bool_errcheck(result, func, args):
if not result:
raise WinError()
return args
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(
('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),),
)
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
('GetConsoleMode', windll.kernel32),
((1, 'hConsoleHandle'), (2, 'lpMode')),
)
GetConsoleMode.errcheck = bool_errcheck
SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
('SetConsoleMode', windll.kernel32),
((1, 'hConsoleHandle'), (1, 'dwMode')),
)
SetConsoleMode.errcheck = bool_errcheck
def enable_virtual_terminal_processing():
"""As of Windows 10, the Windows console supports (some) ANSI escape
sequences, but it needs to be enabled using `SetConsoleMode` first.
More info on the escape sequences supported:
https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx
"""
stdout = GetStdHandle(STD_OUTPUT_HANDLE)
flags = GetConsoleMode(stdout)
SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)

View file

@ -1,17 +1,18 @@
from __future__ import annotations
from __future__ import print_function
from __future__ import unicode_literals
import concurrent.futures
import os.path
import re
import tempfile
from collections.abc import Sequence
from typing import Any
from typing import NamedTuple
import six
from aspy.yaml import ordered_dump
from aspy.yaml import ordered_load
from cfgv import remove_defaults
import pre_commit.constants as C
from pre_commit import git
from pre_commit import output
from pre_commit import xargs
from pre_commit.clientlib import CONFIG_SCHEMA
from pre_commit.clientlib import InvalidManifestError
from pre_commit.clientlib import load_config
from pre_commit.clientlib import load_manifest
@ -21,195 +22,143 @@ from pre_commit.commands.migrate_config import migrate_config
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
from pre_commit.yaml import yaml_dump
from pre_commit.yaml import yaml_load
class RevInfo(NamedTuple):
repo: str
rev: str
frozen: str | None = None
hook_ids: frozenset[str] = frozenset()
@classmethod
def from_config(cls, config: dict[str, Any]) -> RevInfo:
return cls(config['repo'], config['rev'])
def update(self, tags_only: bool, freeze: bool) -> RevInfo:
with tempfile.TemporaryDirectory() as tmp:
_git = ('git', *git.NO_FS_MONITOR, '-C', tmp)
if tags_only:
tag_opt = '--abbrev=0'
else:
tag_opt = '--exact'
tag_cmd = (*_git, 'describe', 'FETCH_HEAD', '--tags', tag_opt)
git.init_repo(tmp, self.repo)
cmd_output_b(*_git, 'config', 'extensions.partialClone', 'true')
cmd_output_b(
*_git, 'fetch', 'origin', 'HEAD',
'--quiet', '--filter=blob:none', '--tags',
)
try:
rev = cmd_output(*tag_cmd)[1].strip()
except CalledProcessError:
rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip()
else:
if tags_only:
rev = git.get_best_candidate_tag(rev, tmp)
frozen = None
if freeze:
exact = cmd_output(*_git, 'rev-parse', rev)[1].strip()
if exact != rev:
rev, frozen = exact, rev
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)
from pre_commit.util import tmpdir
class RepositoryCannotBeUpdatedError(RuntimeError):
pass
def _check_hooks_still_exist_at_rev(
repo_config: dict[str, Any],
info: RevInfo,
) -> None:
def _update_repo(repo_config, store, tags_only):
"""Updates a repository to the tip of `master`. If the repository cannot
be updated because a hook that is configured does not exist in `master`,
this raises a RepositoryCannotBeUpdatedError
Args:
repo_config - A config for a repository
"""
with tmpdir() as repo_path:
git.init_repo(repo_path, repo_config['repo'])
cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path)
tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags')
if tags_only:
tag_cmd += ('--abbrev=0',)
else:
tag_cmd += ('--exact',)
try:
rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip()
except CalledProcessError:
tag_cmd = ('git', 'rev-parse', 'FETCH_HEAD')
rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip()
# Don't bother trying to update if our rev is the same
if rev == repo_config['rev']:
return repo_config
try:
path = store.clone(repo_config['repo'], rev)
manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE))
except InvalidManifestError as e:
raise RepositoryCannotBeUpdatedError(six.text_type(e))
# See if any of our hooks were deleted with the new commits
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:
raise RepositoryCannotBeUpdatedError(
f'[{info.repo}] Cannot update because the update target is '
f'missing these hooks: {", ".join(sorted(hooks_missing))}',
'Cannot update because the tip of master is missing these hooks:\n'
'{}'.format(', '.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
# Construct a new config with the head rev
new_config = repo_config.copy()
new_config['rev'] = rev
return new_config
REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$')
REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)$', re.DOTALL)
REV_LINE_FMT = '{}rev:{}{}{}'
def _original_lines(
path: str,
rev_infos: list[RevInfo | None],
retry: bool = False,
) -> tuple[list[str], list[int]]:
"""detect `rev:` lines or reformat the file"""
with open(path, newline='') as f:
original = f.read()
def _write_new_config_file(path, output):
with open(path) as f:
original_contents = f.read()
output = remove_defaults(output, CONFIG_SCHEMA)
new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS)
lines = original.splitlines(True)
idxs = [i for i, line in enumerate(lines) if REV_LINE_RE.match(line)]
if len(idxs) == len(rev_infos):
return lines, idxs
elif retry:
raise AssertionError('could not find rev lines')
else:
with open(path, 'w') as f:
f.write(yaml_dump(yaml_load(original)))
return _original_lines(path, rev_infos, retry=True)
lines = original_contents.splitlines(True)
rev_line_indices_reversed = list(
reversed([
i for i, line in enumerate(lines) if REV_LINE_RE.match(line)
]),
)
for line in new_contents.splitlines(True):
if REV_LINE_RE.match(line):
# It's possible we didn't identify the rev lines in the original
if not rev_line_indices_reversed:
break
line_index = rev_line_indices_reversed.pop()
original_line = lines[line_index]
orig_match = REV_LINE_RE.match(original_line)
new_match = REV_LINE_RE.match(line)
lines[line_index] = REV_LINE_FMT.format(
orig_match.group(1), orig_match.group(2),
new_match.group(3), orig_match.group(4),
)
# If we failed to intelligently rewrite the rev lines, fall back to the
# pretty-formatted yaml output
to_write = ''.join(lines)
if remove_defaults(ordered_load(to_write), CONFIG_SCHEMA) != output:
to_write = new_contents
with open(path, 'w') as f:
f.write(to_write)
def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None:
lines, idxs = _original_lines(path, rev_infos)
for idx, rev_info in zip(idxs, rev_infos):
if rev_info is None:
continue
match = REV_LINE_RE.match(lines[idx])
assert match is not None
new_rev_s = yaml_dump({'rev': rev_info.rev}, default_style=match[3])
new_rev = new_rev_s.split(':', 1)[1].strip()
if rev_info.frozen is not None:
comment = f' # frozen: {rev_info.frozen}'
elif match[5].strip().startswith('# frozen:'):
comment = ''
else:
comment = match[5]
lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[6]}'
with open(path, 'w', newline='') as f:
f.write(''.join(lines))
def autoupdate(
config_file: str,
tags_only: bool,
freeze: bool,
repos: Sequence[str] = (),
jobs: int = 1,
) -> int:
def autoupdate(config_file, store, tags_only, repos=()):
"""Auto-update the pre-commit config to the latest versions of repos."""
migrate_config(config_file, quiet=True)
changed = False
retv = 0
output_repos = []
changed = False
config_repos = [
repo for repo in load_config(config_file)['repos']
if repo['repo'] not in {LOCAL, META}
]
input_config = load_config(config_file)
rev_infos: list[RevInfo | None] = [None] * len(config_repos)
jobs = jobs or xargs.cpu_count() # 0 => number of cpus
jobs = min(jobs, len(repos) or len(config_repos)) # max 1-per-thread
jobs = max(jobs, 1) # at least one thread
with concurrent.futures.ThreadPoolExecutor(jobs) as exe:
futures = [
exe.submit(
_update_one,
i, repo, tags_only=tags_only, freeze=freeze,
for repo_config in input_config['repos']:
if (
repo_config['repo'] in {LOCAL, META} or
# Skip updating any repo_configs that aren't for the specified repo
repos and repo_config['repo'] not in repos
):
output_repos.append(repo_config)
continue
output.write('Updating {}...'.format(repo_config['repo']))
try:
new_repo_config = _update_repo(repo_config, store, tags_only)
except RepositoryCannotBeUpdatedError as error:
output.write_line(error.args[0])
output_repos.append(repo_config)
retv = 1
continue
if new_repo_config['rev'] != repo_config['rev']:
changed = True
output.write_line(
'updating {} -> {}.'.format(
repo_config['rev'], new_repo_config['rev'],
),
)
for i, repo in enumerate(config_repos)
if not repos or repo['repo'] in repos
]
for future in concurrent.futures.as_completed(futures):
try:
i, old, new = future.result()
except RepositoryCannotBeUpdatedError as e:
output.write_line(str(e))
retv = 1
else:
if new.rev != old.rev:
changed = True
if new.frozen:
new_s = f'{new.frozen} (frozen)'
else:
new_s = new.rev
msg = f'updating {old.rev} -> {new_s}'
rev_infos[i] = new
else:
msg = 'already up to date!'
output.write_line(f'[{old.repo}] {msg}')
output_repos.append(new_repo_config)
else:
output.write_line('already up to date.')
output_repos.append(repo_config)
if changed:
_write_new_config(config_file, rev_infos)
output_config = input_config.copy()
output_config['repos'] = output_repos
_write_new_config_file(config_file, output_config)
return retv

View file

@ -1,16 +1,16 @@
from __future__ import annotations
from __future__ import print_function
from __future__ import unicode_literals
import os.path
from pre_commit import output
from pre_commit.store import Store
from pre_commit.util import rmtree
def clean(store: Store) -> int:
def clean(store):
legacy_path = os.path.expanduser('~/.pre-commit')
for directory in (store.directory, legacy_path):
if os.path.exists(directory):
rmtree(directory)
output.write_line(f'Cleaned {directory}.')
output.write_line('Cleaned {}.'.format(directory))
return 0

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import os.path
from typing import Any
import pre_commit.constants as C
from pre_commit import output
@ -11,24 +11,16 @@ from pre_commit.clientlib import load_config
from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META
from pre_commit.store import Store
from pre_commit.util import rmtree
def _mark_used_repos(
store: Store,
all_repos: dict[tuple[str, str], str],
unused_repos: set[tuple[str, str]],
repo: dict[str, Any],
) -> None:
def _mark_used_repos(store, all_repos, unused_repos, repo):
if repo['repo'] == META:
return
elif repo['repo'] == LOCAL:
for hook in repo['hooks']:
deps = hook.get('additional_dependencies')
unused_repos.discard((
store.db_repo_name(repo['repo'], deps),
C.LOCAL_REPO_VERSION,
store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION,
))
else:
key = (repo['repo'], repo['rev'])
@ -58,41 +50,34 @@ def _mark_used_repos(
))
def _gc(store: Store) -> int:
with store.exclusive_lock(), store.connect() as db:
store._create_configs_table(db)
def _gc_repos(store):
configs = store.select_all_configs()
repos = store.select_all_repos()
repos = db.execute('SELECT repo, ref, path FROM repos').fetchall()
all_repos = {(repo, ref): path for repo, ref, path in repos}
unused_repos = set(all_repos)
# delete config paths which do not exist
dead_configs = [p for p in configs if not os.path.exists(p)]
live_configs = [p for p in configs if os.path.exists(p)]
configs_rows = db.execute('SELECT path FROM configs').fetchall()
configs = [path for path, in configs_rows]
all_repos = {(repo, ref): path for repo, ref, path in repos}
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 = []
for config_path in 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)
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)
store.delete_configs(dead_configs)
for db_repo_name, ref in unused_repos:
store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)])
return len(unused_repos)
def gc(store: Store) -> int:
output.write_line(f'{_gc(store)} repo(s) removed.')
def gc(store):
with store.exclusive_lock():
repos_removed = _gc_repos(store)
output.write_line('{} repo(s) removed.'.format(repos_removed))
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,272 +0,0 @@
from __future__ import annotations
import argparse
import os.path
import subprocess
import sys
from collections.abc import Sequence
from pre_commit.commands.run import run
from pre_commit.envcontext import envcontext
from pre_commit.parse_shebang import normalize_cmd
from pre_commit.store import Store
Z40 = '0' * 40
def _run_legacy(
hook_type: str,
hook_dir: str,
args: Sequence[str],
) -> tuple[int, bytes]:
if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'):
raise SystemExit(
f"bug: pre-commit's script is installed in migration mode\n"
f'run `pre-commit install -f --hook-type {hook_type}` to fix '
f'this\n\n'
f'Please report this bug at '
f'https://github.com/pre-commit/pre-commit/issues',
)
if hook_type == 'pre-push':
stdin = sys.stdin.buffer.read()
else:
stdin = b''
# not running in legacy mode
legacy_hook = os.path.join(hook_dir, f'{hook_type}.legacy')
if not os.access(legacy_hook, os.X_OK):
return 0, stdin
with envcontext((('PRE_COMMIT_RUNNING_LEGACY', '1'),)):
cmd = normalize_cmd((legacy_hook, *args))
return subprocess.run(cmd, input=stdin).returncode, stdin
def _validate_config(
retv: int,
config: str,
skip_on_missing_config: bool,
) -> None:
if not os.path.isfile(config):
if skip_on_missing_config or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'):
print(f'`{config}` config file not found. Skipping `pre-commit`.')
raise SystemExit(retv)
else:
print(
f'No {config} file was found\n'
f'- To temporarily silence this, run '
f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'
f'- To permanently silence this, install pre-commit with the '
f'--allow-missing-config option\n'
f'- To uninstall pre-commit run `pre-commit uninstall`',
)
raise SystemExit(1)
def _ns(
hook_type: str,
color: bool,
*,
all_files: bool = False,
remote_branch: str | None = None,
local_branch: str | None = None,
from_ref: str | None = None,
to_ref: str | None = None,
pre_rebase_upstream: str | None = None,
pre_rebase_branch: str | None = None,
remote_name: str | None = None,
remote_url: str | None = None,
commit_msg_filename: str | None = None,
prepare_commit_message_source: str | None = 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:
return argparse.Namespace(
color=color,
hook_stage=hook_type,
remote_branch=remote_branch,
local_branch=local_branch,
from_ref=from_ref,
to_ref=to_ref,
pre_rebase_upstream=pre_rebase_upstream,
pre_rebase_branch=pre_rebase_branch,
remote_name=remote_name,
remote_url=remote_url,
commit_msg_filename=commit_msg_filename,
prepare_commit_message_source=prepare_commit_message_source,
commit_object_name=commit_object_name,
all_files=all_files,
checkout_type=checkout_type,
is_squash_merge=is_squash_merge,
rewrite_command=rewrite_command,
files=(),
hook=None,
verbose=False,
show_diff_on_failure=False,
fail_fast=False,
)
def _rev_exists(rev: str) -> bool:
return not subprocess.call(('git', 'rev-list', '--quiet', rev))
def _pre_push_ns(
color: bool,
args: Sequence[str],
stdin: bytes,
) -> argparse.Namespace | None:
remote_name = args[0]
remote_url = args[1]
for line in stdin.decode().splitlines():
parts = line.rsplit(maxsplit=3)
local_branch, local_sha, remote_branch, remote_sha = parts
if local_sha == Z40:
continue
elif remote_sha != Z40 and _rev_exists(remote_sha):
return _ns(
'pre-push', color,
from_ref=remote_sha, to_ref=local_sha,
remote_branch=remote_branch,
local_branch=local_branch,
remote_name=remote_name, remote_url=remote_url,
)
else:
# ancestors not found in remote
ancestors = subprocess.check_output((
'git', 'rev-list', local_sha, '--topo-order', '--reverse',
'--not', f'--remotes={remote_name}',
)).decode().strip()
if not ancestors:
continue
else:
first_ancestor = ancestors.splitlines()[0]
cmd = ('git', 'rev-list', '--max-parents=0', local_sha)
roots = set(subprocess.check_output(cmd).decode().splitlines())
if first_ancestor in roots:
# pushing the whole tree including root commit
return _ns(
'pre-push', color,
all_files=True,
remote_name=remote_name, remote_url=remote_url,
remote_branch=remote_branch,
local_branch=local_branch,
)
else:
rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^')
source = subprocess.check_output(rev_cmd).decode().strip()
return _ns(
'pre-push', color,
from_ref=source, to_ref=local_sha,
remote_name=remote_name, remote_url=remote_url,
remote_branch=remote_branch,
local_branch=local_branch,
)
# nothing to push
return None
_EXPECTED_ARG_LENGTH_BY_HOOK = {
'commit-msg': 1,
'post-checkout': 3,
'post-commit': 0,
'pre-commit': 0,
'pre-merge-commit': 0,
'post-merge': 1,
'post-rewrite': 1,
'pre-push': 2,
}
def _check_args_length(hook_type: str, args: Sequence[str]) -> None:
if hook_type == 'prepare-commit-msg':
if len(args) < 1 or len(args) > 3:
raise SystemExit(
f'hook-impl for {hook_type} expected 1, 2, or 3 arguments '
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:
expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type]
if len(args) != expected:
arguments_s = 'argument' if expected == 1 else 'arguments'
raise SystemExit(
f'hook-impl for {hook_type} expected {expected} {arguments_s} '
f'but got {len(args)}: {args}',
)
else:
raise AssertionError(f'unexpected hook type: {hook_type}')
def _run_ns(
hook_type: str,
color: bool,
args: Sequence[str],
stdin: bytes,
) -> argparse.Namespace | None:
_check_args_length(hook_type, args)
if hook_type == 'pre-push':
return _pre_push_ns(color, args, stdin)
elif hook_type in 'commit-msg':
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'}:
return _ns(hook_type, color)
elif hook_type == 'post-checkout':
return _ns(
hook_type, color,
from_ref=args[0], to_ref=args[1], checkout_type=args[2],
)
elif hook_type == 'post-merge':
return _ns(hook_type, color, is_squash_merge=args[0])
elif hook_type == 'post-rewrite':
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:
raise AssertionError(f'unexpected hook type: {hook_type}')
def hook_impl(
store: Store,
*,
config: str,
color: bool,
hook_type: str,
hook_dir: str,
skip_on_missing_config: bool,
args: Sequence[str],
) -> int:
retv, stdin = _run_legacy(hook_type, hook_dir, args)
_validate_config(retv, config, skip_on_missing_config)
ns = _run_ns(hook_type, color, args, stdin)
if ns is None:
return retv
else:
return retv | run(config, store, ns)

View file

@ -1,30 +1,17 @@
from __future__ import annotations
import logging
import os.path
from pre_commit.commands.install_uninstall import install
from pre_commit.store import Store
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
logger = logging.getLogger('pre_commit')
def init_templatedir(
config_file: str,
store: Store,
directory: str,
hook_types: list[str] | None,
skip_on_missing_config: bool = True,
) -> int:
def init_templatedir(config_file, store, directory, hook_types):
install(
config_file,
store,
hook_types=hook_types,
overwrite=True,
skip_on_missing_config=skip_on_missing_config,
git_dir=directory,
config_file, store, hook_types=hook_types,
overwrite=True, skip_on_missing_config=True, git_dir=directory,
)
try:
_, out, _ = cmd_output('git', 'config', 'init.templateDir')
@ -35,5 +22,6 @@ def init_templatedir(
dest = os.path.realpath(directory)
if configured_path != dest:
logger.warning('`init.templateDir` not set to the target directory')
logger.warning(f'maybe `git config --global init.templateDir {dest}`?')
return 0
logger.warning(
'maybe `git config --global init.templateDir {}`?'.format(dest),
)

View file

@ -1,19 +1,20 @@
from __future__ import annotations
from __future__ import print_function
from __future__ import unicode_literals
import io
import itertools
import logging
import os.path
import shlex
import shutil
import sys
from pre_commit import git
from pre_commit import output
from pre_commit.clientlib import InvalidConfigError
from pre_commit.clientlib import load_config
from pre_commit.repository import all_hooks
from pre_commit.repository import install_hook_envs
from pre_commit.store import Store
from pre_commit.util import make_executable
from pre_commit.util import mkdirp
from pre_commit.util import resource_text
@ -21,56 +22,57 @@ logger = logging.getLogger(__name__)
# This is used to identify the hook file we install
PRIOR_HASHES = (
b'4d9958c90bc262f47553e2c073f14cfe',
b'd8ee923c46731b42cd95cc869add4062',
b'49fd668cb42069aa1b6048464be5d395',
b'79f09a650522a87b0da915d0d983b2de',
b'e358c9dae00eac5d06b38dfdb1e33a8c',
'4d9958c90bc262f47553e2c073f14cfe',
'd8ee923c46731b42cd95cc869add4062',
'49fd668cb42069aa1b6048464be5d395',
'79f09a650522a87b0da915d0d983b2de',
'e358c9dae00eac5d06b38dfdb1e33a8c',
)
CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03'
CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03'
TEMPLATE_START = '# start templated\n'
TEMPLATE_END = '# end templated\n'
def _hook_types(cfg_filename: str, hook_types: list[str] | None) -> list[str]:
if hook_types is not None:
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(
hook_type: str,
git_dir: str | None = None,
) -> tuple[str, str]:
git_dir = git_dir if git_dir is not None else git.get_git_common_dir()
def _hook_paths(hook_type, git_dir=None):
git_dir = git_dir if git_dir is not None else git.get_git_dir()
pth = os.path.join(git_dir, 'hooks', hook_type)
return pth, f'{pth}.legacy'
return pth, '{}.legacy'.format(pth)
def is_our_script(filename: str) -> bool:
if not os.path.exists(filename): # pragma: win32 no cover (symlink)
def is_our_script(filename):
if not os.path.exists(filename): # pragma: windows no cover (symlink)
return False
with open(filename, 'rb') as f:
with io.open(filename) as f:
contents = f.read()
return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES)
def shebang():
if sys.platform == 'win32':
py = 'python'
else:
# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH`
path_choices = [p for p in os.defpath.split(os.pathsep) if p]
exe_choices = [
'python{}'.format('.'.join(str(v) for v in sys.version_info[:i]))
for i in range(3)
]
for path, exe in itertools.product(path_choices, exe_choices):
if os.path.exists(os.path.join(path, exe)):
py = exe
break
else:
py = 'python'
return '#!/usr/bin/env {}'.format(py)
def _install_hook_script(
config_file: str,
hook_type: str,
overwrite: bool = False,
skip_on_missing_config: bool = False,
git_dir: str | None = None,
) -> None:
config_file, hook_type,
overwrite=False, skip_on_missing_config=False, git_dir=None,
):
hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir)
os.makedirs(os.path.dirname(hook_path), exist_ok=True)
mkdirp(os.path.dirname(hook_path))
# If we have an existing hook, move it to pre-commit.legacy
if os.path.lexists(hook_path) and not is_our_script(hook_path):
@ -81,53 +83,47 @@ def _install_hook_script(
os.remove(legacy_path)
elif os.path.exists(legacy_path):
output.write_line(
f'Running in migration mode with existing hooks at {legacy_path}\n'
f'Use -f to use only pre-commit.',
'Running in migration mode with existing hooks at {}\n'
'Use -f to use only pre-commit.'.format(legacy_path),
)
args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}']
if skip_on_missing_config:
args.append('--skip-on-missing-config')
params = {
'CONFIG': config_file,
'HOOK_TYPE': hook_type,
'INSTALL_PYTHON': sys.executable,
'SKIP_ON_MISSING_CONFIG': skip_on_missing_config,
}
with open(hook_path, 'w') as hook_file:
with io.open(hook_path, 'w') as hook_file:
contents = resource_text('hook-tmpl')
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
# 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')
before = before.replace('#!/usr/bin/env python3', shebang())
hook_file.write(before + TEMPLATE_START)
hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n')
args_s = shlex.join(args)
hook_file.write(f'ARGS=({args_s})\n')
for line in to_template.splitlines():
var = line.split()[0]
hook_file.write('{} = {!r}\n'.format(var, params[var]))
hook_file.write(TEMPLATE_END + after)
make_executable(hook_path)
output.write_line(f'pre-commit installed at {hook_path}')
output.write_line('pre-commit installed at {}'.format(hook_path))
def install(
config_file: str,
store: Store,
hook_types: list[str] | None,
overwrite: bool = False,
hooks: bool = False,
skip_on_missing_config: bool = False,
git_dir: str | None = None,
) -> int:
if git_dir is None and git.has_core_hookpaths_set():
config_file, store, hook_types,
overwrite=False, hooks=False,
skip_on_missing_config=False, git_dir=None,
):
if git.has_core_hookpaths_set():
logger.error(
'Cowardly refusing to install hooks with `core.hooksPath` set.\n'
'hint: `git config --unset-all core.hooksPath`',
)
return 1
for hook_type in _hook_types(config_file, hook_types):
for hook_type in hook_types:
_install_hook_script(
config_file, hook_type,
overwrite=overwrite,
@ -141,12 +137,11 @@ def install(
return 0
def install_hooks(config_file: str, store: Store) -> int:
def install_hooks(config_file, store):
install_hook_envs(all_hooks(load_config(config_file), store), store)
return 0
def _uninstall_hook_script(hook_type: str) -> None:
def _uninstall_hook_script(hook_type): # type: (str) -> None
hook_path, legacy_path = _hook_paths(hook_type)
# If our file doesn't exist or it isn't ours, gtfo.
@ -154,14 +149,14 @@ def _uninstall_hook_script(hook_type: str) -> None:
return
os.remove(hook_path)
output.write_line(f'{hook_type} uninstalled')
output.write_line('{} uninstalled'.format(hook_type))
if os.path.exists(legacy_path):
os.replace(legacy_path, hook_path)
output.write_line(f'Restored previous hooks to {hook_path}')
os.rename(legacy_path, hook_path)
output.write_line('Restored previous hooks to {}'.format(hook_path))
def uninstall(config_file: str, hook_types: list[str] | None) -> int:
for hook_type in _hook_types(config_file, hook_types):
def uninstall(hook_types):
for hook_type in hook_types:
_uninstall_hook_script(hook_type)
return 0

View file

@ -1,135 +1,62 @@
from __future__ import annotations
from __future__ import print_function
from __future__ import unicode_literals
import functools
import itertools
import textwrap
from collections.abc import Callable
import io
import re
import cfgv
import yaml
from yaml.nodes import ScalarNode
from pre_commit.clientlib import InvalidConfigError
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
from aspy.yaml import ordered_load
def _is_header_line(line: str) -> bool:
return line.startswith(('#', '---')) or not line.strip()
def _indent(s):
lines = s.splitlines(True)
return ''.join(' ' * 4 + line if line.strip() else line for line in lines)
def _migrate_map(contents: str) -> str:
if isinstance(yaml_load(contents), list):
# Find the first non-header line
lines = contents.splitlines(True)
i = 0
# Only loop on non empty configuration file
while i < len(lines) and _is_header_line(lines[i]):
i += 1
def _is_header_line(line):
return (line.startswith(('#', '---')) or not line.strip())
header = ''.join(lines[:i])
rest = ''.join(lines[i:])
def _migrate_map(contents):
# Find the first non-header line
lines = contents.splitlines(True)
i = 0
# Only loop on non empty configuration file
while i < len(lines) and _is_header_line(lines[i]):
i += 1
header = ''.join(lines[:i])
rest = ''.join(lines[i:])
if isinstance(ordered_load(contents), list):
# If they are using the "default" flow style of yaml, this operation
# will yield a valid configuration
try:
trial_contents = f'{header}repos:\n{rest}'
yaml_load(trial_contents)
trial_contents = header + 'repos:\n' + rest
ordered_load(trial_contents)
contents = trial_contents
except yaml.YAMLError:
contents = f'{header}repos:\n{textwrap.indent(rest, " " * 4)}'
contents = header + 'repos:\n' + _indent(rest)
return contents
def _preserve_style(n: ScalarNode, *, s: str) -> str:
style = n.style or ''
return f'{style}{s}{style}'
def _migrate_sha_to_rev(contents):
reg = re.compile(r'(\n\s+)sha:')
return reg.sub(r'\1rev:', contents)
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:
with open(config_file) as f:
def migrate_config(config_file, quiet=False):
with io.open(config_file) as f:
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_composed(contents)
contents = _migrate_sha_to_rev(contents)
if contents != orig_contents:
with open(config_file, 'w') as f:
with io.open(config_file, 'w') as f:
f.write(contents)
print('Configuration has been migrated.')
elif not quiet:
print('Configuration is already migrated.')
return 0

View file

@ -1,248 +1,184 @@
from __future__ import annotations
from __future__ import unicode_literals
import argparse
import contextlib
import functools
import logging
import os
import re
import subprocess
import time
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
import sys
from identify.identify import tags_from_path
from pre_commit import color
from pre_commit import git
from pre_commit import output
from pre_commit.all_languages import languages
from pre_commit.clientlib import load_config
from pre_commit.hook import Hook
from pre_commit.output import get_hook_message
from pre_commit.repository import all_hooks
from pre_commit.repository import install_hook_envs
from pre_commit.staged_files_only import staged_files_only
from pre_commit.store import Store
from pre_commit.util import cmd_output_b
from pre_commit.util import noop_context
logger = logging.getLogger('pre_commit')
def _len_cjk(msg: str) -> int:
widths = {'A': 1, 'F': 2, 'H': 1, 'N': 1, 'Na': 1, 'W': 2}
return sum(widths[unicodedata.east_asian_width(c)] for c in msg)
def _start_msg(*, start: str, cols: int, end_len: int) -> str:
dots = '.' * (cols - _len_cjk(start) - end_len - 1)
return f'{start}{dots}'
def _full_msg(
*,
start: str,
cols: int,
end_msg: str,
end_color: str,
use_color: bool,
postfix: str = '',
) -> str:
dots = '.' * (cols - _len_cjk(start) - len(postfix) - len(end_msg) - 1)
end = color.format_color(end_msg, end_color, use_color)
return f'{start}{dots}{postfix}{end}\n'
def filter_by_include_exclude(
names: Iterable[str],
include: str,
exclude: str,
) -> Generator[str]:
def filter_by_include_exclude(names, include, exclude):
include_re, exclude_re = re.compile(include), re.compile(exclude)
return (
return [
filename for filename in names
if include_re.search(filename)
if not exclude_re.search(filename)
)
]
class Classifier:
def __init__(self, filenames: Iterable[str]) -> None:
self.filenames = [f for f in filenames if os.path.lexists(f)]
@functools.cache
def _types_for_file(self, filename: str) -> set[str]:
return tags_from_path(filename)
def by_types(
self,
names: Iterable[str],
types: Iterable[str],
types_or: Iterable[str],
exclude_types: Iterable[str],
) -> Generator[str]:
types = frozenset(types)
types_or = frozenset(types_or)
exclude_types = frozenset(exclude_types)
for filename in names:
tags = self._types_for_file(filename)
if (
tags >= types and
(not types_or or tags & types_or) and
not tags & exclude_types
):
yield filename
def filenames_for_hook(self, hook: Hook) -> Generator[str]:
return self.by_types(
filter_by_include_exclude(
self.filenames,
hook.files,
hook.exclude,
),
hook.types,
hook.types_or,
hook.exclude_types,
)
@classmethod
def from_config(
cls,
filenames: Iterable[str],
include: str,
exclude: str,
) -> Classifier:
class Classifier(object):
def __init__(self, filenames):
# on windows we normalize all filenames to use forward slashes
# this makes it easier to filter using the `files:` regex
# this also makes improperly quoted shell-based hooks work better
# see #1173
if os.altsep == '/' and os.sep == '\\':
filenames = (f.replace(os.sep, os.altsep) for f in filenames)
filenames = filter_by_include_exclude(filenames, include, exclude)
return Classifier(filenames)
self.filenames = [f for f in filenames if os.path.lexists(f)]
self._types_cache = {}
def _types_for_file(self, filename):
try:
return self._types_cache[filename]
except KeyError:
ret = self._types_cache[filename] = tags_from_path(filename)
return ret
def by_types(self, names, types, exclude_types):
types, exclude_types = frozenset(types), frozenset(exclude_types)
ret = []
for filename in names:
tags = self._types_for_file(filename)
if tags >= types and not tags & exclude_types:
ret.append(filename)
return ret
def filenames_for_hook(self, hook):
names = self.filenames
names = filter_by_include_exclude(names, hook.files, hook.exclude)
names = self.by_types(names, hook.types, hook.exclude_types)
return names
def _get_skips(environ: MutableMapping[str, str]) -> set[str]:
def _get_skips(environ):
skips = environ.get('SKIP', '')
return {skip.strip() for skip in skips.split(',') if skip.strip()}
def _hook_msg_start(hook, verbose):
return '{}{}'.format('[{}] '.format(hook.id) if verbose else '', hook.name)
SKIPPED = 'Skipped'
NO_FILES = '(no files to check)'
def _subtle_line(s: str, use_color: bool) -> None:
output.write_line(color.format_color(s, color.SUBTLE, use_color))
def _run_single_hook(classifier, hook, args, skips, cols, use_color):
filenames = classifier.filenames_for_hook(hook)
def _run_single_hook(
classifier: Classifier,
hook: Hook,
skips: set[str],
cols: int,
diff_before: bytes,
verbose: bool,
use_color: bool,
) -> tuple[bool, bytes]:
filenames = tuple(classifier.filenames_for_hook(hook))
if hook.language == 'pcre':
logger.warning(
'`{}` (from {}) uses the deprecated pcre language.\n'
'The pcre language is scheduled for removal in pre-commit 2.x.\n'
'The pygrep language is a more portable (and usually drop-in) '
'replacement.'.format(hook.id, hook.src),
)
if hook.id in skips or hook.alias in skips:
output.write(
_full_msg(
start=hook.name,
get_hook_message(
_hook_msg_start(hook, args.verbose),
end_msg=SKIPPED,
end_color=color.YELLOW,
use_color=use_color,
use_color=args.color,
cols=cols,
),
)
duration = None
retcode = 0
diff_after = diff_before
files_modified = False
out = b''
return 0
elif not filenames and not hook.always_run:
output.write(
_full_msg(
start=hook.name,
get_hook_message(
_hook_msg_start(hook, args.verbose),
postfix=NO_FILES,
end_msg=SKIPPED,
end_color=color.TURQUOISE,
use_color=use_color,
use_color=args.color,
cols=cols,
),
)
duration = None
retcode = 0
diff_after = diff_before
files_modified = False
out = b''
return 0
# Print the hook and the dots first in case the hook takes hella long to
# run.
output.write(
get_hook_message(
_hook_msg_start(hook, args.verbose), end_len=6, cols=cols,
),
)
sys.stdout.flush()
diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None)
filenames = tuple(filenames) if hook.pass_filenames else ()
retcode, out = hook.run(filenames, use_color)
diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None)
file_modifications = diff_before != diff_after
# If the hook makes changes, fail the commit
if file_modifications:
retcode = 1
if retcode:
retcode = 1
print_color = color.RED
pass_fail = 'Failed'
else:
# print hook and dots first in case the hook takes a while to run
output.write(_start_msg(start=hook.name, end_len=6, cols=cols))
retcode = 0
print_color = color.GREEN
pass_fail = 'Passed'
if not hook.pass_filenames:
filenames = ()
time_before = time.monotonic()
language = languages[hook.language]
with language.in_env(hook.prefix, hook.language_version):
retcode, out = language.run_hook(
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()
output.write_line(color.format_color(pass_fail, print_color, args.color))
# if the hook makes changes, fail the commit
files_modified = diff_before != diff_after
if retcode or files_modified:
print_color = color.RED
status = 'Failed'
else:
print_color = color.GREEN
status = 'Passed'
output.write_line(color.format_color(status, print_color, use_color))
if verbose or hook.verbose or retcode or files_modified:
_subtle_line(f'- hook id: {hook.id}', use_color)
if (verbose or hook.verbose) and duration is not None:
_subtle_line(f'- duration: {duration}s', use_color)
if retcode:
_subtle_line(f'- exit code: {retcode}', use_color)
if (
(out or file_modifications) and
(retcode or args.verbose or hook.verbose)
):
output.write_line('hookid: {}\n'.format(hook.id))
# Print a message if failing due to file modifications
if files_modified:
_subtle_line('- files were modified by this hook', use_color)
if file_modifications:
output.write('Files were modified by this hook.')
if out:
output.write_line(' Additional output:')
output.write_line()
if out.strip():
output.write_line()
output.write_line_b(out.strip(), logfile_name=hook.log_file)
output.write_line()
output.write_line(out.strip(), logfile_name=hook.log_file)
output.write_line()
return files_modified or bool(retcode), diff_after
return retcode
def _compute_cols(hooks: Sequence[Hook]) -> int:
def _compute_cols(hooks, verbose):
"""Compute the number of columns to display hook messages. The widest
that will be displayed is in the no files skipped case:
Hook name...(no files to check) Skipped
or in the verbose case
Hook name [hookid]...(no files to check) Skipped
"""
if hooks:
name_len = max(_len_cjk(hook.name) for hook in hooks)
name_len = max(len(_hook_msg_start(hook, verbose)) for hook in hooks)
else:
name_len = 0
@ -250,17 +186,11 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
return max(cols, 80)
def _all_filenames(args: argparse.Namespace) -> Iterable[str]:
# these hooks do not operate on files
if args.hook_stage in {
'post-checkout', 'post-commit', 'post-merge', 'post-rewrite',
'pre-rebase',
}:
return ()
def _all_filenames(args):
if args.origin and args.source:
return git.get_changed_files(args.origin, args.source)
elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}:
return (args.commit_msg_filename,)
elif args.from_ref and args.to_ref:
return git.get_changed_files(args.from_ref, args.to_ref)
elif args.files:
return args.files
elif args.all_files:
@ -271,37 +201,21 @@ def _all_filenames(args: argparse.Namespace) -> Iterable[str]:
return git.get_staged_files()
def _get_diff() -> bytes:
_, out, _ = cmd_output_b(
'git', 'diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules',
check=False,
)
return out
def _run_hooks(
config: dict[str, Any],
hooks: Sequence[Hook],
skips: set[str],
args: argparse.Namespace,
) -> int:
def _run_hooks(config, hooks, args, environ):
"""Actually run the hooks."""
cols = _compute_cols(hooks)
classifier = Classifier.from_config(
_all_filenames(args), config['files'], config['exclude'],
)
skips = _get_skips(environ)
cols = _compute_cols(hooks, args.verbose)
filenames = _all_filenames(args)
filenames = filter_by_include_exclude(filenames, '', config['exclude'])
classifier = Classifier(filenames)
retval = 0
prior_diff = _get_diff()
for hook in hooks:
current_retval, prior_diff = _run_single_hook(
classifier, hook, skips, cols, prior_diff,
verbose=args.verbose, use_color=args.color,
retval |= _run_single_hook(
classifier, hook, args, skips, cols, args.color,
)
retval |= current_retval
fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast)
if current_retval and fail_fast:
if retval and config['fail_fast']:
break
if retval and args.show_diff_on_failure and prior_diff:
if retval and args.show_diff_on_failure and git.has_diff():
if args.all_files:
output.write_line(
'pre-commit hook(s) made changes.\n'
@ -313,113 +227,56 @@ def _run_hooks(
output.write_line('All changes made by hooks:')
# args.color is a boolean.
# See user_color function in color.py
git_color_opt = 'always' if args.color else 'never'
subprocess.call((
'git', '--no-pager', 'diff', '--no-ext-diff',
f'--color={git_color_opt}',
'--color={}'.format({True: 'always', False: 'never'}[args.color]),
))
return retval
def _has_unmerged_paths() -> bool:
def _has_unmerged_paths():
_, stdout, _ = cmd_output_b('git', 'ls-files', '--unmerged')
return bool(stdout.strip())
def _has_unstaged_config(config_file: str) -> bool:
def _has_unstaged_config(config_file):
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.
return retcode == 1
def run(
config_file: str,
store: Store,
args: argparse.Namespace,
environ: MutableMapping[str, str] = os.environ,
) -> int:
stash = not args.all_files and not args.files
def run(config_file, store, args, environ=os.environ):
no_stash = args.all_files or bool(args.files)
# 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.')
return 1
if bool(args.from_ref) != bool(args.to_ref):
logger.error('Specify both --from-ref and --to-ref.')
if bool(args.source) != bool(args.origin):
logger.error('Specify both --origin and --source.')
return 1
if stash and _has_unstaged_config(config_file):
if _has_unstaged_config(config_file) and not no_stash:
logger.error(
f'Your pre-commit configuration is unstaged.\n'
f'`git add {config_file}` to fix this.',
'Your pre-commit configuration is unstaged.\n'
'`git add {}` to fix this.'.format(config_file),
)
return 1
if (
args.hook_stage in {'prepare-commit-msg', 'commit-msg'} and
not args.commit_msg_filename
):
logger.error(
f'`--commit-msg-filename` is required for '
f'`--hook-stage {args.hook_stage}`',
)
return 1
# prevent recursive post-checkout hooks (#1418)
if (
args.hook_stage == 'post-checkout' and
environ.get('_PRE_COMMIT_SKIP_POST_CHECKOUT')
):
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
)
# Expose origin / source as environment variables for hooks to consume
if args.origin and args.source:
environ['PRE_COMMIT_ORIGIN'] = args.origin
environ['PRE_COMMIT_SOURCE'] = args.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
if args.from_ref and args.to_ref:
# legacy names
environ['PRE_COMMIT_ORIGIN'] = args.from_ref
environ['PRE_COMMIT_SOURCE'] = args.to_ref
# new names
environ['PRE_COMMIT_FROM_REF'] = args.from_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 (
args.remote_name and args.remote_url and
args.remote_branch and args.local_branch
):
environ['PRE_COMMIT_LOCAL_BRANCH'] = args.local_branch
environ['PRE_COMMIT_REMOTE_BRANCH'] = args.remote_branch
environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name
environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url
if args.checkout_type:
environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type
if args.is_squash_merge:
environ['PRE_COMMIT_IS_SQUASH_MERGE'] = args.is_squash_merge
if args.rewrite_command:
environ['PRE_COMMIT_REWRITE_COMMAND'] = args.rewrite_command
# Set pre_commit flag
environ['PRE_COMMIT'] = '1'
with contextlib.ExitStack() as exit_stack:
if stash:
exit_stack.enter_context(staged_files_only(store.directory))
if no_stash:
ctx = noop_context()
else:
ctx = staged_files_only(store.directory)
with ctx:
config = load_config(config_file)
hooks = [
hook
@ -430,19 +287,12 @@ def run(
if args.hook and not hooks:
output.write_line(
f'No hook with id `{args.hook}` in stage `{args.hook_stage}`',
'No hook with id `{}` in stage `{}`'.format(
args.hook, args.hook_stage,
),
)
return 1
skips = _get_skips(environ)
to_install = [
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(hooks, store)
return _run_hooks(config, hooks, skips, args)
# https://github.com/python/mypy/issues/7726
raise AssertionError('unreachable')
return _run_hooks(config, hooks, args, environ)

View file

@ -1,10 +1,18 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
# 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 = '''\
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
rev: v2.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@ -13,6 +21,6 @@ repos:
'''
def sample_config() -> int:
def sample_config():
print(SAMPLE_CONFIG, end='')
return 0

View file

@ -1,9 +1,11 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import argparse
import collections
import logging
import os.path
import tempfile
from aspy.yaml import ordered_dump
import pre_commit.constants as C
from pre_commit import git
@ -12,15 +14,15 @@ from pre_commit.clientlib import load_manifest
from pre_commit.commands.run import run
from pre_commit.store import Store
from pre_commit.util import cmd_output_b
from pre_commit.util import tmpdir
from pre_commit.xargs import xargs
from pre_commit.yaml import yaml_dump
logger = logging.getLogger(__name__)
def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]:
def _repo_ref(tmpdir, repo, ref):
# if `ref` is explicitly passed, use it
if ref is not None:
if ref:
return repo, ref
ref = git.head_rev(repo)
@ -48,8 +50,8 @@ def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]:
return repo, ref
def try_repo(args: argparse.Namespace) -> int:
with tempfile.TemporaryDirectory() as tempdir:
def try_repo(args):
with tmpdir() as tempdir:
repo, ref = _repo_ref(tempdir, args.repo, args.ref)
store = Store(tempdir)
@ -61,8 +63,9 @@ def try_repo(args: argparse.Namespace) -> int:
manifest = sorted(manifest, key=lambda hook: hook['id'])
hooks = [{'id': hook['id']} for hook in manifest]
config = {'repos': [{'repo': repo, 'rev': ref, 'hooks': hooks}]}
config_s = yaml_dump(config)
items = (('repo', repo), ('rev', ref), ('hooks', hooks))
config = {'repos': [collections.OrderedDict(items)]}
config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS)
config_filename = os.path.join(tempdir, C.CONFIG_FILE)
with open(config_filename, 'w') as cfg:

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,31 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import importlib.metadata
import sys
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'
MANIFEST_FILE = '.pre-commit-hooks.yaml'
YAML_DUMP_KWARGS = {
'default_flow_style': False,
# Use unicode
'encoding': None,
'indent': 4,
}
# Bump when installation changes in a backwards / forwards incompatible way
INSTALLED_STATE_VERSION = '1'
# Bump when modifying `empty_template`
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', 'prepare-commit-msg', 'commit-msg', 'manual', 'push')
DEFAULT = 'default'

View file

@ -1,28 +1,19 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import collections
import contextlib
import enum
import os
from collections.abc import Generator
from collections.abc import MutableMapping
from typing import NamedTuple
from typing import Union
_Unset = enum.Enum('_Unset', 'UNSET')
UNSET = _Unset.UNSET
class Var(NamedTuple):
name: str
default: str = ''
UNSET = collections.namedtuple('UNSET', ())()
SubstitutionT = tuple[Union[str, Var], ...]
ValueT = Union[str, _Unset, SubstitutionT]
PatchesT = tuple[tuple[str, ValueT], ...]
Var = collections.namedtuple('Var', ('name', 'default'))
Var.__new__.__defaults__ = ('',)
def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
def format_env(parts, env):
return ''.join(
env.get(part.name, part.default) if isinstance(part, Var) else part
for part in parts
@ -30,10 +21,7 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
@contextlib.contextmanager
def envcontext(
patch: PatchesT,
_env: MutableMapping[str, str] | None = None,
) -> Generator[None]:
def envcontext(patch, _env=None):
"""In this context, `os.environ` is modified according to `patch`.
`patch` is an iterable of 2-tuples (key, value):
@ -45,7 +33,7 @@ def envcontext(
replaced with the previous environment
"""
env = os.environ if _env is None else _env
before = dict(env)
before = env.copy()
for k, v in patch:
if v is UNSET:

View file

@ -1,81 +1,80 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import contextlib
import functools
import os.path
import sys
import traceback
from collections.abc import Generator
from typing import IO
import six
import pre_commit.constants as C
from pre_commit import five
from pre_commit import output
from pre_commit.errors import FatalError
from pre_commit.store import Store
from pre_commit.util import cmd_output_b
from pre_commit.util import force_bytes
def _log_and_exit(
msg: str,
ret_code: int,
exc: BaseException,
formatted: str,
) -> None:
error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc)
output.write_line_b(error_msg)
class FatalError(RuntimeError):
pass
_, git_version_b, _ = cmd_output_b('git', '--version', check=False)
git_version = git_version_b.decode(errors='backslashreplace').rstrip()
storedir = Store().directory
log_path = os.path.join(storedir, 'pre-commit.log')
with contextlib.ExitStack() as ctx:
if os.access(storedir, os.W_OK):
output.write_line(f'Check the log at {log_path}')
log: IO[bytes] = ctx.enter_context(open(log_path, 'wb'))
else: # pragma: win32 no cover
output.write_line(f'Failed to write to log at {log_path}')
log = sys.stdout.buffer
def _to_bytes(exc):
try:
return bytes(exc)
except Exception:
return six.text_type(exc).encode('UTF-8')
_log_line = functools.partial(output.write_line, stream=log)
_log_line_b = functools.partial(output.write_line_b, stream=log)
def _log_and_exit(msg, exc, formatted):
error_msg = b''.join((
five.to_bytes(msg), b': ',
five.to_bytes(type(exc).__name__), b': ',
_to_bytes(exc),
))
output.write_line(error_msg)
store = Store()
log_path = os.path.join(store.directory, 'pre-commit.log')
output.write_line('Check the log at {}'.format(log_path))
with open(log_path, 'wb') as log:
def _log_line(*s): # type: (*str) -> None
output.write_line(*s, stream=log)
_log_line('### version information')
_log_line()
_log_line('```')
_log_line(f'pre-commit version: {C.VERSION}')
_log_line(f'git --version: {git_version}')
_log_line('pre-commit version: {}'.format(C.VERSION))
_log_line('sys.version:')
for line in sys.version.splitlines():
_log_line(f' {line}')
_log_line(f'sys.executable: {sys.executable}')
_log_line(f'os.name: {os.name}')
_log_line(f'sys.platform: {sys.platform}')
_log_line(' {}'.format(line))
_log_line('sys.executable: {}'.format(sys.executable))
_log_line('os.name: {}'.format(os.name))
_log_line('sys.platform: {}'.format(sys.platform))
_log_line('```')
_log_line()
_log_line('### error information')
_log_line()
_log_line('```')
_log_line_b(error_msg)
_log_line(error_msg)
_log_line('```')
_log_line()
_log_line('```')
_log_line(formatted.rstrip())
_log_line(formatted)
_log_line('```')
raise SystemExit(ret_code)
raise SystemExit(1)
@contextlib.contextmanager
def error_handler() -> Generator[None]:
def error_handler():
try:
yield
except (Exception, KeyboardInterrupt) as e:
if isinstance(e, FatalError):
msg, ret_code = 'An error has occurred', 1
msg = 'An error has occurred'
elif isinstance(e, KeyboardInterrupt):
msg, ret_code = 'Interrupted (^C)', 130
msg = 'Interrupted (^C)'
else:
msg, ret_code = 'An unexpected error has occurred', 3
_log_and_exit(msg, ret_code, e, traceback.format_exc())
msg = 'An unexpected error has occurred'
_log_and_exit(msg, e, traceback.format_exc())

View file

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

View file

@ -1,13 +1,11 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import contextlib
import errno
import sys
from collections.abc import Callable
from collections.abc import Generator
if sys.platform == 'win32': # pragma: no cover (windows)
try: # pragma: no cover (windows)
import msvcrt
# https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking
@ -17,18 +15,15 @@ if sys.platform == 'win32': # pragma: no cover (windows)
_region = 0xffff
@contextlib.contextmanager
def _locked(
fileno: int,
blocked_cb: Callable[[], None],
) -> Generator[None]:
def _locked(fileno, blocked_cb):
try:
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
except OSError:
except IOError:
blocked_cb()
while True:
try:
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region)
except OSError as e:
except IOError as e:
# Locking violation. Returned when the _LK_LOCK or _LK_RLCK
# flag is specified and the file cannot be locked after 10
# attempts.
@ -46,17 +41,14 @@ if sys.platform == 'win32': # pragma: no cover (windows)
# "Regions should be locked only briefly and should be unlocked
# before closing a file or exiting the program."
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region)
else: # pragma: win32 no cover
except ImportError: # pragma: windows no cover
import fcntl
@contextlib.contextmanager
def _locked(
fileno: int,
blocked_cb: Callable[[], None],
) -> Generator[None]:
def _locked(fileno, blocked_cb):
try:
fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError: # pragma: no cover (tests are single-threaded)
except IOError: # pragma: no cover (tests are single-threaded)
blocked_cb()
fcntl.flock(fileno, fcntl.LOCK_EX)
try:
@ -66,10 +58,7 @@ else: # pragma: win32 no cover
@contextlib.contextmanager
def lock(
path: str,
blocked_cb: Callable[[], None],
) -> Generator[None]:
def lock(path, blocked_cb):
with open(path, 'a+') as f:
with _locked(f.fileno(), blocked_cb):
yield

15
pre_commit/five.py Normal file
View file

@ -0,0 +1,15 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import six
def to_text(s):
return s if isinstance(s, six.text_type) else s.decode('UTF-8')
def to_bytes(s):
return s if isinstance(s, bytes) else s.encode('UTF-8')
n = to_bytes if six.PY2 else to_text

View file

@ -1,22 +1,17 @@
from __future__ import annotations
from __future__ import unicode_literals
import logging
import os.path
import sys
from collections.abc import Mapping
from pre_commit.errors import FatalError
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
logger = logging.getLogger(__name__)
# see #2046
NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false')
def zsplit(s: str) -> list[str]:
def zsplit(s):
s = s.strip('\0')
if s:
return s.split('\0')
@ -24,7 +19,7 @@ def zsplit(s: str) -> list[str]:
return []
def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]:
def no_git_env(_env=None):
# Too many bugs dealing with environment variables and GIT:
# https://github.com/pre-commit/pre-commit/issues/300
# In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running
@ -37,63 +32,30 @@ def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]:
return {
k: v for k, v in _env.items()
if not k.startswith('GIT_') or
k.startswith(('GIT_CONFIG_KEY_', 'GIT_CONFIG_VALUE_')) or
k in {
'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO',
'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT',
'GIT_HTTP_PROXY_AUTHMETHOD',
'GIT_ALLOW_PROTOCOL',
'GIT_ASKPASS',
}
k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND'}
}
def get_root() -> str:
# Git 2.25 introduced a change to "rev-parse --show-toplevel" that exposed
# underlying volumes for Windows drives mapped with SUBST. We use
# "rev-parse --show-cdup" to get the appropriate path, but must perform
# an extra check to see if we are in the .git directory.
try:
root = os.path.abspath(
cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(),
)
inside_git_dir = cmd_output(
'git', 'rev-parse', '--is-inside-git-dir',
)[1].strip()
except CalledProcessError:
raise FatalError(
'git failed. Is it installed, and are you in a Git repository '
'directory?',
)
if inside_git_dir != 'false':
raise FatalError(
'git toplevel unexpectedly empty! make sure you are not '
'inside the `.git` directory of your repository.',
)
return root
def get_root():
return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip()
def get_git_dir(git_root: str = '.') -> str:
opt = '--git-dir'
_, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root)
git_dir = out.strip()
if git_dir != opt:
return os.path.normpath(os.path.join(git_root, git_dir))
def get_git_dir(git_root='.'):
opts = ('--git-common-dir', '--git-dir')
_, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root)
for line, opt in zip(out.splitlines(), opts):
if line != opt: # pragma: no branch (git < 2.5)
return os.path.normpath(os.path.join(git_root, line))
else:
raise AssertionError('unreachable: no git dir')
def get_git_common_dir(git_root: str = '.') -> str:
opt = '--git-common-dir'
_, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root)
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 get_remote_url(git_root):
_, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)
return out.strip()
def is_in_merge_conflict() -> bool:
def is_in_merge_conflict():
git_dir = get_git_dir('.')
return (
os.path.exists(os.path.join(git_dir, 'MERGE_MSG')) and
@ -101,17 +63,17 @@ 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):
# Conflicted files start with tabs
return [
line.lstrip(b'#').strip().decode()
line.lstrip(b'#').strip().decode('UTF-8')
for line in merge_msg.splitlines()
# '#\t' for git 2.4.1
if line.startswith((b'\t', b'#\t'))
]
def get_conflicted_files() -> set[str]:
def get_conflicted_files():
logger.info('Checking merge-conflict files only.')
# Need to get the conflicted files from the MERGE_MSG because they could
# have resolved the conflict by choosing one side or the other
@ -126,13 +88,13 @@ def get_conflicted_files() -> set[str]:
merge_diff_filenames = zsplit(
cmd_output(
'git', 'diff', '--name-only', '--no-ext-diff', '-z',
'-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--',
'-m', tree_hash, 'HEAD', 'MERGE_HEAD',
)[1],
)
return set(merge_conflict_filenames) | set(merge_diff_filenames)
def get_staged_files(cwd: str | None = None) -> list[str]:
def get_staged_files(cwd=None):
return zsplit(
cmd_output(
'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z',
@ -143,57 +105,60 @@ def get_staged_files(cwd: str | None = None) -> list[str]:
)
def intent_to_add_files() -> list[str]:
_, stdout, _ = cmd_output(
'git', 'diff', '--no-ext-diff', '--ignore-submodules',
'--diff-filter=A', '--name-only', '-z',
)
return zsplit(stdout)
def intent_to_add_files():
_, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z')
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():
return zsplit(cmd_output('git', 'ls-files', '-z')[1])
def get_changed_files(old: str, new: str) -> list[str]:
diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z')
try:
_, out, _ = cmd_output(*diff_cmd, f'{old}...{new}')
except CalledProcessError: # pragma: no cover (new git)
# on newer git where old and new do not have a merge base git fails
# so we try a full diff (this is what old git did for us!)
_, out, _ = cmd_output(*diff_cmd, f'{old}..{new}')
return zsplit(out)
def get_changed_files(new, old):
return zsplit(
cmd_output(
'git', 'diff', '--name-only', '--no-ext-diff', '-z',
'{}...{}'.format(old, new),
)[1],
)
def head_rev(remote: str) -> str:
def head_rev(remote):
_, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD')
return out.split()[0]
def has_diff(*args: str, repo: str = '.') -> bool:
cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args)
return cmd_output_b(*cmd, cwd=repo, check=False)[0] == 1
def has_diff(*args, **kwargs):
repo = kwargs.pop('repo', '.')
assert not kwargs, kwargs
cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args
return cmd_output_b(*cmd, cwd=repo, retcode=None)[0]
def has_core_hookpaths_set() -> bool:
_, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', check=False)
def has_core_hookpaths_set():
_, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None)
return bool(out.strip())
def init_repo(path: str, remote: str) -> None:
def init_repo(path, remote):
if os.path.isdir(remote):
remote = os.path.abspath(remote)
git = ('git', *NO_FS_MONITOR)
env = no_git_env()
# avoid the user's template so that hooks do not recurse
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', 'init', path, env=env)
cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env)
def commit(repo: str = '.') -> None:
def commit(repo='.'):
env = no_git_env()
name, email = 'pre-commit', 'asottile+pre-commit@umich.edu'
env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name
@ -202,44 +167,28 @@ def commit(repo: str = '.') -> None:
cmd_output_b(*cmd, cwd=repo, env=env)
def git_path(name: str, repo: str = '.') -> str:
def git_path(name, repo='.'):
_, out, _ = cmd_output('git', 'rev-parse', '--git-path', name, cwd=repo)
return os.path.join(repo, out.strip())
def check_for_cygwin_mismatch() -> None:
def check_for_cygwin_mismatch():
"""See https://github.com/pre-commit/pre-commit/issues/354"""
if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows)
is_cygwin_python = sys.platform == 'cygwin'
try:
toplevel = get_root()
except FatalError: # skip the check if we're not in a git repo
return
toplevel = cmd_output('git', 'rev-parse', '--show-toplevel')[1]
is_cygwin_git = toplevel.startswith('/')
if is_cygwin_python ^ is_cygwin_git:
exe_type = {True: '(cygwin)', False: '(windows)'}
logger.warning(
f'pre-commit has detected a mix of cygwin python / git\n'
f'This combination is not supported, it is likely you will '
f'receive an error later in the program.\n'
f'Make sure to use cygwin git+python while using cygwin\n'
f'These can be installed through the cygwin installer.\n'
f' - python {exe_type[is_cygwin_python]}\n'
f' - git {exe_type[is_cygwin_git]}\n',
logger.warn(
'pre-commit has detected a mix of cygwin python / git\n'
'This combination is not supported, it is likely you will '
'receive an error later in the program.\n'
'Make sure to use cygwin git+python while using cygwin\n'
'These can be installed through the cygwin installer.\n'
' - python {}\n'
' - git {}\n'.format(
exe_type[is_cygwin_python], exe_type[is_cygwin_git],
),
)
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,60 +0,0 @@
from __future__ import annotations
import logging
from collections.abc import Sequence
from typing import Any
from typing import NamedTuple
from pre_commit.prefix import Prefix
logger = logging.getLogger('pre_commit')
class Hook(NamedTuple):
src: str
prefix: Prefix
id: str
name: str
entry: str
language: str
alias: str
files: str
exclude: str
types: Sequence[str]
types_or: Sequence[str]
exclude_types: Sequence[str]
additional_dependencies: Sequence[str]
args: Sequence[str]
always_run: bool
fail_fast: bool
pass_filenames: bool
description: str
language_version: str
log_file: str
minimum_pre_commit_version: str
require_serial: bool
stages: Sequence[str]
verbose: bool
@property
def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]:
return (
self.prefix,
self.language,
self.language_version,
tuple(self.additional_dependencies),
)
@classmethod
def create(cls, src: str, prefix: Prefix, dct: dict[str, Any]) -> Hook:
# TODO: have cfgv do this (?)
extra_keys = set(dct) - _KEYS
if extra_keys:
logger.warning(
f'Unexpected key(s) present on {src} => {dct["id"]}: '
f'{", ".join(sorted(extra_keys))}',
)
return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS})
_KEYS = frozenset(set(Hook._fields) - {'src', 'prefix'})

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,70 @@
from __future__ import unicode_literals
from pre_commit.languages import docker
from pre_commit.languages import docker_image
from pre_commit.languages import fail
from pre_commit.languages import golang
from pre_commit.languages import node
from pre_commit.languages import pcre
from pre_commit.languages import pygrep
from pre_commit.languages import python
from pre_commit.languages import python_venv
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
# A language implements the following constant and functions in its module:
#
# # Use None for no environment
# ENVIRONMENT_DIR = 'foo_env'
#
# def get_default_version():
# """Return a value to replace the 'default' value for language_version.
#
# return 'default' if there is no better option.
# """
#
# def healthy(prefix, language_version):
# """Return whether or not the environment is considered functional."""
#
# def install_environment(prefix, version, additional_dependencies):
# """Installs a repository in the given repository. Note that the current
# working directory will already be inside the repository.
#
# Args:
# prefix - `Prefix` bound to the repository.
# version - A version specified in the hook configuration or 'default'.
# """
#
# def run_hook(hook, file_args, color):
# """Runs a hook and returns the returncode and output of running that
# hook.
#
# Args:
# hook - `Hook`
# file_args - The files to be run
# color - whether the hook should be given a pty (when supported)
#
# Returns:
# (returncode, output)
# """
languages = {
'docker': docker,
'docker_image': docker_image,
'fail': fail,
'golang': golang,
'node': node,
'pcre': pcre,
'pygrep': pygrep,
'python': python,
'python_venv': python_venv,
'ruby': ruby,
'rust': rust,
'script': script,
'swift': swift,
'system': system,
}
all_languages = sorted(languages)

View file

@ -1,77 +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 SubstitutionT
from pre_commit.envcontext import UNSET
from pre_commit.envcontext import Var
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'conda'
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(env: str) -> PatchesT:
# On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows
# they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin,
# $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only
# seems to be used for python.exe.
path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
if sys.platform == 'win32': # pragma: win32 cover
path = (env, os.pathsep, *path)
path = (os.path.join(env, 'Scripts'), os.pathsep, *path)
path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path)
return (
('PYTHONHOME', UNSET),
('VIRTUAL_ENV', UNSET),
('CONDA_PREFIX', env),
('PATH', 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 _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(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
lang_base.assert_version_default('conda', version)
conda_exe = _conda_exe()
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(
conda_exe, 'install', '-p', env_dir, *additional_dependencies,
cwd=prefix.prefix_dir,
)

View file

@ -1,76 +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.parse_shebang import find_executable
from pre_commit.prefix import Prefix
ENVIRONMENT_DIR = 'coursier'
get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check
run_hook = lang_base.basic_run_hook
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
lang_base.assert_version_default('coursier', version)
# Support both possible executable names (either "cs" or "coursier")
cs = find_executable('cs') or find_executable('coursier')
if cs is None:
raise AssertionError(
'pre-commit requires system-installed "cs" or "coursier" '
'executables in the application search path',
)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
def _install(*opts: str) -> None:
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',
'--channel', channel,
app,
)
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:
return (
('PATH', (target_dir, os.pathsep, Var('PATH'))),
('COURSIER_CACHE', os.path.join(target_dir, '.cs-cache')),
)
@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

View file

@ -1,97 +0,0 @@
from __future__ import annotations
import contextlib
import os.path
import shutil
import tempfile
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 win_exe
from pre_commit.yaml import yaml_load
ENVIRONMENT_DIR = 'dartenv'
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:
return (
('PATH', (os.path.join(venv, 'bin'), 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('dart', version)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
bin_dir = os.path.join(envdir, 'bin')
def _install_dir(prefix_p: Prefix, pub_cache: str) -> None:
dart_env = {**os.environ, 'PUB_CACHE': pub_cache}
with open(prefix_p.path('pubspec.yaml')) as f:
pubspec_contents = yaml_load(f)
lang_base.setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env)
for executable in pubspec_contents['executables']:
lang_base.setup_cmd(
prefix_p,
(
'dart', 'compile', 'exe',
'--output', os.path.join(bin_dir, win_exe(executable)),
prefix_p.path('bin', f'{executable}.dart'),
),
env=dart_env,
)
os.makedirs(bin_dir)
with tempfile.TemporaryDirectory() as tmp:
_install_dir(prefix, tmp)
for dep_s in additional_dependencies:
with tempfile.TemporaryDirectory() as dep_tmp:
dep, _, version = dep_s.partition(':')
if version:
dep_cmd: tuple[str, ...] = (dep, '--version', version)
else:
dep_cmd = (dep,)
lang_base.setup_cmd(
prefix,
('dart', 'pub', 'cache', 'add', *dep_cmd),
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}',
)

View file

@ -1,86 +1,51 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import contextlib
import functools
import hashlib
import json
import os
import re
from collections.abc import Sequence
from pre_commit import lang_base
from pre_commit.prefix import Prefix
import pre_commit.constants as C
from pre_commit import five
from pre_commit.languages import helpers
from pre_commit.util import CalledProcessError
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'docker'
PRE_COMMIT_LABEL = 'PRE_COMMIT'
get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check
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,
)
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
def _get_container_id() -> str | None:
with contextlib.suppress(FileNotFoundError):
with open('/proc/1/mountinfo', 'rb') as f:
for line in f:
m = _HOSTNAME_MOUNT_RE.search(line)
if m:
return m[1].decode()
return None
def md5(s): # pragma: windows no cover
return hashlib.md5(five.to_bytes(s)).hexdigest()
def _get_docker_path(path: str) -> str:
container_id = _get_container_id()
if container_id is None:
return path
try:
_, out, _ = cmd_output_b('docker', 'inspect', container_id)
except CalledProcessError:
# self-container was not visible from here (perhaps docker-in-docker)
return path
container, = json.loads(out)
for mount in container['Mounts']:
src_path = mount['Source']
to_path = mount['Destination']
if os.path.commonpath((path, to_path)) == to_path:
# So there is something in common,
# and we can proceed remapping it
return path.replace(to_path, src_path)
# we're in Docker, but the path is not mounted, cannot really do anything,
# so fall back to original path
return path
def md5(s: str) -> str: # pragma: win32 no cover
return hashlib.md5(s.encode()).hexdigest()
def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover
def docker_tag(prefix): # pragma: windows no cover
md5sum = md5(os.path.basename(prefix.prefix_dir)).lower()
return f'pre-commit-{md5sum}'
return 'pre-commit-{}'.format(md5sum)
def build_docker_image(
prefix: Prefix,
*,
pull: bool,
) -> None: # pragma: win32 no cover
cmd: tuple[str, ...] = (
def docker_is_running(): # pragma: windows no cover
try:
cmd_output_b('docker', 'ps')
except CalledProcessError:
return False
else:
return True
def assert_docker_available(): # pragma: windows no cover
assert docker_is_running(), (
'Docker is either not running or not configured in this environment'
)
def build_docker_image(prefix, **kwargs): # pragma: windows no cover
pull = kwargs.pop('pull')
assert not kwargs, kwargs
cmd = (
'docker', 'build',
'--tag', docker_tag(prefix),
'--label', PRE_COMMIT_LABEL,
@ -89,93 +54,56 @@ def build_docker_image(
cmd += ('--pull',)
# This must come last for old versions of docker. See #477
cmd += ('.',)
lang_base.setup_cmd(prefix, cmd)
helpers.run_setup_cmd(prefix, cmd)
def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None: # pragma: win32 no cover
lang_base.assert_version_default('docker', version)
lang_base.assert_no_additional_deps('docker', additional_dependencies)
prefix, version, additional_dependencies,
): # pragma: windows no cover
helpers.assert_version_default('docker', version)
helpers.assert_no_additional_deps('docker', additional_dependencies)
assert_docker_available()
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
# still needs to cleanup its state files on failure
build_docker_image(prefix, pull=True)
os.mkdir(directory)
# still needs to cleanup it's state files on failure
with clean_path_on_failure(directory):
build_docker_image(prefix, pull=True)
os.mkdir(directory)
@functools.lru_cache(maxsize=1)
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)
def get_docker_user(): # pragma: windows no cover
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:
return ('-u', f'{os.getuid()}:{os.getgid()}')
return '{}:{}'.format(os.getuid(), os.getgid())
except AttributeError:
return ()
return '1000:1000'
def get_docker_tty(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover # noqa: E501
return (('--tty',) if color else ())
def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover
def docker_cmd(): # pragma: windows no cover
return (
'docker', 'run',
'--rm',
*get_docker_tty(color=color),
*get_docker_user(),
'-u', get_docker_user(),
# 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
# unshared label. Only the current container can use a private volume.
'-v', f'{_get_docker_path(os.getcwd())}:/src:rw,Z',
'-v', '{}:/src:rw,Z'.format(os.getcwd()),
'--workdir', '/src',
)
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]: # pragma: win32 no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
assert_docker_available()
# Rebuild the docker image in case it has gone missing, as many people do
# 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)
hook_cmd = hook.cmd
entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:]
entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix))
return lang_base.run_xargs(
(*docker_cmd(color=color), *entry_tag, *cmd_rest),
file_args,
require_serial=require_serial,
color=color,
)
entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix))
cmd = docker_cmd() + entry_tag + cmd_rest
return helpers.run_xargs(hook, cmd, file_args, color=color)

View file

@ -1,32 +1,18 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
from collections.abc import Sequence
from pre_commit import lang_base
from pre_commit.languages import helpers
from pre_commit.languages.docker import assert_docker_available
from pre_commit.languages.docker import docker_cmd
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
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
install_environment = helpers.no_install
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]: # pragma: win32 no cover
cmd = docker_cmd(color=color) + lang_base.hook_cmd(entry, args)
return lang_base.run_xargs(
cmd,
file_args,
require_serial=require_serial,
color=color,
)
def run_hook(hook, file_args, color): # pragma: windows no cover
assert_docker_available()
cmd = docker_cmd() + hook.cmd
return helpers.run_xargs(hook, cmd, file_args, color=color)

View file

@ -1,111 +0,0 @@
from __future__ import annotations
import contextlib
import os.path
import re
import tempfile
import xml.etree.ElementTree
import zipfile
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
ENVIRONMENT_DIR = 'dotnetenv'
BIN_DIR = 'bin'
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:
return (
('PATH', (os.path.join(venv, BIN_DIR), 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
@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(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
lang_base.assert_version_default('dotnet', version)
lang_base.assert_no_additional_deps('dotnet', additional_dependencies)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
build_dir = prefix.path('pre-commit-build')
# Build & pack nupkg file
lang_base.setup_cmd(
prefix,
(
'dotnet', 'pack',
'--configuration', 'Release',
'--property', f'PackageOutputPath={build_dir}',
),
)
nupkg_dir = prefix.path(build_dir)
nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')]
if not nupkgs:
raise AssertionError('could not find any build outputs to install')
for nupkg in nupkgs:
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
with _nuget_config_no_sources() as nuget_config:
lang_base.setup_cmd(
prefix,
(
'dotnet', 'tool', 'install',
'--configfile', nuget_config,
'--tool-path', os.path.join(envdir, BIN_DIR),
'--add-source', build_dir,
tool_id,
),
)

View file

@ -1,27 +1,15 @@
from __future__ import annotations
from __future__ import unicode_literals
from collections.abc import Sequence
from pre_commit.languages import helpers
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
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
install_environment = helpers.no_install
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]:
out = f'{entry}\n\n'.encode()
out += b'\n'.join(f.encode() for f in file_args) + b'\n'
def run_hook(hook, file_args, color):
out = hook.entry.encode('UTF-8') + b'\n\n'
out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n'
return 1, out

View file

@ -1,161 +1,86 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import functools
import json
import os.path
import platform
import shutil
import sys
import tarfile
import tempfile
import urllib.error
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
from pre_commit import lang_base
from pre_commit import git
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var
from pre_commit.git import no_git_env
from pre_commit.prefix import Prefix
from pre_commit.languages import helpers
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
from pre_commit.util import rmtree
ENVIRONMENT_DIR = 'golangenv'
health_check = lang_base.basic_health_check
run_hook = lang_base.basic_run_hook
_ARCH_ALIASES = {
'x86_64': 'amd64',
'i386': '386',
'aarch64': 'arm64',
'armv8': 'arm64',
'armv7l': 'armv6l',
}
_ARCH = platform.machine().lower()
_ARCH = _ARCH_ALIASES.get(_ARCH, _ARCH)
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
class ExtractAll(Protocol):
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'))),
)
def get_env_patch(venv):
return (
('GOROOT', os.path.join(venv, '.go')),
('GOTOOLCHAIN', 'local'),
(
'PATH', (
os.path.join(venv, 'bin'), os.pathsep,
os.path.join(venv, '.go', 'bin'), os.pathsep, Var('PATH'),
),
),
('PATH', (os.path.join(venv, '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
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)):
def in_env(prefix):
envdir = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
with envcontext(get_env_patch(envdir)):
yield
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
if version != 'system':
_install_go(version, env_dir)
if sys.platform == 'cygwin': # pragma: no cover
gopath = cmd_output('cygpath', '-w', env_dir)[1].strip()
def guess_go_dir(remote_url):
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:
gopath = env_dir
return 'unknown_src_dir'
env = no_git_env(dict(os.environ, GOPATH=gopath))
env.pop('GOBIN', None)
if version != 'system':
env['GOTOOLCHAIN'] = 'local'
env['GOROOT'] = os.path.join(env_dir, '.go')
env['PATH'] = os.pathsep.join((
os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'],
))
lang_base.setup_cmd(prefix, ('go', 'install', './...'), env=env)
for dependency in additional_dependencies:
lang_base.setup_cmd(prefix, ('go', 'install', dependency), env=env)
def install_environment(prefix, version, additional_dependencies):
helpers.assert_version_default('golang', version)
directory = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
# save some disk space -- we don't need this after installation
pkgdir = os.path.join(env_dir, 'pkg')
if os.path.exists(pkgdir): # pragma: no branch (always true on windows?)
rmtree(pkgdir)
with clean_path_on_failure(directory):
remote = git.get_remote_url(prefix.prefix_dir)
repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote))
# Clone into the goenv we'll create
helpers.run_setup_cmd(prefix, ('git', 'clone', '.', repo_src_dir))
if sys.platform == 'cygwin': # pragma: no cover
_, gopath, _ = cmd_output('cygpath', '-w', directory)
gopath = gopath.strip()
else:
gopath = directory
env = dict(os.environ, GOPATH=gopath)
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)
def run_hook(hook, file_args, color):
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,86 @@
from __future__ import unicode_literals
import multiprocessing
import os
import random
import six
import pre_commit.constants as C
from pre_commit.util import cmd_output_b
from pre_commit.xargs import xargs
FIXED_RANDOM_SEED = 1542676186
def run_setup_cmd(prefix, cmd):
cmd_output_b(*cmd, cwd=prefix.prefix_dir)
def environment_dir(ENVIRONMENT_DIR, language_version):
if ENVIRONMENT_DIR is None:
return None
else:
return '{}-{}'.format(ENVIRONMENT_DIR, language_version)
def assert_version_default(binary, version):
if version != C.DEFAULT:
raise AssertionError(
'For now, pre-commit requires system-installed {}'.format(binary),
)
def assert_no_additional_deps(lang, additional_deps):
if additional_deps:
raise AssertionError(
'For now, pre-commit does not support '
'additional_dependencies for {}'.format(lang),
)
def basic_get_default_version():
return C.DEFAULT
def basic_healthy(prefix, language_version):
return True
def no_install(prefix, version, additional_dependencies):
raise AssertionError('This type is not installable')
def target_concurrency(hook):
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):
"""Deterministically shuffle identically under both py2 + py3."""
fixed_random = random.Random()
if six.PY2: # pragma: no cover (py2)
fixed_random.seed(FIXED_RANDOM_SEED)
else: # pragma: no cover (py3)
fixed_random.seed(FIXED_RANDOM_SEED, version=1)
seq = list(seq)
random.shuffle(seq, random=fixed_random.random)
return seq
def run_xargs(hook, cmd, file_args, **kwargs):
# 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,110 +1,83 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import functools
import os
import sys
from collections.abc import Generator
from collections.abc import Sequence
import pre_commit.constants as C
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.envcontext import Var
from pre_commit.languages import helpers
from pre_commit.languages.python import bin_dir
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_b
from pre_commit.util import rmtree
ENVIRONMENT_DIR = 'node_env'
run_hook = lang_base.basic_run_hook
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
@functools.lru_cache(maxsize=1)
def get_default_version() -> str:
# nodeenv does not yet support `-n system` on windows
if sys.platform == 'win32':
return C.DEFAULT
# if node is already installed, we can save a bunch of setup time by
# using the installed version
elif all(lang_base.exe_exists(exe) for exe in ('node', 'npm')):
return 'system'
else:
return C.DEFAULT
def _envdir(prefix, version):
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
return prefix.path(directory)
def get_env_patch(venv: str) -> PatchesT:
def get_env_patch(venv): # pragma: windows no cover
if sys.platform == 'cygwin': # pragma: no cover
_, win_venv, _ = cmd_output('cygpath', '-w', venv)
install_prefix = fr'{win_venv.strip()}\bin'
install_prefix = r'{}\bin'.format(win_venv.strip())
lib_dir = 'lib'
elif sys.platform == 'win32': # pragma: no cover
install_prefix = bin_dir(venv)
lib_dir = 'Scripts'
else: # pragma: win32 no cover
else: # pragma: windows no cover
install_prefix = venv
lib_dir = 'lib'
return (
('NODE_VIRTUAL_ENV', venv),
('NPM_CONFIG_PREFIX', install_prefix),
('npm_config_prefix', install_prefix),
('NPM_CONFIG_USERCONFIG', UNSET),
('npm_config_userconfig', UNSET),
('NODE_PATH', os.path.join(venv, lib_dir, 'node_modules')),
('PATH', (bin_dir(venv), 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)):
def in_env(prefix, language_version): # pragma: windows no cover
with envcontext(get_env_patch(_envdir(prefix, language_version))):
yield
def health_check(prefix: Prefix, version: str) -> str | None:
with in_env(prefix, version):
retcode, _, _ = cmd_output_b('node', '--version', check=False)
if retcode != 0: # pragma: win32 no cover
return f'`node --version` returned {retcode}'
else:
return None
def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None:
prefix, version, additional_dependencies,
): # pragma: windows no cover
additional_dependencies = tuple(additional_dependencies)
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
if sys.platform == 'win32': # pragma: no cover
envdir = fr'\\?\{os.path.normpath(envdir)}'
cmd = [sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir]
if version != C.DEFAULT:
cmd.extend(['-n', version])
cmd_output_b(*cmd)
envdir = '\\\\?\\' + os.path.normpath(envdir)
with clean_path_on_failure(envdir):
cmd = [
sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir,
]
if version != C.DEFAULT:
cmd.extend(['-n', version])
cmd_output_b(*cmd)
with in_env(prefix, version):
# https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449
# install as if we installed from git
with in_env(prefix, version):
# https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449
# install as if we installed from git
helpers.run_setup_cmd(prefix, ('npm', 'install'))
helpers.run_setup_cmd(
prefix,
('npm', 'install', '-g', '.') + additional_dependencies,
)
local_install_cmd = (
'npm', 'install', '--include=dev', '--include=prod',
'--ignore-prepublish', '--no-progress', '--no-save',
)
lang_base.setup_cmd(prefix, local_install_cmd)
_, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir)
pkg = prefix.path(pkg.strip())
install = ('npm', 'install', '-g', pkg, *additional_dependencies)
lang_base.setup_cmd(prefix, install)
# clean these up after installation
if prefix.exists('node_modules'): # pragma: win32 no cover
rmtree(prefix.path('node_modules'))
os.remove(pkg)
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -0,0 +1,22 @@
from __future__ import unicode_literals
import sys
from pre_commit.languages import helpers
from pre_commit.xargs import xargs
ENVIRONMENT_DIR = None
GREP = 'ggrep' if sys.platform == 'darwin' else 'grep'
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
install_environment = helpers.no_install
def run_hook(hook, file_args, color):
# For PCRE the entry is the regular expression to match
cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,)
# Grep usually returns 0 for matches, and nonzero for non-matches so we
# negate it here.
return xargs(cmd, file_args, negate=True, color=color)

View file

@ -1,50 +0,0 @@
from __future__ import annotations
import contextlib
import os
import shlex
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
ENVIRONMENT_DIR = 'perl_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:
return (
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
('PERL5LIB', os.path.join(venv, 'lib', 'perl5')),
('PERL_MB_OPT', f'--install_base {shlex.quote(venv)}'),
(
'PERL_MM_OPT', (
f'INSTALL_BASE={shlex.quote(venv)} '
f'INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none'
),
),
)
@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('perl', version)
with in_env(prefix, version):
lang_base.setup_cmd(
prefix, ('cpan', '-T', '.', *additional_dependencies),
)

View file

@ -1,36 +1,33 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import argparse
import re
import sys
from collections.abc import Sequence
from re import Pattern
from typing import NamedTuple
from pre_commit import lang_base
from pre_commit import output
from pre_commit.prefix import Prefix
from pre_commit.languages import helpers
from pre_commit.xargs import xargs
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
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
install_environment = helpers.no_install
def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int:
def _process_filename_by_line(pattern, filename):
retv = 0
with open(filename, 'rb') as f:
for line_no, line in enumerate(f, start=1):
if pattern.search(line):
retv = 1
output.write(f'{filename}:{line_no}:')
output.write_line_b(line.rstrip(b'\r\n'))
output.write('{}:{}:'.format(filename, line_no))
output.write_line(line.rstrip(b'\r\n'))
return retv
def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int:
def _process_filename_at_once(pattern, filename):
retv = 0
with open(filename, 'rb') as f:
contents = f.read()
@ -38,70 +35,21 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int:
if match:
retv = 1
line_no = contents[:match.start()].count(b'\n')
output.write(f'{filename}:{line_no + 1}:')
output.write('{}:{}:'.format(filename, line_no + 1))
matched_lines = match[0].split(b'\n')
matched_lines = match.group().split(b'\n')
matched_lines[0] = contents.split(b'\n')[line_no]
output.write_line_b(b'\n'.join(matched_lines))
output.write_line(b'\n'.join(matched_lines))
return retv
def _process_filename_by_line_negated(
pattern: Pattern[bytes],
filename: str,
) -> int:
with open(filename, 'rb') as f:
for line in f:
if pattern.search(line):
return 0
else:
output.write_line(filename)
return 1
def run_hook(hook, file_args, color):
exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,)
return xargs(exe, file_args, color=color)
def _process_filename_at_once_negated(
pattern: Pattern[bytes],
filename: str,
) -> int:
with open(filename, 'rb') as f:
contents = f.read()
match = pattern.search(contents)
if match:
return 0
else:
output.write_line(filename)
return 1
class Choice(NamedTuple):
multiline: bool
negate: bool
FNS = {
Choice(multiline=True, negate=True): _process_filename_at_once_negated,
Choice(multiline=True, negate=False): _process_filename_at_once,
Choice(multiline=False, negate=True): _process_filename_by_line_negated,
Choice(multiline=False, negate=False): _process_filename_by_line,
}
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 = (sys.executable, '-m', __name__, *args, entry)
return xargs(cmd, file_args, color=color)
def main(argv: Sequence[str] | None = None) -> int:
def main(argv=None):
parser = argparse.ArgumentParser(
description=(
'grep-like finder using python regexes. Unlike grep, this tool '
@ -111,7 +59,6 @@ def main(argv: Sequence[str] | None = None) -> int:
)
parser.add_argument('-i', '--ignore-case', action='store_true')
parser.add_argument('--multiline', action='store_true')
parser.add_argument('--negate', action='store_true')
parser.add_argument('pattern', help='python regex pattern.')
parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv)
@ -123,11 +70,13 @@ def main(argv: Sequence[str] | None = None) -> int:
pattern = re.compile(args.pattern.encode(), flags)
retv = 0
process_fn = FNS[Choice(multiline=args.multiline, negate=args.negate)]
for filename in args.filenames:
retv |= process_fn(pattern, filename)
if args.multiline:
retv |= _process_filename_at_once(pattern, filename)
else:
retv |= _process_filename_by_line(pattern, filename)
return retv
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,94 +1,55 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import functools
import os
import sys
from collections.abc import Generator
from collections.abc import Sequence
import pre_commit.constants as C
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.envcontext import Var
from pre_commit.languages import helpers
from pre_commit.parse_shebang import find_executable
from pre_commit.prefix import Prefix
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_b
from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'py_env'
run_hook = lang_base.basic_run_hook
@functools.cache
def _version_info(exe: str) -> str:
prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
try:
return cmd_output(exe, '-S', '-c', prog)[1].strip()
except CalledProcessError:
return f'<<error retrieving version from {exe}>>'
def _read_pyvenv_cfg(filename: str) -> dict[str, str]:
ret = {}
with open(filename, encoding='UTF-8') as f:
for line in f:
try:
k, v = line.split('=')
except ValueError: # blank line / comment / etc.
continue
else:
ret[k.strip()] = v.strip()
return ret
def bin_dir(venv: str) -> str:
def bin_dir(venv):
"""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)
def get_env_patch(venv: str) -> PatchesT:
def get_env_patch(venv):
return (
('PIP_DISABLE_PIP_VERSION_CHECK', '1'),
('PYTHONHOME', UNSET),
('VIRTUAL_ENV', venv),
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
)
def _find_by_py_launcher(
version: str,
) -> str | None: # pragma: no cover (windows only)
def _find_by_py_launcher(version): # pragma: no cover (windows only)
if version.startswith('python'):
num = version.removeprefix('python')
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
env = dict(os.environ, PYTHONIOENCODING='UTF-8')
try:
return cmd_output(*cmd, env=env)[1].strip()
return cmd_output(
'py', '-{}'.format(version[len('python'):]),
'-c', 'import sys; print(sys.executable)',
)[1].strip()
except CalledProcessError:
pass
return None
def _impl_exe_name() -> str:
if sys.implementation.name == 'cpython': # pragma: cpython cover
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:
def _find_by_sys_executable():
def _norm(path):
_, exe = os.path.split(path.lower())
exe, _, _ = exe.partition('.exe')
if exe not in {'python', 'pythonw'} and find_executable(exe):
if find_executable(exe) and exe not in {'python', 'pythonw'}:
return exe
return None
# On linux, I see these common sys.executables:
#
@ -105,53 +66,57 @@ def _find_by_sys_executable() -> str | None:
return None
@functools.lru_cache(maxsize=1)
def get_default_version() -> str: # pragma: no cover (platform dependent)
v_major = f'{sys.version_info[0]}'
v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}'
def _get_default_version(): # pragma: no cover (platform dependent)
# First attempt from `sys.executable` (or the realpath)
exe = _find_by_sys_executable()
if exe:
return exe
# attempt the likely implementation exe
for potential in (v_minor, v_major):
exe = f'{_impl_exe_name()}{potential}'
if find_executable(exe):
return exe
# Next try the `pythonX.X` executable
exe = 'python{}.{}'.format(*sys.version_info)
if find_executable(exe):
return exe
# next try `sys.executable` (or the realpath)
maybe_exe = _find_by_sys_executable()
if maybe_exe:
return maybe_exe
if _find_by_py_launcher(exe):
return 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
# Give a best-effort try for windows
if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))):
return exe
# We tried!
return C.DEFAULT
def _sys_executable_matches(version: str) -> bool:
def get_default_version():
# TODO: when dropping python2, use `functools.lru_cache(maxsize=1)`
try:
return get_default_version.cached_version
except AttributeError:
get_default_version.cached_version = _get_default_version()
return get_default_version()
def _sys_executable_matches(version):
if version == 'python':
return True
elif not version.startswith('python'):
return False
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:
return False
return sys.version_info[:len(info)] == info
def norm_version(version: str) -> str | None:
if version == C.DEFAULT: # use virtualenv's default
return None
elif _sys_executable_matches(version): # virtualenv defaults to our exe
return None
def norm_version(version):
# first see if our current executable is appropriate
if _sys_executable_matches(version):
return sys.executable
if sys.platform == 'win32': # pragma: no cover (windows)
if os.name == 'nt': # pragma: no cover (windows)
version_exec = _find_by_py_launcher(version)
if version_exec:
return version_exec
@ -161,68 +126,60 @@ def norm_version(version: str) -> str | None:
if version_exec and version_exec != version:
return version_exec
# If it is in the form pythonx.x search in the default
# place on windows
if version.startswith('python'):
return r'C:\{}\python.exe'.format(version.replace('.', ''))
# Otherwise assume it is a path
return os.path.expanduser(version)
@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 py_interface(_dir, _make_venv):
@contextlib.contextmanager
def in_env(prefix, language_version):
envdir = prefix.path(helpers.environment_dir(_dir, language_version))
with envcontext(get_env_patch(envdir)):
yield
def healthy(prefix, language_version):
with in_env(prefix, language_version):
retcode, _, _ = cmd_output_b(
'python', '-c',
'import ctypes, datetime, io, os, ssl, weakref',
cwd='/',
retcode=None,
)
return retcode == 0
def run_hook(hook, file_args, color):
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
def install_environment(prefix, version, additional_dependencies):
additional_dependencies = tuple(additional_dependencies)
directory = helpers.environment_dir(_dir, version)
env_dir = prefix.path(directory)
with clean_path_on_failure(env_dir):
if version != C.DEFAULT:
python = norm_version(version)
else:
python = os.path.realpath(sys.executable)
_make_venv(env_dir, python)
with in_env(prefix, version):
helpers.run_setup_cmd(
prefix, ('pip', 'install', '.') + additional_dependencies,
)
return in_env, healthy, run_hook, install_environment
def health_check(prefix: Prefix, version: str) -> str | None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg')
# created with "old" virtualenv
if not os.path.exists(pyvenv_cfg):
return 'pyvenv.cfg does not exist (old virtualenv?)'
exe_name = win_exe('python')
py_exe = prefix.path(bin_dir(envdir), exe_name)
cfg = _read_pyvenv_cfg(pyvenv_cfg)
if 'version_info' not in cfg:
return "created virtualenv's pyvenv.cfg is missing `version_info`"
# always use uncached lookup here in case we replaced an unhealthy env
virtualenv_version = _version_info.__wrapped__(py_exe)
if virtualenv_version != 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 make_venv(envdir, python):
env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1')
cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
cmd_output_b(*cmd, env=env, cwd='/')
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
venv_cmd = [sys.executable, '-mvirtualenv', envdir]
python = norm_version(version)
if python is not None:
venv_cmd.extend(('-p', python))
install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies)
cmd_output_b(*venv_cmd, cwd='/')
with in_env(prefix, version):
lang_base.setup_cmd(prefix, install_cmd)
_interface = py_interface(ENVIRONMENT_DIR, make_venv)
in_env, healthy, run_hook, install_environment = _interface

View file

@ -0,0 +1,56 @@
from __future__ import unicode_literals
import os.path
import sys
from pre_commit.languages import python
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'py_venv'
def get_default_version(): # pragma: no cover (version specific)
if sys.version_info < (3,):
return 'python3'
else:
return python.get_default_version()
def orig_py_exe(exe): # pragma: no cover (platform specific)
"""A -mvenv virtualenv made from a -mvirtualenv virtualenv installs
packages to the incorrect location. Attempt to find the _original_ exe
and invoke `-mvenv` from there.
See:
- https://github.com/pre-commit/pre-commit/issues/755
- https://github.com/pypa/virtualenv/issues/1095
- https://bugs.python.org/issue30811
"""
try:
prefix_script = 'import sys; print(sys.real_prefix)'
_, prefix, _ = cmd_output(exe, '-c', prefix_script)
prefix = prefix.strip()
except CalledProcessError:
# not created from -mvirtualenv
return exe
if os.name == 'nt':
expected = os.path.join(prefix, 'python.exe')
else:
expected = os.path.join(prefix, 'bin', os.path.basename(exe))
if os.path.exists(expected):
return expected
else:
return exe
def make_venv(envdir, python):
cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/')
_interface = python.py_interface(ENVIRONMENT_DIR, make_venv)
in_env, healthy, run_hook, install_environment = _interface

View file

@ -1,278 +0,0 @@
from __future__ import annotations
import contextlib
import os
import shlex
import shutil
import tempfile
import textwrap
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
from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'renv'
get_default_version = lang_base.basic_get_default_version
_RENV_ACTIVATED_OPTS = (
'--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:
return (
('R_PROFILE_USER', os.path.join(venv, 'activate.R')),
('RENV_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)):
yield
def _prefix_if_file_entry(
entry: list[str],
prefix: Prefix,
*,
is_local: bool,
) -> Sequence[str]:
if entry[1] == '-e' or is_local:
return entry[1:]
else:
return (prefix.path(entry[1]),)
def _rscript_exec() -> str:
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:
# Rscript -e expr
# Rscript path/to/file
"""
if entry[0] != 'Rscript':
raise ValueError('entry must start with `Rscript`.')
if entry[1] == '-e':
if len(entry) > 3:
raise ValueError('You can supply at most one expression.')
elif len(entry) > 2:
raise ValueError(
'The only valid syntax is `Rscript -e {expr}`'
'or `Rscript path/to/hook/script`',
)
def _cmd_from_hook(
prefix: Prefix,
entry: str,
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 (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args)
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
lang_base.assert_version_default('r', version)
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
os.makedirs(env_dir, exist_ok=True)
shutil.copy(prefix.path('renv.lock'), env_dir)
shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv'))
r_code_inst_environment = f"""\
prefix_dir <- {prefix.prefix_dir!r}
options(
repos = c(CRAN = "https://cran.rstudio.com"),
renv.consent = TRUE
)
source("renv/activate.R")
renv::restore()
activate_statement <- paste0(
'suppressWarnings({{',
'old <- setwd("', getwd(), '"); ',
'source("renv/activate.R"); ',
'setwd(old); ',
'renv::load("', getwd(), '");}})'
)
writeLines(activate_statement, 'activate.R')
is_package <- tryCatch(
{{
path_desc <- file.path(prefix_dir, 'DESCRIPTION')
suppressWarnings(desc <- read.dcf(path_desc))
"Package" %in% colnames(desc)
}},
error = function(...) FALSE
)
if (is_package) {{
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,
)
def _inline_r_setup(code: str) -> str:
"""
Some behaviour of R cannot be configured via env variables, but can
only be configured via R options once R has started. These are set here.
"""
with_option = [
textwrap.dedent("""\
options(
install.packages.compile.from.source = "never",
pkgType = "binary"
)
"""),
code,
]
return '\n'.join(with_option)
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 = _cmd_from_hook(prefix, entry, args, is_local=is_local)
return lang_base.run_xargs(
cmd,
file_args,
require_serial=require_serial,
color=color,
)

View file

@ -1,145 +1,129 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import functools
import importlib.resources
import io
import os.path
import shutil
import tarfile
from collections.abc import Generator
from collections.abc import Sequence
from typing import IO
import pre_commit.constants as C
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.envcontext import Var
from pre_commit.prefix import Prefix
from pre_commit.languages import helpers
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'
health_check = lang_base.basic_health_check
run_hook = lang_base.basic_run_hook
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
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)
def get_default_version() -> str:
if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')):
return 'system'
else:
return C.DEFAULT
def get_env_patch(
venv: str,
language_version: str,
) -> PatchesT:
patches: PatchesT = (
def get_env_patch(venv, language_version): # pragma: windows no cover
patches = (
('GEM_HOME', os.path.join(venv, 'gems')),
('GEM_PATH', UNSET),
('RBENV_ROOT', venv),
('BUNDLE_IGNORE_CONFIG', '1'),
(
'PATH', (
os.path.join(venv, 'gems', 'bin'), os.pathsep,
os.path.join(venv, 'shims'), os.pathsep,
os.path.join(venv, 'bin'), os.pathsep, Var('PATH'),
),
),
)
if language_version == 'system':
patches += (
(
'PATH', (
os.path.join(venv, 'gems', 'bin'), os.pathsep,
Var('PATH'),
),
),
)
else: # pragma: win32 no cover
patches += (
('RBENV_ROOT', venv),
(
'PATH', (
os.path.join(venv, 'gems', 'bin'), os.pathsep,
os.path.join(venv, 'shims'), os.pathsep,
os.path.join(venv, 'bin'), os.pathsep, Var('PATH'),
),
),
)
if language_version not in {'system', 'default'}: # pragma: win32 no cover
if language_version != C.DEFAULT:
patches += (('RBENV_VERSION', language_version),)
return patches
@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)):
def in_env(prefix, language_version): # pragma: windows no cover
envdir = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, language_version),
)
with envcontext(get_env_patch(envdir, language_version)):
yield
def _extract_resource(filename: str, dest: str) -> None:
with _resource_bytesio(filename) as bio:
def _extract_resource(filename, dest):
with resource_bytesio(filename) as bio:
with tarfile.open(fileobj=bio) as tf:
tf.extractall(dest)
def _install_rbenv(
prefix: Prefix,
version: str,
) -> None: # pragma: win32 no cover
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
_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
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-build.tar.gz', plugins_dir)
activate_path = prefix.path(directory, 'bin', 'activate')
with io.open(activate_path, 'w') as activate_file:
# This is similar to how you would install rbenv to your home directory
# However we do a couple things to make the executables exposed and
# configure it to work in our directory.
# We also modify the PS1 variable for manual debugging sake.
activate_file.write(
'#!/usr/bin/env bash\n'
"export RBENV_ROOT='{directory}'\n"
'export PATH="$RBENV_ROOT/bin:$PATH"\n'
'eval "$(rbenv init -)"\n'
'export PS1="(rbenv)$PS1"\n'
# This lets us install gems in an isolated and repeatable
# directory
"export GEM_HOME='{directory}/gems'\n"
'export PATH="$GEM_HOME/bin:$PATH"\n'
'\n'.format(directory=prefix.path(directory)),
)
def _install_ruby(
prefix: Prefix,
version: str,
) -> None: # pragma: win32 no cover
# If we aren't using the system ruby, add a version here
if version != C.DEFAULT:
activate_file.write('export RBENV_VERSION="{}"\n'.format(version))
def _install_ruby(prefix, version): # pragma: windows no cover
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)
# 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(
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
if version != 'system': # pragma: win32 no cover
_install_rbenv(prefix, version)
prefix, version, additional_dependencies,
): # pragma: windows no cover
additional_dependencies = tuple(additional_dependencies)
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
with clean_path_on_failure(prefix.path(directory)):
# TODO: this currently will fail if there's no version specified and
# there's no system ruby installed. Is this ok?
_install_rbenv(prefix, version=version)
with in_env(prefix, version):
# Need to call this before installing so rbenv's directories
# are set up
lang_base.setup_cmd(prefix, ('rbenv', 'init', '-'))
# 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
lang_base.setup_cmd(prefix, ('rbenv', 'rehash'))
helpers.run_setup_cmd(prefix, ('rbenv', 'rehash'))
helpers.run_setup_cmd(
prefix, ('gem', 'build') + prefix.star('.gemspec'),
)
helpers.run_setup_cmd(
prefix,
('gem', 'install', '--no-document') +
prefix.star('.gem') + additional_dependencies,
)
with in_env(prefix, version):
lang_base.setup_cmd(
prefix, ('gem', 'build', *prefix.star('.gemspec')),
)
lang_base.setup_cmd(
prefix,
(
'gem', 'install',
'--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,
),
)
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,121 +1,58 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import functools
import os.path
import shutil
import sys
import tempfile
import urllib.request
from collections.abc import Generator
from collections.abc import Sequence
import toml
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 PatchesT
from pre_commit.envcontext import Var
from pre_commit.prefix import Prefix
from pre_commit.languages import helpers
from pre_commit.util import clean_path_on_failure
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'
health_check = lang_base.basic_health_check
run_hook = lang_base.basic_run_hook
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
@functools.lru_cache(maxsize=1)
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:
def get_env_patch(target_dir):
return (
('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 ()
(
'PATH',
(os.path.join(target_dir, 'bin'), 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, version)):
def in_env(prefix):
target_dir = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
with envcontext(get_env_patch(target_dir)):
yield
def _add_dependencies(
prefix: Prefix,
additional_dependencies: set[str],
) -> None:
crates = []
for dep in additional_dependencies:
name, _, spec = dep.partition(':')
crate = f'{name}@{spec or "*"}'
crates.append(crate)
lang_base.setup_cmd(prefix, ('cargo', 'add', *crates))
def _add_dependencies(cargo_toml_path, additional_dependencies):
with open(cargo_toml_path, 'r+') as f:
cargo_toml = toml.load(f)
cargo_toml.setdefault('dependencies', {})
for dep in additional_dependencies:
name, _, spec = dep.partition(':')
cargo_toml['dependencies'][name] = spec or '*'
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(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
def install_environment(prefix, version, additional_dependencies):
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:
# as dependencies for the library being built, and as binary packages
@ -132,29 +69,26 @@ def install_environment(
}
lib_deps = set(additional_dependencies) - cli_deps
packages_to_install: set[tuple[str, ...]] = {('--path', '.')}
for cli_dep in cli_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,))
if len(lib_deps) > 0:
_add_dependencies(prefix.path('Cargo.toml'), lib_deps)
with contextlib.ExitStack() as ctx:
ctx.enter_context(in_env(prefix, version))
with clean_path_on_failure(directory):
packages_to_install = {('--path', '.')}
for cli_dep in cli_deps:
cli_dep = cli_dep[len('cli:'):]
package, _, version = cli_dep.partition(':')
if version != '':
packages_to_install.add((package, '--version', version))
else:
packages_to_install.add((package,))
if version != 'system':
install_rust_with_toolchain(_rust_toolchain(version), envdir)
tmpdir = ctx.enter_context(tempfile.TemporaryDirectory())
ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),)))
if len(lib_deps) > 0:
_add_dependencies(prefix, lib_deps)
for args in packages_to_install:
for package in packages_to_install:
cmd_output_b(
'cargo', 'install', '--bins', '--root', envdir, *args,
cwd=prefix.prefix_dir,
'cargo', 'install', '--bins', '--root', directory, *package,
cwd=prefix.prefix_dir
)
def run_hook(hook, file_args, color):
with in_env(hook.prefix):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -0,0 +1,15 @@
from __future__ import unicode_literals
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, file_args, color):
cmd = hook.cmd
cmd = (hook.prefix.path(cmd[0]),) + cmd[1:]
return helpers.run_xargs(hook, cmd, file_args, color=color)

View file

@ -1,50 +1,56 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import os
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 PatchesT
from pre_commit.envcontext import Var
from pre_commit.prefix import Prefix
from pre_commit.languages import helpers
from pre_commit.util import clean_path_on_failure
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_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): # pragma: windows no cover
bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG)
return (('PATH', (bin_path, os.pathsep, Var('PATH'))),)
@contextlib.contextmanager # pragma: win32 no cover
def in_env(prefix: Prefix, version: str) -> Generator[None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
@contextlib.contextmanager
def in_env(prefix): # pragma: windows no cover
envdir = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
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('swift', version)
lang_base.assert_no_additional_deps('swift', additional_dependencies)
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
prefix, version, additional_dependencies,
): # pragma: windows no cover
helpers.assert_version_default('swift', version)
helpers.assert_no_additional_deps('swift', additional_dependencies)
directory = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
)
# Build the swift package
os.mkdir(envdir)
cmd_output_b(
'swift', 'build',
'--package-path', prefix.prefix_dir,
'-c', BUILD_CONFIG,
'--build-path', os.path.join(envdir, BUILD_DIR),
)
with clean_path_on_failure(directory):
os.mkdir(directory)
cmd_output_b(
'swift', 'build',
'-C', prefix.prefix_dir,
'-c', BUILD_CONFIG,
'--build-path', os.path.join(directory, BUILD_DIR),
)
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -0,0 +1,13 @@
from __future__ import unicode_literals
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, file_args, color):
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,12 +1,12 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import logging
from collections.abc import Generator
from pre_commit import color
from pre_commit import output
logger = logging.getLogger('pre_commit')
LOG_LEVEL_COLORS = {
@ -18,22 +18,26 @@ LOG_LEVEL_COLORS = {
class LoggingHandler(logging.Handler):
def __init__(self, use_color: bool) -> None:
super().__init__()
def __init__(self, use_color):
super(LoggingHandler, self).__init__()
self.use_color = use_color
def emit(self, record: logging.LogRecord) -> None:
level_msg = color.format_color(
f'[{record.levelname}]',
LOG_LEVEL_COLORS[record.levelname],
self.use_color,
def emit(self, record):
output.write_line(
'{} {}'.format(
color.format_color(
'[{}]'.format(record.levelname),
LOG_LEVEL_COLORS[record.levelname],
self.use_color,
),
record.getMessage(),
),
)
output.write_line(f'{level_msg} {record.getMessage()}')
@contextlib.contextmanager
def logging_handler(use_color: bool) -> Generator[None]:
handler = LoggingHandler(use_color)
def logging_handler(*args, **kwargs):
handler = LoggingHandler(*args, **kwargs)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
try:

View file

@ -1,20 +1,17 @@
from __future__ import annotations
from __future__ import unicode_literals
import argparse
import logging
import os
import sys
from collections.abc import Sequence
import pre_commit.constants as C
from pre_commit import clientlib
from pre_commit import color
from pre_commit import five
from pre_commit import git
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.clean import clean
from pre_commit.commands.gc import gc
from pre_commit.commands.hook_impl import hook_impl
from pre_commit.commands.init_templatedir import init_templatedir
from pre_commit.commands.install_uninstall import install
from pre_commit.commands.install_uninstall import install_hooks
@ -23,11 +20,11 @@ from pre_commit.commands.migrate_config import migrate_config
from pre_commit.commands.run import run
from pre_commit.commands.sample_config import sample_config
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 FatalError
from pre_commit.logging_handler import logging_handler
from pre_commit.store import Store
from pre_commit.util import CalledProcessError
logger = logging.getLogger('pre_commit')
@ -38,239 +35,170 @@ logger = logging.getLogger('pre_commit')
# pyvenv
os.environ.pop('__PYVENV_LAUNCHER__', None)
# https://github.com/getsentry/snuba/pull/5388
os.environ.pop('PYTHONEXECUTABLE', None)
COMMANDS_NO_GIT = {
'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config',
'validate-config', 'validate-manifest',
}
COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'}
def _add_config_option(parser: argparse.ArgumentParser) -> None:
def _add_color_option(parser):
parser.add_argument(
'--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'),
type=color.use_color,
metavar='{' + ','.join(color.COLOR_CHOICES) + '}',
help='Whether to use color in output. Defaults to `%(default)s`.',
)
def _add_config_option(parser):
parser.add_argument(
'-c', '--config', default=C.CONFIG_FILE,
help='Path to alternate config file',
)
def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
class AppendReplaceDefault(argparse.Action):
def __init__(self, *args, **kwargs):
super(AppendReplaceDefault, self).__init__(*args, **kwargs)
self.appended = False
def __call__(self, parser, namespace, values, option_string=None):
if not self.appended:
setattr(namespace, self.dest, [])
self.appended = True
getattr(namespace, self.dest).append(values)
def _add_hook_type_option(parser):
parser.add_argument(
'-t', '--hook-type',
choices=clientlib.HOOK_TYPES, action='append', dest='hook_types',
'-t', '--hook-type', choices=(
'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg',
),
action=AppendReplaceDefault,
default=['pre-commit'],
dest='hook_types',
)
def _add_run_options(parser: argparse.ArgumentParser) -> None:
def _add_run_options(parser):
parser.add_argument('hook', nargs='?', help='A single hook-id to run')
parser.add_argument('--verbose', '-v', action='store_true')
mutex_group = parser.add_mutually_exclusive_group(required=False)
mutex_group.add_argument(
'--all-files', '-a', action='store_true',
help='Run on all the files in the repo.',
)
mutex_group.add_argument(
'--files', nargs='*', default=[],
help='Specific filenames to run hooks on.',
parser.add_argument('--verbose', '-v', action='store_true', default=False)
parser.add_argument(
'--origin', '-o',
help="The origin branch's commit_id when using `git push`.",
)
parser.add_argument(
'--show-diff-on-failure', action='store_true',
help='When hooks fail, run `git diff` directly afterward.',
)
parser.add_argument(
'--fail-fast', action='store_true',
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',
)
parser.add_argument(
'--remote-branch', help='Remote branch ref used by `git push`.',
)
parser.add_argument(
'--local-branch', help='Local branch ref used by `git push`.',
)
parser.add_argument(
'--from-ref', '--source', '-s',
help=(
'(for usage with `--to-ref`) -- this option represents the '
'original ref in a `from_ref...to_ref` diff expression. '
'For `pre-push` hooks, this represents the branch you are pushing '
'to. '
'For `post-checkout` hooks, this represents the branch that was '
'previously checked out.'
),
)
parser.add_argument(
'--to-ref', '--origin', '-o',
help=(
'(for usage with `--from-ref`) -- this option represents the '
'destination ref in a `from_ref...to_ref` diff expression. '
'For `pre-push` hooks, this represents the branch being pushed. '
'For `post-checkout` hooks, this represents the branch that is '
'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.'
),
'--source', '-s',
help="The remote branch's commit_id when using `git push`.",
)
parser.add_argument(
'--commit-msg-filename',
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)'
),
'--hook-stage', choices=C.STAGES, default='commit',
help='The stage during which the hook is fired. One of %(choices)s',
)
parser.add_argument(
'--commit-object-name',
help=(
'Commit object name '
'(typically the third argument to .git/hooks/prepare-commit-msg)'
),
'--show-diff-on-failure', action='store_true',
help='When hooks fail, run `git diff` directly afterward.',
)
parser.add_argument(
'--remote-name', help='Remote name used by `git push`.',
mutex_group = parser.add_mutually_exclusive_group(required=False)
mutex_group.add_argument(
'--all-files', '-a', action='store_true', default=False,
help='Run on all the files in the repo.',
)
parser.add_argument('--remote-url', help='Remote url used by `git push`.')
parser.add_argument(
'--checkout-type',
help=(
'Indicates whether the checkout was a branch checkout '
'(changing branches, flag=1) or a file checkout (retrieving a '
'file from the index, flag=0).'
),
)
parser.add_argument(
'--is-squash-merge',
help=(
'During a post-merge hook, indicates whether the merge was a '
'squash merge'
),
)
parser.add_argument(
'--rewrite-command',
help=(
'During a post-rewrite hook, specifies the command that invoked '
'the rewrite'
),
mutex_group.add_argument(
'--files', nargs='*', default=[],
help='Specific filenames to run hooks on.',
)
def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
def _adjust_args_and_chdir(args):
# `--config` was specified relative to the non-root working directory
if os.path.exists(args.config):
args.config = os.path.abspath(args.config)
if args.command in {'run', 'try-repo'}:
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):
args.repo = os.path.abspath(args.repo)
toplevel = git.get_root()
os.chdir(toplevel)
try:
os.chdir(git.get_root())
except CalledProcessError:
raise FatalError(
'git failed. Is it installed, and are you in a Git repository '
'directory?',
)
args.config = os.path.relpath(args.config)
if args.command in {'run', 'try-repo'}:
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):
args.repo = os.path.relpath(args.repo)
def main(argv: Sequence[str] | None = None) -> int:
def main(argv=None):
argv = argv if argv is not None else sys.argv[1:]
parser = argparse.ArgumentParser(prog='pre-commit')
argv = [five.to_text(arg) for arg in argv]
parser = argparse.ArgumentParser()
# https://stackoverflow.com/a/8521644/812183
parser.add_argument(
'-V', '--version',
action='version',
version=f'%(prog)s {C.VERSION}',
version='%(prog)s {}'.format(C.VERSION),
)
subparsers = parser.add_subparsers(dest='command')
def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser:
parser = subparsers.add_parser(name, help=help)
add_color_option(parser)
return parser
autoupdate_parser = _add_cmd(
autoupdate_parser = subparsers.add_parser(
'autoupdate',
help="Auto-update pre-commit config to the latest repos' versions.",
)
_add_color_option(autoupdate_parser)
_add_config_option(autoupdate_parser)
autoupdate_parser.add_argument(
'--tags-only', action='store_true', help='LEGACY: for compatibility',
)
autoupdate_parser.add_argument(
'--bleeding-edge', action='store_true',
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).'
),
)
autoupdate_parser.add_argument(
'--freeze', action='store_true',
help='Store "frozen" hashes in `rev` instead of tag names',
)
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.',
)
autoupdate_parser.add_argument(
'-j', '--jobs', type=int, default=1,
help='Number of threads to use. (default %(default)s).',
clean_parser = subparsers.add_parser(
'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.')
gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.')
_add_color_option(gc_parser)
_add_config_option(gc_parser)
_add_cmd('gc', help='Clean unused cached repos.')
hazmat_parser = _add_cmd(
'hazmat', help='Composable tools for rare use in hook `entry`.',
)
hazmat.add_parsers(hazmat_parser)
init_templatedir_parser = _add_cmd(
init_templatedir_parser = subparsers.add_parser(
'init-templatedir',
help=(
'Install hook script in a directory intended for use with '
'`git config init.templateDir`.'
),
)
_add_color_option(init_templatedir_parser)
_add_config_option(init_templatedir_parser)
init_templatedir_parser.add_argument(
'directory', help='The directory in which to write the hook script.',
)
init_templatedir_parser.add_argument(
'--no-allow-missing-config',
action='store_false',
dest='allow_missing_config',
help='Assume cloned repos should have a `pre-commit` config.',
)
_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)
install_parser.add_argument(
'-f', '--overwrite', action='store_true',
@ -285,14 +213,14 @@ def main(argv: Sequence[str] | None = None) -> int:
)
_add_hook_type_option(install_parser)
install_parser.add_argument(
'--allow-missing-config', action='store_true',
'--allow-missing-config', action='store_true', default=False,
help=(
'Whether to allow a missing `pre-commit` configuration file '
'or exit with a failure code.'
),
)
install_hooks_parser = _add_cmd(
install_hooks_parser = subparsers.add_parser(
'install-hooks',
help=(
'Install hook environments for all environments in the config '
@ -300,24 +228,32 @@ def main(argv: Sequence[str] | None = None) -> int:
'useful.'
),
)
_add_color_option(install_hooks_parser)
_add_config_option(install_hooks_parser)
migrate_config_parser = _add_cmd(
migrate_config_parser = subparsers.add_parser(
'migrate-config',
help='Migrate list configuration to new map configuration.',
)
_add_color_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_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='Produce a sample {} file'.format(C.CONFIG_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',
help='Try the hooks in a repository, useful for developing new hooks.',
)
_add_color_option(try_repo_parser)
_add_config_option(try_repo_parser)
try_repo_parser.add_argument(
'repo', help='Repository to source hooks from.',
@ -331,39 +267,18 @@ def main(argv: Sequence[str] | None = None) -> int:
)
_add_run_options(try_repo_parser)
uninstall_parser = _add_cmd(
uninstall_parser = subparsers.add_parser(
'uninstall', help='Uninstall the pre-commit script.',
)
_add_color_option(uninstall_parser)
_add_config_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', help='Show help for a specific command.',
)
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
if len(argv) == 0:
argv = ['run']
@ -375,51 +290,37 @@ def main(argv: Sequence[str] | None = None) -> int:
parser.parse_args(['--help'])
with error_handler(), logging_handler(args.color):
if args.command not in COMMANDS_NO_GIT:
_adjust_args_and_chdir(args)
git.check_for_cygwin_mismatch()
store = Store()
if args.command not in COMMANDS_NO_GIT:
_adjust_args_and_chdir(args)
store.mark_config_used(args.config)
store.mark_config_used(args.config)
if args.command == 'autoupdate':
if args.tags_only:
logger.warning('--tags-only is the default')
return autoupdate(
args.config,
args.config, store,
tags_only=not args.bleeding_edge,
freeze=args.freeze,
repos=args.repos,
jobs=args.jobs,
)
elif args.command == 'clean':
return clean(store)
elif args.command == 'gc':
return gc(store)
elif args.command == 'hazmat':
return hazmat.impl(args)
elif args.command == 'hook-impl':
return hook_impl(
store,
config=args.config,
color=args.color,
hook_type=args.hook_type,
hook_dir=args.hook_dir,
skip_on_missing_config=args.skip_on_missing_config,
args=args.rest[1:],
)
elif args.command == 'install':
return install(
args.config, store,
hook_types=args.hook_types,
overwrite=args.overwrite,
hooks=args.install_hooks,
overwrite=args.overwrite, hooks=args.install_hooks,
skip_on_missing_config=args.allow_missing_config,
)
elif args.command == 'init-templatedir':
return init_templatedir(
args.config, store, args.directory,
hook_types=args.hook_types,
skip_on_missing_config=args.allow_missing_config,
)
elif args.command == 'install-hooks':
return install_hooks(args.config, store)
@ -432,23 +333,16 @@ def main(argv: Sequence[str] | None = None) -> int:
elif args.command == 'try-repo':
return try_repo(args)
elif args.command == 'uninstall':
return uninstall(
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)
return uninstall(hook_types=args.hook_types)
else:
raise NotImplementedError(
f'Command {args.command} not implemented.',
'Command {} not implemented.'.format(args.command),
)
raise AssertionError(
f'Command {args.command} failed to exit with a returncode',
'Command {} failed to exit with a returncode'.format(args.command),
)
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -0,0 +1,68 @@
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse
import os.path
import tarfile
from pre_commit import output
from pre_commit.util import cmd_output_b
from pre_commit.util import rmtree
from pre_commit.util import tmpdir
# This is a script for generating the tarred resources for git repo
# dependencies. Currently it's just for "vendoring" ruby support packages.
REPOS = (
('rbenv', 'git://github.com/rbenv/rbenv', 'a3fa9b7'),
('ruby-build', 'git://github.com/rbenv/ruby-build', '1a902f3'),
(
'ruby-download',
'git://github.com/garnieretienne/rvm-download',
'09bd7c6',
),
)
def make_archive(name, repo, ref, destdir):
"""Makes an archive of a repository in the given destdir.
:param text name: Name to give the archive. For instance foo. The file
that is created will be called foo.tar.gz.
:param text repo: Repository to clone.
:param text ref: Tag/SHA/branch to check out.
:param text destdir: Directory to place archives in.
"""
output_path = os.path.join(destdir, name + '.tar.gz')
with tmpdir() as tempdir:
# Clone the repository to the temporary directory
cmd_output_b('git', 'clone', repo, tempdir)
cmd_output_b('git', 'checkout', ref, cwd=tempdir)
# We don't want the '.git' directory
# It adds a bunch of size to the archive and we don't use it at
# runtime
rmtree(os.path.join(tempdir, '.git'))
with tarfile.open(output_path, 'w|gz') as tf:
tf.add(tempdir, name)
return output_path
def main(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument('--dest', default='pre_commit/resources')
args = parser.parse_args(argv)
for archive_name, repo, ref in REPOS:
output.write_line(
'Making {}.tar.gz for {}@{}'.format(archive_name, repo, ref),
)
make_archive(archive_name, repo, ref, args.dest)
if __name__ == '__main__':
exit(main())

View file

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

View file

@ -1,9 +1,7 @@
from __future__ import annotations
from __future__ import print_function
import argparse
import re
from collections.abc import Iterable
from collections.abc import Sequence
from cfgv import apply_defaults
@ -14,11 +12,7 @@ from pre_commit.clientlib import MANIFEST_HOOK_DICT
from pre_commit.commands.run import Classifier
def exclude_matches_any(
filenames: Iterable[str],
include: str,
exclude: str,
) -> bool:
def exclude_matches_any(filenames, include, exclude):
if exclude == '^$':
return True
include_re, exclude_re = re.compile(include), re.compile(exclude)
@ -28,47 +22,39 @@ def exclude_matches_any(
return False
def check_useless_excludes(config_file: str) -> int:
def check_useless_excludes(config_file):
config = load_config(config_file)
filenames = git.get_all_files()
classifier = Classifier.from_config(
filenames, config['files'], config['exclude'],
)
classifier = Classifier(git.get_all_files())
retv = 0
exclude = config['exclude']
if not exclude_matches_any(filenames, '', exclude):
if not exclude_matches_any(classifier.filenames, '', exclude):
print(
f'The global exclude pattern {exclude!r} does not match any files',
'The global exclude pattern {!r} does not match any files'
.format(exclude),
)
retv = 1
for repo in config['repos']:
for hook in repo['hooks']:
# the default of manifest hooks is `types: [file]` but we may
# be configuring a symlink hook while there's a broken symlink
hook.setdefault('types', [])
# Not actually a manifest dict, but this more accurately reflects
# the defaults applied during runtime
hook = apply_defaults(hook, MANIFEST_HOOK_DICT)
names = classifier.by_types(
classifier.filenames,
hook['types'],
hook['types_or'],
hook['exclude_types'],
)
names = classifier.filenames
types, exclude_types = hook['types'], hook['exclude_types']
names = classifier.by_types(names, types, exclude_types)
include, exclude = hook['files'], hook['exclude']
if not exclude_matches_any(names, include, exclude):
print(
f'The exclude pattern {exclude!r} for {hook["id"]} does '
f'not match any files',
'The exclude pattern {!r} for {} does not match any files'
.format(exclude, hook['id']),
)
retv = 1
return retv
def main(argv: Sequence[str] | None = None) -> int:
def main(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
args = parser.parse_args(argv)
@ -80,4 +66,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

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

View file

@ -1,33 +1,88 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import sys
from typing import Any
from typing import IO
from pre_commit import color
from pre_commit import five
from pre_commit.util import noop_context
def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None:
stream.write(s.encode())
def get_hook_message(
start,
postfix='',
end_msg=None,
end_len=0,
end_color=None,
use_color=None,
cols=80,
):
"""Prints a message for running a hook.
This currently supports three approaches:
# Print `start` followed by dots, leaving 6 characters at the end
>>> print_hook_message('start', end_len=6)
start...............................................................
# Print `start` followed by dots with the end message colored if coloring
# is specified and a newline afterwards
>>> print_hook_message(
'start',
end_msg='end',
end_color=color.RED,
use_color=True,
)
start...................................................................end
# Print `start` followed by dots, followed by the `postfix` message
# uncolored, followed by the `end_msg` colored if specified and a newline
# afterwards
>>> print_hook_message(
'start',
postfix='postfix ',
end_msg='end',
end_color=color.RED,
use_color=True,
)
start...........................................................postfix end
"""
if bool(end_msg) == bool(end_len):
raise ValueError('Expected one of (`end_msg`, `end_len`)')
if end_msg is not None and (end_color is None or use_color is None):
raise ValueError(
'`end_color` and `use_color` are required with `end_msg`',
)
if end_len:
return start + '.' * (cols - len(start) - end_len - 1)
else:
return '{}{}{}{}\n'.format(
start,
'.' * (cols - len(start) - len(postfix) - len(end_msg) - 1),
postfix,
color.format_color(end_msg, end_color, use_color),
)
stdout_byte_stream = getattr(sys.stdout, 'buffer', sys.stdout)
def write(s, stream=stdout_byte_stream):
stream.write(five.to_bytes(s))
stream.flush()
def write_line_b(
s: bytes | None = None,
stream: IO[bytes] = sys.stdout.buffer,
logfile_name: str | None = None,
) -> None:
with contextlib.ExitStack() as exit_stack:
output_streams = [stream]
if logfile_name:
stream = exit_stack.enter_context(open(logfile_name, 'ab'))
output_streams.append(stream)
def write_line(s=None, stream=stdout_byte_stream, logfile_name=None):
output_streams = [stream]
if logfile_name:
ctx = open(logfile_name, 'ab')
output_streams.append(ctx)
else:
ctx = noop_context()
with ctx:
for output_stream in output_streams:
if s is not None:
output_stream.write(s)
output_stream.write(five.to_bytes(s))
output_stream.write(b'\n')
output_stream.flush()
def write_line(s: str | None = None, **kwargs: Any) -> None:
write_line_b(s.encode() if s is not None else s, **kwargs)

View file

@ -1,36 +1,35 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import unicode_literals
import os.path
from collections.abc import Mapping
from typing import NoReturn
from identify.identify import parse_shebang_from_file
class ExecutableNotFoundError(OSError):
def to_output(self) -> tuple[int, bytes, None]:
return (1, self.args[0].encode(), None)
def to_output(self):
return (1, self.args[0].encode('UTF-8'), b'')
def parse_filename(filename: str) -> tuple[str, ...]:
def parse_filename(filename):
if not os.path.exists(filename):
return ()
else:
return parse_shebang_from_file(filename)
def find_executable(
exe: str, *, env: Mapping[str, str] | None = None,
) -> str | None:
def find_executable(exe, _environ=None):
exe = os.path.normpath(exe)
if os.sep in 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:
exts = environ['PATHEXT'].split(os.pathsep)
possible_exe_names = tuple(f'{exe}{ext}' for ext in exts) + (exe,)
possible_exe_names = tuple(
exe + ext.lower() for ext in environ['PATHEXT'].split(os.pathsep)
) + (exe,)
else:
possible_exe_names = (exe,)
@ -43,12 +42,12 @@ def find_executable(
return None
def normexe(orig: str, *, env: Mapping[str, str] | None = None) -> str:
def _error(msg: str) -> NoReturn:
raise ExecutableNotFoundError(f'Executable `{orig}` {msg}')
def normexe(orig):
def _error(msg):
raise ExecutableNotFoundError('Executable `{}` {}'.format(orig, msg))
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:
_error('not found')
return exe
@ -56,17 +55,13 @@ def normexe(orig: str, *, env: Mapping[str, str] | None = None) -> str:
_error('is a directory')
elif not os.path.isfile(orig):
_error('not found')
elif not os.access(orig, os.X_OK): # pragma: win32 no cover
elif not os.access(orig, os.X_OK): # pragma: windows no cover
_error('is not executable')
else:
return orig
def normalize_cmd(
cmd: tuple[str, ...],
*,
env: Mapping[str, str] | None = None,
) -> tuple[str, ...]:
def normalize_cmd(cmd):
"""Fixes for the following issues on windows
- https://bugs.python.org/issue8557
- windows does not parse shebangs
@ -74,12 +69,12 @@ def normalize_cmd(
This function also makes deep-path shebangs work just fine
"""
# Use PATH to determine the executable
exe = normexe(cmd[0], env=env)
exe = normexe(cmd[0])
# Figure out the shebang from the resulting command
cmd = parse_filename(exe) + (exe,) + cmd[1:]
# This could have given us back another bare executable
exe = normexe(cmd[0], env=env)
exe = normexe(cmd[0])
return (exe,) + cmd[1:]

View file

@ -1,18 +1,18 @@
from __future__ import annotations
from __future__ import unicode_literals
import collections
import os.path
from typing import NamedTuple
class Prefix(NamedTuple):
prefix_dir: str
class Prefix(collections.namedtuple('Prefix', ('prefix_dir',))):
__slots__ = ()
def path(self, *parts: str) -> str:
def path(self, *parts):
return os.path.normpath(os.path.join(self.prefix_dir, *parts))
def exists(self, *parts: str) -> bool:
def exists(self, *parts):
return os.path.exists(self.path(*parts))
def star(self, end: str) -> tuple[str, ...]:
def star(self, end):
paths = os.listdir(self.prefix_dir)
return tuple(path for path in paths if path.endswith(end))

View file

@ -1,119 +1,139 @@
from __future__ import annotations
from __future__ import unicode_literals
import collections
import io
import json
import logging
import os
from collections.abc import Sequence
from typing import Any
import shlex
import pre_commit.constants as C
from pre_commit.all_languages import languages
from pre_commit import five
from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import MANIFEST_HOOK_DICT
from pre_commit.clientlib import META
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.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
logger = logging.getLogger('pre_commit')
def _state_filename_v1(venv: str) -> str:
return os.path.join(venv, '.install_state_v1')
def _state(additional_deps):
return {'additional_dependencies': sorted(additional_deps)}
def _state_filename_v2(venv: str) -> str:
return os.path.join(venv, '.install_state_v2')
def _state_filename(prefix, venv):
return prefix.path(venv, '.install_state_v' + C.INSTALLED_STATE_VERSION)
def _state(additional_deps: Sequence[str]) -> object:
return {'additional_dependencies': additional_deps}
def _read_state(venv: str) -> object | None:
filename = _state_filename_v1(venv)
def _read_state(prefix, venv):
filename = _state_filename(prefix, venv)
if not os.path.exists(filename):
return None
else:
with open(filename) as f:
with io.open(filename) as f:
return json.load(f)
def _hook_installed(hook: Hook) -> bool:
lang = languages[hook.language]
if lang.ENVIRONMENT_DIR is None:
return True
venv = environment_dir(
hook.prefix,
lang.ENVIRONMENT_DIR,
hook.language_version,
)
return (
(
os.path.exists(_state_filename_v2(venv)) or
_read_state(venv) == _state(hook.additional_dependencies)
) and
not lang.health_check(hook.prefix, hook.language_version)
)
def _write_state(prefix, venv, state):
state_filename = _state_filename(prefix, venv)
staging = state_filename + 'staging'
with io.open(staging, 'w') as state_file:
state_file.write(five.to_text(json.dumps(state)))
# Move the file into place atomically to indicate we've installed
os.rename(staging, state_filename)
def _hook_install(hook: Hook) -> None:
logger.info(f'Installing environment for {hook.src}.')
logger.info('Once installed this environment will be reused.')
logger.info('This may take a few minutes...')
_KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items)
lang = languages[hook.language]
assert lang.ENVIRONMENT_DIR is not None
venv = environment_dir(
hook.prefix,
lang.ENVIRONMENT_DIR,
hook.language_version,
)
class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)):
__slots__ = ()
# There's potentially incomplete cleanup from previous runs
# Clean it up!
if os.path.exists(venv):
rmtree(venv)
@property
def cmd(self):
return tuple(shlex.split(self.entry)) + tuple(self.args)
with clean_path_on_failure(venv):
lang.install_environment(
hook.prefix, hook.language_version, hook.additional_dependencies,
@property
def install_key(self):
return (
self.prefix,
self.language,
self.language_version,
tuple(self.additional_dependencies),
)
health_error = lang.health_check(hook.prefix, hook.language_version)
if health_error:
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}',
def installed(self):
lang = languages[self.language]
venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version)
return (
venv is None or (
(
_read_state(self.prefix, venv) ==
_state(self.additional_dependencies)
) and
lang.healthy(self.prefix, self.language_version)
)
)
# TODO: remove v1 state writing, no longer needed after pre-commit 3.0
def install(self):
logger.info('Installing environment for {}.'.format(self.src))
logger.info('Once installed this environment will be reused.')
logger.info('This may take a few minutes...')
lang = languages[self.language]
venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version)
# There's potentially incomplete cleanup from previous runs
# Clean it up!
if self.prefix.exists(venv):
rmtree(self.prefix.path(venv))
lang.install_environment(
self.prefix, self.language_version, self.additional_dependencies,
)
# 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)
_write_state(self.prefix, venv, _state(self.additional_dependencies))
open(_state_filename_v2(venv), 'a+').close()
def run(self, file_args, color):
lang = languages[self.language]
return lang.run_hook(self, file_args, color)
@classmethod
def create(cls, src, prefix, dct):
# TODO: have cfgv do this (?)
extra_keys = set(dct) - set(_KEYS)
if extra_keys:
logger.warning(
'Unexpected key(s) present on {} => {}: '
'{}'.format(src, dct['id'], ', '.join(sorted(extra_keys))),
)
return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS})
def _hook(
*hook_dicts: dict[str, Any],
root_config: dict[str, Any],
) -> dict[str, Any]:
def _hook(*hook_dicts, **kwargs):
root_config = kwargs.pop('root_config')
assert not kwargs, kwargs
ret, rest = dict(hook_dicts[0]), hook_dicts[1:]
for dct in rest:
ret.update(dct)
version = ret['minimum_pre_commit_version']
if parse_version(version) > parse_version(C.VERSION):
logger.error(
'The hook `{}` requires pre-commit version {} but version {} '
'is installed. '
'Perhaps run `pip install --upgrade pre-commit`.'.format(
ret['id'], version, C.VERSION,
),
)
exit(1)
lang = ret['language']
if ret['language_version'] == C.DEFAULT:
ret['language_version'] = root_config['default_language_version'][lang]
@ -123,35 +143,13 @@ def _hook(
if not ret['stages']:
ret['stages'] = root_config['default_stages']
if languages[lang].ENVIRONMENT_DIR is None:
if ret['language_version'] != C.DEFAULT:
logger.error(
f'The hook `{ret["id"]}` specifies `language_version` but is '
f'using language `{lang}` which does not install an '
f'environment. '
f'Perhaps you meant to use a specific language?',
)
exit(1)
if ret['additional_dependencies']:
logger.error(
f'The hook `{ret["id"]}` specifies `additional_dependencies` '
f'but is using language `{lang}` which does not install an '
f'environment. '
f'Perhaps you meant to use a specific language?',
)
exit(1)
return ret
def _non_cloned_repository_hooks(
repo_config: dict[str, Any],
store: Store,
root_config: dict[str, Any],
) -> tuple[Hook, ...]:
def _prefix(language_name: str, deps: Sequence[str]) -> Prefix:
def _non_cloned_repository_hooks(repo_config, store, root_config):
def _prefix(language_name, deps):
language = languages[language_name]
# pygrep / script / system / docker_image do not have
# pcre / pygrep / script / system / docker_image do not have
# environments so they work out of the current directory
if language.ENVIRONMENT_DIR is None:
return Prefix(os.getcwd())
@ -168,11 +166,7 @@ def _non_cloned_repository_hooks(
)
def _cloned_repository_hooks(
repo_config: dict[str, Any],
store: Store,
root_config: dict[str, Any],
) -> tuple[Hook, ...]:
def _cloned_repository_hooks(repo_config, store, root_config):
repo, rev = repo_config['repo'], repo_config['rev']
manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE)
by_id = {hook['id']: hook for hook in load_manifest(manifest_path)}
@ -180,9 +174,10 @@ def _cloned_repository_hooks(
for hook in repo_config['hooks']:
if hook['id'] not in by_id:
logger.error(
f'`{hook["id"]}` is not present in repository {repo}. '
f'Typo? Perhaps it is introduced in a newer version? '
f'Often `pre-commit autoupdate` fixes this.',
'`{}` is not present in repository {}. '
'Typo? Perhaps it is introduced in a newer version? '
'Often `pre-commit autoupdate` fixes this.'
.format(hook['id'], repo),
)
exit(1)
@ -200,23 +195,19 @@ def _cloned_repository_hooks(
)
def _repository_hooks(
repo_config: dict[str, Any],
store: Store,
root_config: dict[str, Any],
) -> tuple[Hook, ...]:
def _repository_hooks(repo_config, store, root_config):
if repo_config['repo'] in {LOCAL, META}:
return _non_cloned_repository_hooks(repo_config, store, root_config)
else:
return _cloned_repository_hooks(repo_config, store, root_config)
def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None:
def _need_installed() -> list[Hook]:
seen: set[tuple[Prefix, str, str, tuple[str, ...]]] = set()
def install_hook_envs(hooks, store):
def _need_installed():
seen = set()
ret = []
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():
ret.append(hook)
seen.add(hook.install_key)
return ret
@ -226,10 +217,10 @@ def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None:
with store.exclusive_lock():
# Another process may have already completed this work
for hook in _need_installed():
_hook_install(hook)
hook.install()
def all_hooks(root_config: dict[str, Any], store: Store) -> tuple[Hook, ...]:
def all_hooks(root_config, store):
return tuple(
hook
for repo in root_config['repos']

View file

@ -1,7 +0,0 @@
Copyright 2021 RStudio, PBC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,6 +0,0 @@
use ExtUtils::MakeMaker;
WriteMakefile(
NAME => "PreCommitPlaceholder",
VERSION => "0.0.1",
);

View file

@ -1,440 +0,0 @@
local({
# the requested version of renv
version <- "0.12.5"
# the project directory
project <- getwd()
# avoid recursion
if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA)))
return(invisible(TRUE))
# signal that we're loading renv during R startup
Sys.setenv("RENV_R_INITIALIZING" = "true")
on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE)
# signal that we've consented to use renv
options(renv.consent = TRUE)
# load the 'utils' package eagerly -- this ensures that renv shims, which
# mask 'utils' packages, will come first on the search path
library(utils, lib.loc = .Library)
# check to see if renv has already been loaded
if ("renv" %in% loadedNamespaces()) {
# if renv has already been loaded, and it's the requested version of renv,
# nothing to do
spec <- .getNamespaceInfo(.getNamespace("renv"), "spec")
if (identical(spec[["version"]], version))
return(invisible(TRUE))
# otherwise, unload and attempt to load the correct version of renv
unloadNamespace("renv")
}
# load bootstrap tools
bootstrap <- function(version, library) {
# attempt to download renv
tarball <- tryCatch(renv_bootstrap_download(version), error = identity)
if (inherits(tarball, "error"))
stop("failed to download renv ", version)
# now attempt to install
status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity)
if (inherits(status, "error"))
stop("failed to install renv ", version)
}
renv_bootstrap_tests_running <- function() {
getOption("renv.tests.running", default = FALSE)
}
renv_bootstrap_repos <- function() {
# check for repos override
repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA)
if (!is.na(repos))
return(repos)
# if we're testing, re-use the test repositories
if (renv_bootstrap_tests_running())
return(getOption("renv.tests.repos"))
# retrieve current repos
repos <- getOption("repos")
# ensure @CRAN@ entries are resolved
repos[repos == "@CRAN@"] <- "https://cloud.r-project.org"
# add in renv.bootstrap.repos if set
default <- c(CRAN = "https://cloud.r-project.org")
extra <- getOption("renv.bootstrap.repos", default = default)
repos <- c(repos, extra)
# remove duplicates that might've snuck in
dupes <- duplicated(repos) | duplicated(names(repos))
repos[!dupes]
}
renv_bootstrap_download <- function(version) {
# if the renv version number has 4 components, assume it must
# be retrieved via github
nv <- numeric_version(version)
components <- unclass(nv)[[1]]
methods <- if (length(components) == 4L) {
list(
renv_bootstrap_download_github
)
} else {
list(
renv_bootstrap_download_cran_latest,
renv_bootstrap_download_cran_archive
)
}
for (method in methods) {
path <- tryCatch(method(version), error = identity)
if (is.character(path) && file.exists(path))
return(path)
}
stop("failed to download renv ", version)
}
renv_bootstrap_download_impl <- function(url, destfile) {
mode <- "wb"
# https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715
fixup <-
Sys.info()[["sysname"]] == "Windows" &&
substring(url, 1L, 5L) == "file:"
if (fixup)
mode <- "w+b"
utils::download.file(
url = url,
destfile = destfile,
mode = mode,
quiet = TRUE
)
}
renv_bootstrap_download_cran_latest <- function(version) {
repos <- renv_bootstrap_download_cran_latest_find(version)
message("* Downloading renv ", version, " from CRAN ... ", appendLF = FALSE)
info <- tryCatch(
utils::download.packages(
pkgs = "renv",
repos = repos,
destdir = tempdir(),
quiet = TRUE
),
condition = identity
)
if (inherits(info, "condition")) {
message("FAILED")
return(FALSE)
}
message("OK")
info[1, 2]
}
renv_bootstrap_download_cran_latest_find <- function(version) {
all <- renv_bootstrap_repos()
for (repos in all) {
db <- tryCatch(
as.data.frame(
x = utils::available.packages(repos = repos),
stringsAsFactors = FALSE
),
error = identity
)
if (inherits(db, "error"))
next
entry <- db[db$Package %in% "renv" & db$Version %in% version, ]
if (nrow(entry) == 0)
next
return(repos)
}
fmt <- "renv %s is not available from your declared package repositories"
stop(sprintf(fmt, version))
}
renv_bootstrap_download_cran_archive <- function(version) {
name <- sprintf("renv_%s.tar.gz", version)
repos <- renv_bootstrap_repos()
urls <- file.path(repos, "src/contrib/Archive/renv", name)
destfile <- file.path(tempdir(), name)
message("* Downloading renv ", version, " from CRAN archive ... ", appendLF = FALSE)
for (url in urls) {
status <- tryCatch(
renv_bootstrap_download_impl(url, destfile),
condition = identity
)
if (identical(status, 0L)) {
message("OK")
return(destfile)
}
}
message("FAILED")
return(FALSE)
}
renv_bootstrap_download_github <- function(version) {
enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE")
if (!identical(enabled, "TRUE"))
return(FALSE)
# prepare download options
pat <- Sys.getenv("GITHUB_PAT")
if (nzchar(Sys.which("curl")) && nzchar(pat)) {
fmt <- "--location --fail --header \"Authorization: token %s\""
extra <- sprintf(fmt, pat)
saved <- options("download.file.method", "download.file.extra")
options(download.file.method = "curl", download.file.extra = extra)
on.exit(do.call(base::options, saved), add = TRUE)
} else if (nzchar(Sys.which("wget")) && nzchar(pat)) {
fmt <- "--header=\"Authorization: token %s\""
extra <- sprintf(fmt, pat)
saved <- options("download.file.method", "download.file.extra")
options(download.file.method = "wget", download.file.extra = extra)
on.exit(do.call(base::options, saved), add = TRUE)
}
message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE)
url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version)
name <- sprintf("renv_%s.tar.gz", version)
destfile <- file.path(tempdir(), name)
status <- tryCatch(
renv_bootstrap_download_impl(url, destfile),
condition = identity
)
if (!identical(status, 0L)) {
message("FAILED")
return(FALSE)
}
message("OK")
return(destfile)
}
renv_bootstrap_install <- function(version, tarball, library) {
# attempt to install it into project library
message("* Installing renv ", version, " ... ", appendLF = FALSE)
dir.create(library, showWarnings = FALSE, recursive = TRUE)
# invoke using system2 so we can capture and report output
bin <- R.home("bin")
exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R"
r <- file.path(bin, exe)
args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball))
output <- system2(r, args, stdout = TRUE, stderr = TRUE)
message("Done!")
# check for successful install
status <- attr(output, "status")
if (is.numeric(status) && !identical(status, 0L)) {
header <- "Error installing renv:"
lines <- paste(rep.int("=", nchar(header)), collapse = "")
text <- c(header, lines, output)
writeLines(text, con = stderr())
}
status
}
renv_bootstrap_prefix <- function() {
# construct version prefix
version <- paste(R.version$major, R.version$minor, sep = ".")
prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-")
# include SVN revision for development versions of R
# (to avoid sharing platform-specific artefacts with released versions of R)
devel <-
identical(R.version[["status"]], "Under development (unstable)") ||
identical(R.version[["nickname"]], "Unsuffered Consequences")
if (devel)
prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r")
# build list of path components
components <- c(prefix, R.version$platform)
# include prefix if provided by user
prefix <- Sys.getenv("RENV_PATHS_PREFIX")
if (nzchar(prefix))
components <- c(prefix, components)
# build prefix
paste(components, collapse = "/")
}
renv_bootstrap_library_root_name <- function(project) {
# use project name as-is if requested
asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE")
if (asis)
return(basename(project))
# otherwise, disambiguate based on project's path
id <- substring(renv_bootstrap_hash_text(project), 1L, 8L)
paste(basename(project), id, sep = "-")
}
renv_bootstrap_library_root <- function(project) {
path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA)
if (!is.na(path))
return(path)
path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA)
if (!is.na(path)) {
name <- renv_bootstrap_library_root_name(project)
return(file.path(path, name))
}
file.path(project, "renv/library")
}
renv_bootstrap_validate_version <- function(version) {
loadedversion <- utils::packageDescription("renv", fields = "Version")
if (version == loadedversion)
return(TRUE)
# assume four-component versions are from GitHub; three-component
# versions are from CRAN
components <- strsplit(loadedversion, "[.-]")[[1]]
remote <- if (length(components) == 4L)
paste("rstudio/renv", loadedversion, sep = "@")
else
paste("renv", loadedversion, sep = "@")
fmt <- paste(
"renv %1$s was loaded from project library, but this project is configured to use renv %2$s.",
"Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.",
"Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.",
sep = "\n"
)
msg <- sprintf(fmt, loadedversion, version, remote)
warning(msg, call. = FALSE)
FALSE
}
renv_bootstrap_hash_text <- function(text) {
hashfile <- tempfile("renv-hash-")
on.exit(unlink(hashfile), add = TRUE)
writeLines(text, con = hashfile)
tools::md5sum(hashfile)
}
renv_bootstrap_load <- function(project, libpath, version) {
# try to load renv from the project library
if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE))
return(FALSE)
# warn if the version of renv loaded does not match
renv_bootstrap_validate_version(version)
# load the project
renv::load(project)
TRUE
}
# construct path to library root
root <- renv_bootstrap_library_root(project)
# construct library prefix for platform
prefix <- renv_bootstrap_prefix()
# construct full libpath
libpath <- file.path(root, prefix)
# attempt to load
if (renv_bootstrap_load(project, libpath, version))
return(TRUE)
# load failed; inform user we're about to bootstrap
prefix <- paste("# Bootstrapping renv", version)
postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "")
header <- paste(prefix, postfix)
message(header)
# perform bootstrap
bootstrap(version, libpath)
# exit early if we're just testing bootstrap
if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA)))
return(TRUE)
# try again to load
if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) {
message("* Successfully installed and loaded renv ", version, ".")
return(renv::load())
}
# failed to download or load renv; warn the user
msg <- c(
"Failed to find an renv installation: the project will not be loaded.",
"Use `renv::activate()` to re-initialize the project."
)
warning(paste(msg, collapse = "\n"), call. = FALSE)
})

View file

@ -1,9 +0,0 @@
channels:
- conda-forge
- defaults
dependencies:
# This cannot be empty as otherwise no environment will be created.
# We're using openssl here as it is available on all system and will
# most likely be always installed anyways.
# See https://github.com/conda/conda/issues/9487
- openssl

View file

@ -1 +0,0 @@
module pre-commit-placeholder-empty-module

View file

@ -1,4 +1,4 @@
{
"name": "pre_commit_placeholder_package",
"name": "pre_commit_dummy_package",
"version": "0.0.0"
}

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

@ -0,0 +1,6 @@
Gem::Specification.new do |s|
s.name = 'pre_commit_dummy_package'
s.version = '0.0.0'
s.summary = 'dummy gem for pre-commit hooks'
s.authors = ['Anthony Sottile']
end

View file

@ -1,6 +0,0 @@
Gem::Specification.new do |s|
s.name = 'pre_commit_placeholder_package'
s.version = '0.0.0'
s.summary = 'placeholder gem for pre-commit hooks'
s.authors = ['Anthony Sottile']
end

View file

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

View file

@ -1,20 +0,0 @@
{
"R": {
"Version": "4.0.3",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cran.rstudio.com"
}
]
},
"Packages": {
"renv": {
"Package": "renv",
"Version": "0.12.5",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c"
}
}
}

View file

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

View file

@ -1,20 +1,195 @@
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03
#!/usr/bin/env python3
"""File generated by pre-commit: https://pre-commit.com"""
from __future__ import print_function
import distutils.spawn
import os
import subprocess
import sys
# work around https://github.com/Homebrew/homebrew-core/issues/30445
os.environ.pop('__PYVENV_LAUNCHER__', None)
HERE = os.path.dirname(os.path.abspath(__file__))
Z40 = '0' * 40
ID_HASH = '138fd403232d2ddd5efb44317e38bf03'
# start templated
INSTALL_PYTHON=''
ARGS=(hook-impl)
CONFIG = None
HOOK_TYPE = None
INSTALL_PYTHON = None
SKIP_ON_MISSING_CONFIG = None
# end templated
HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")
if [ -x "$INSTALL_PYTHON" ]; then
exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
exec pre-commit "${ARGS[@]}"
else
echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2
exit 1
fi
class EarlyExit(RuntimeError):
pass
class FatalError(RuntimeError):
pass
def _norm_exe(exe):
"""Necessary for shebang support on windows.
roughly lifted from `identify.identify.parse_shebang`
"""
with open(exe, 'rb') as f:
if f.read(2) != b'#!':
return ()
try:
first_line = f.readline().decode('UTF-8')
except UnicodeDecodeError:
return ()
cmd = first_line.split()
if cmd[0] == '/usr/bin/env':
del cmd[0]
return tuple(cmd)
def _run_legacy():
if __file__.endswith('.legacy'):
raise SystemExit(
"bug: pre-commit's script is installed in migration mode\n"
'run `pre-commit install -f --hook-type {}` to fix this\n\n'
'Please report this bug at '
'https://github.com/pre-commit/pre-commit/issues'.format(
HOOK_TYPE,
),
)
if HOOK_TYPE == 'pre-push':
stdin = getattr(sys.stdin, 'buffer', sys.stdin).read()
else:
stdin = None
legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE))
if os.access(legacy_hook, os.X_OK):
cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:])
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None)
proc.communicate(stdin)
return proc.returncode, stdin
else:
return 0, stdin
def _validate_config():
cmd = ('git', 'rev-parse', '--show-toplevel')
top_level = subprocess.check_output(cmd).decode('UTF-8').strip()
cfg = os.path.join(top_level, CONFIG)
if os.path.isfile(cfg):
pass
elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'):
print(
'`{}` config file not found. '
'Skipping `pre-commit`.'.format(CONFIG),
)
raise EarlyExit()
else:
raise FatalError(
'No {} file was found\n'
'- To temporarily silence this, run '
'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'
'- To permanently silence this, install pre-commit with the '
'--allow-missing-config option\n'
'- To uninstall pre-commit run '
'`pre-commit uninstall`'.format(CONFIG),
)
def _exe():
with open(os.devnull, 'wb') as devnull:
for exe in (INSTALL_PYTHON, sys.executable):
try:
if not subprocess.call(
(exe, '-c', 'import pre_commit.main'),
stdout=devnull, stderr=devnull,
):
return (exe, '-m', 'pre_commit.main', 'run')
except OSError:
pass
if distutils.spawn.find_executable('pre-commit'):
return ('pre-commit', 'run')
raise FatalError(
'`pre-commit` not found. Did you forget to activate your virtualenv?',
)
def _rev_exists(rev):
return not subprocess.call(('git', 'rev-list', '--quiet', rev))
def _pre_push(stdin):
remote = sys.argv[1]
opts = ()
for line in stdin.decode('UTF-8').splitlines():
_, local_sha, _, remote_sha = line.split()
if local_sha == Z40:
continue
elif remote_sha != Z40 and _rev_exists(remote_sha):
opts = ('--origin', local_sha, '--source', remote_sha)
else:
# ancestors not found in remote
ancestors = subprocess.check_output((
'git', 'rev-list', local_sha, '--topo-order', '--reverse',
'--not', '--remotes={}'.format(remote),
)).decode().strip()
if not ancestors:
continue
else:
first_ancestor = ancestors.splitlines()[0]
cmd = ('git', 'rev-list', '--max-parents=0', local_sha)
roots = set(subprocess.check_output(cmd).decode().splitlines())
if first_ancestor in roots:
# pushing the whole tree including root commit
opts = ('--all-files',)
else:
cmd = ('git', 'rev-parse', '{}^'.format(first_ancestor))
source = subprocess.check_output(cmd).decode().strip()
opts = ('--origin', local_sha, '--source', source)
if opts:
return opts
else:
# An attempt to push an empty changeset
raise EarlyExit()
def _opts(stdin):
fns = {
'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]),
'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]),
'pre-commit': lambda _: (),
'pre-push': _pre_push,
}
stage = HOOK_TYPE.replace('pre-', '')
return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin)
if sys.version_info < (3, 7): # https://bugs.python.org/issue25942
def _subprocess_call(cmd): # this is the python 2.7 implementation
return subprocess.Popen(cmd).wait()
else:
_subprocess_call = subprocess.call
def main():
retv, stdin = _run_legacy()
try:
_validate_config()
return retv | _subprocess_call(_exe() + _opts(stdin))
except EarlyExit:
return retv
except FatalError as e:
print(e.args[0])
return 1
except KeyboardInterrupt:
return 1
if __name__ == '__main__':
exit(main())

Binary file not shown.

View file

@ -1,29 +1,23 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import io
import logging
import os.path
import time
from collections.abc import Generator
from pre_commit import git
from pre_commit.errors import FatalError
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
from pre_commit.util import mkdirp
from pre_commit.xargs import xargs
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):
args = ('apply', '--whitespace=nowarn', patch)
try:
cmd_output_b('git', *args)
@ -33,7 +27,7 @@ def _git_apply(patch: str) -> None:
@contextlib.contextmanager
def _intent_to_add_cleared() -> Generator[None]:
def _intent_to_add_cleared():
intent_to_add = git.intent_to_add_files()
if intent_to_add:
logger.warning('Unstaged intent-to-add files detected.')
@ -48,37 +42,28 @@ def _intent_to_add_cleared() -> Generator[None]:
@contextlib.contextmanager
def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
def _unstaged_changes_cleared(patch_dir):
tree = cmd_output('git', 'write-tree')[1].strip()
diff_cmd = (
retcode, diff_stdout_binary, _ = cmd_output_b(
'git', 'diff-index', '--ignore-submodules', '--binary',
'--exit-code', '--no-color', '--no-ext-diff', tree, '--',
retcode=None,
)
retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False)
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()}'
if retcode and diff_stdout_binary.strip():
patch_filename = 'patch{}'.format(int(time.time()))
patch_filename = os.path.join(patch_dir, patch_filename)
logger.warning('Unstaged files detected.')
logger.info(f'Stashing unstaged files to {patch_filename}.')
logger.info(
'Stashing unstaged files to {}.'.format(patch_filename),
)
# Save the current unstaged changes as a patch
os.makedirs(patch_dir, exist_ok=True)
with open(patch_filename, 'wb') as patch_file:
patch_file.write(diff_stdout)
# prevent recursive post-checkout hooks (#1418)
no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')
mkdirp(patch_dir)
with io.open(patch_filename, 'wb') as patch_file:
patch_file.write(diff_stdout_binary)
# Clear the working directory of unstaged changes
cmd_output_b('git', 'checkout', '--', '.')
try:
cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)
yield
finally:
# Try to apply the patch we saved
@ -92,20 +77,17 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
# We failed to apply the patch, presumably due to fixes made
# by hooks.
# Roll back the changes made by hooks.
cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)
cmd_output_b('git', 'checkout', '--', '.')
_git_apply(patch_filename)
logger.info(f'Restored changes from {patch_filename}.')
else: # pragma: win32 no cover
# some error occurred while requesting the diff
e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr)
raise FatalError(
f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}',
)
logger.info('Restored changes from {}.'.format(patch_filename))
else:
# There weren't any staged files so we don't need to do anything
# special
yield
@contextlib.contextmanager
def staged_files_only(patch_dir: str) -> Generator[None]:
def staged_files_only(patch_dir):
"""Clear any unstaged changes from the git working directory inside this
context.
"""

View file

@ -1,75 +1,49 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import io
import logging
import os.path
import sqlite3
import tempfile
from collections.abc import Callable
from collections.abc import Generator
from collections.abc import Sequence
import pre_commit.constants as C
from pre_commit import clientlib
from pre_commit import file_lock
from pre_commit import git
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 mkdirp
from pre_commit.util import resource_text
from pre_commit.util import rmtree
logger = logging.getLogger('pre_commit')
def _get_default_directory() -> str:
def _get_default_directory():
"""Returns the default directory for the Store. This is intentionally
underscored to indicate that `Store.get_default_directory` is the intended
way to get this information. This is also done so
`Store.get_default_directory` can be mocked in tests and
`_get_default_directory` can be tested.
"""
ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join(
return os.environ.get('PRE_COMMIT_HOME') or os.path.join(
os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),
'pre-commit',
)
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(object):
get_default_directory = staticmethod(_get_default_directory)
def __init__(self, directory: str | None = None) -> None:
def __init__(self, directory=None):
self.directory = directory or Store.get_default_directory()
self.db_path = os.path.join(self.directory, 'db.db')
self.readonly = (
os.path.exists(self.directory) and
not os.access(self.directory, os.W_OK)
)
if not os.path.exists(self.directory):
os.makedirs(self.directory, exist_ok=True)
with open(os.path.join(self.directory, 'README'), 'w') as f:
mkdirp(self.directory)
with io.open(os.path.join(self.directory, 'README'), 'w') as f:
f.write(
'This directory is maintained by the pre-commit project.\n'
'Learn more: https://github.com/pre-commit/pre-commit\n',
@ -95,24 +69,21 @@ class Store:
' PRIMARY KEY (repo, ref)'
');',
)
self._create_configs_table(db)
self._create_config_table_if_not_exists(db)
# Atomic file move
os.replace(tmpfile, self.db_path)
os.rename(tmpfile, self.db_path)
@contextlib.contextmanager
def exclusive_lock(self) -> Generator[None]:
def blocked_cb() -> None: # pragma: no cover (tests are in-process)
def exclusive_lock(self):
def blocked_cb(): # pragma: no cover (tests are single-process)
logger.info('Locking pre-commit directory')
with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb):
yield
@contextlib.contextmanager
def connect(
self,
db_path: str | None = None,
) -> Generator[sqlite3.Connection]:
def connect(self, db_path=None):
db_path = db_path or self.db_path
# sqlite doesn't close its fd with its contextmanager >.<
# contextlib.closing fixes this.
@ -123,30 +94,24 @@ class Store:
yield db
@classmethod
def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str:
def db_repo_name(cls, repo, deps):
if deps:
return f'{repo}:{",".join(deps)}'
return '{}:{}'.format(repo, ','.join(sorted(deps)))
else:
return repo
def _new_repo(
self,
repo: str,
ref: str,
deps: Sequence[str],
make_strategy: Callable[[str], None],
) -> str:
original_repo = repo
def _new_repo(self, repo, ref, deps, make_strategy):
repo = self.db_repo_name(repo, deps)
def _get_result() -> str | None:
def _get_result():
# Check if we already exist
with self.connect() as db:
result = db.execute(
'SELECT path FROM repos WHERE repo = ? AND ref = ?',
(repo, ref),
).fetchone()
return result[0] if result else None
if result:
return result[0]
result = _get_result()
if result:
@ -157,7 +122,7 @@ class Store:
if result: # pragma: no cover (race)
return result
logger.info(f'Initializing environment for {repo}.')
logger.info('Initializing environment for {}.'.format(repo))
directory = tempfile.mkdtemp(prefix='repo', dir=self.directory)
with clean_path_on_failure(directory):
@ -169,19 +134,16 @@ class Store:
'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',
[repo, ref, directory],
)
clientlib.warn_for_stages_on_repo_init(original_repo, directory)
return directory
def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
def _complete_clone(self, ref, git_cmd):
"""Perform a complete clone of a repository and its submodules """
git_cmd('fetch', 'origin', '--tags')
git_cmd('checkout', ref)
git_cmd('submodule', 'update', '--init', '--recursive')
def _shallow_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
def _shallow_clone(self, ref, git_cmd):
"""Perform a shallow clone of a repository and its submodules """
git_config = 'protocol.version=2'
@ -192,14 +154,14 @@ class Store:
'--depth=1',
)
def clone(self, repo: str, ref: str, deps: Sequence[str] = ()) -> str:
def clone(self, repo, ref, deps=()):
"""Clone the given url and checkout the specific ref."""
def clone_strategy(directory: str) -> None:
def clone_strategy(directory):
git.init_repo(directory, repo)
env = git.no_git_env()
def _git_cmd(*args: str) -> None:
def _git_cmd(*args):
cmd_output_b('git', *args, cwd=directory, env=env)
try:
@ -209,12 +171,33 @@ class Store:
return self._new_repo(repo, ref, deps, clone_strategy)
def make_local(self, deps: Sequence[str]) -> str:
LOCAL_RESOURCES = (
'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json',
'pre_commit_dummy_package.gemspec', 'setup.py',
)
def make_local(self, deps):
def make_local_strategy(directory):
for resource in self.LOCAL_RESOURCES:
contents = resource_text('empty_template_{}'.format(resource))
with io.open(os.path.join(directory, resource), '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):
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(
'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_if_not_exists(self, db):
db.executescript(
'CREATE TABLE IF NOT EXISTS configs ('
' path TEXT NOT NULL,'
@ -222,14 +205,35 @@ class Store:
');',
)
def mark_config_used(self, path: str) -> None:
if self.readonly: # pragma: win32 no cover
return
def mark_config_used(self, path):
path = os.path.realpath(path)
# don't insert config files that do not exist
if not os.path.exists(path):
return
with self.connect() as db:
# TODO: eventually remove this and only create in _create
self._create_configs_table(db)
self._create_config_table_if_not_exists(db)
db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
def select_all_configs(self):
with self.connect() as db:
self._create_config_table_if_not_exists(db)
rows = db.execute('SELECT path FROM configs').fetchall()
return [path for path, in rows]
def delete_configs(self, configs):
with self.connect() as db:
rows = [(path,) for path in configs]
db.executemany('DELETE FROM configs WHERE path = ?', rows)
def select_all_repos(self):
with self.connect() as db:
return db.execute('SELECT repo, ref, path from repos').fetchall()
def delete_repo(self, db_repo_name, ref, path):
with self.connect() as db:
db.execute(
'DELETE FROM repos WHERE repo = ? and ref = ?',
(db_repo_name, ref),
)
rmtree(path)

View file

@ -1,31 +1,37 @@
from __future__ import annotations
from __future__ import unicode_literals
import contextlib
import errno
import importlib.resources
import os.path
import shutil
import stat
import subprocess
import sys
from collections.abc import Callable
from collections.abc import Generator
from types import TracebackType
from typing import Any
import tempfile
import six
from pre_commit import five
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
def force_bytes(exc: Any) -> bytes:
with contextlib.suppress(TypeError):
return bytes(exc)
with contextlib.suppress(Exception):
return str(exc).encode()
return f'<unprintable {type(exc).__name__} object>'.encode()
def mkdirp(path):
try:
os.makedirs(path)
except OSError:
if not os.path.exists(path):
raise
@contextlib.contextmanager
def clean_path_on_failure(path: str) -> Generator[None]:
def clean_path_on_failure(path):
"""Cleans up the directory on an exceptional failure."""
try:
yield
@ -35,138 +41,158 @@ def clean_path_on_failure(path: str) -> Generator[None]:
raise
def resource_text(filename: str) -> str:
files = importlib.resources.files('pre_commit.resources')
return files.joinpath(filename).read_text()
@contextlib.contextmanager
def noop_context():
yield
def make_executable(filename: str) -> None:
@contextlib.contextmanager
def tmpdir():
"""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):
return open_binary('pre_commit.resources', filename)
def resource_text(filename):
return read_text('pre_commit.resources', filename)
def make_executable(filename):
original_mode = os.stat(filename).st_mode
new_mode = original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
os.chmod(filename, new_mode)
os.chmod(
filename, original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH,
)
class CalledProcessError(RuntimeError):
def __init__(
self,
returncode: int,
cmd: tuple[str, ...],
stdout: bytes,
stderr: bytes | None,
) -> None:
super().__init__(returncode, cmd, stdout, stderr)
def __init__(self, returncode, cmd, expected_returncode, output=None):
super(CalledProcessError, self).__init__(
returncode, cmd, expected_returncode, output,
)
self.returncode = returncode
self.cmd = cmd
self.stdout = stdout
self.stderr = stderr
self.expected_returncode = expected_returncode
self.output = output
def __bytes__(self) -> bytes:
def _indent_or_none(part: bytes | None) -> bytes:
if part:
return b'\n ' + part.replace(b'\n', b'\n ').rstrip()
def to_bytes(self):
output = []
for maybe_text in self.output:
if maybe_text:
output.append(
b'\n ' +
five.to_bytes(maybe_text).replace(b'\n', b'\n '),
)
else:
return b' (none)'
output.append(b'(none)')
return b''.join((
f'command: {self.cmd!r}\n'.encode(),
f'return code: {self.returncode}\n'.encode(),
b'stdout:', _indent_or_none(self.stdout), b'\n',
b'stderr:', _indent_or_none(self.stderr),
five.to_bytes(
'Command: {!r}\n'
'Return code: {}\n'
'Expected return code: {}\n'.format(
self.cmd, self.returncode, self.expected_returncode,
),
),
b'Output: ', output[0], b'\n',
b'Errors: ', output[1],
))
def __str__(self) -> str:
return self.__bytes__().decode()
def to_text(self):
return self.to_bytes().decode('UTF-8')
if six.PY2: # pragma: no cover (py2)
__str__ = to_bytes
__unicode__ = to_text
else: # pragma: no cover (py3)
__bytes__ = to_bytes
__str__ = to_text
def _setdefault_kwargs(kwargs: dict[str, Any]) -> None:
def _cmd_kwargs(*cmd, **kwargs):
# py2/py3 on windows are more strict about the types here
cmd = tuple(five.n(arg) for arg in cmd)
kwargs['env'] = {
five.n(key): five.n(value)
for key, value in kwargs.pop('env', {}).items()
} or None
for arg in ('stdin', 'stdout', 'stderr'):
kwargs.setdefault(arg, subprocess.PIPE)
return cmd, kwargs
def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]:
return 1, force_bytes(e).rstrip(b'\n') + b'\n', None
def cmd_output_b(
*cmd: str,
check: bool = True,
**kwargs: Any,
) -> tuple[int, bytes, bytes | None]:
_setdefault_kwargs(kwargs)
def cmd_output_b(*cmd, **kwargs):
retcode = kwargs.pop('retcode', 0)
cmd, kwargs = _cmd_kwargs(*cmd, **kwargs)
try:
cmd = parse_shebang.normalize_cmd(cmd, env=kwargs.get('env'))
cmd = parse_shebang.normalize_cmd(cmd)
except parse_shebang.ExecutableNotFoundError as e:
returncode, stdout_b, stderr_b = e.to_output()
else:
try:
proc = subprocess.Popen(cmd, **kwargs)
except OSError as e:
returncode, stdout_b, stderr_b = _oserror_to_output(e)
else:
stdout_b, stderr_b = proc.communicate()
returncode = proc.returncode
proc = subprocess.Popen(cmd, **kwargs)
stdout_b, stderr_b = proc.communicate()
returncode = proc.returncode
if check and returncode:
raise CalledProcessError(returncode, cmd, stdout_b, stderr_b)
if retcode is not None and retcode != returncode:
raise CalledProcessError(
returncode, cmd, retcode, output=(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, **kwargs):
returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs)
stdout = stdout_b.decode() if stdout_b is not None else None
stderr = stderr_b.decode() if stderr_b is not None else None
stdout = stdout_b.decode('UTF-8') if stdout_b is not None else None
stderr = stderr_b.decode('UTF-8') if stderr_b is not None else None
return returncode, stdout, stderr
if sys.platform != 'win32': # pragma: win32 no cover
if os.name != 'nt': # pragma: windows no cover
from os import openpty
import termios
class Pty:
def __init__(self) -> None:
self.r: int | None = None
self.w: int | None = None
class Pty(object):
def __init__(self):
self.r = self.w = None
def __enter__(self) -> Pty:
def __enter__(self):
self.r, self.w = openpty()
# tty flags normally change \n to \r\n
attrs = termios.tcgetattr(self.w)
assert isinstance(attrs[1], int)
attrs = termios.tcgetattr(self.r)
attrs[1] &= ~(termios.ONLCR | termios.OPOST)
termios.tcsetattr(self.w, termios.TCSANOW, attrs)
termios.tcsetattr(self.r, termios.TCSANOW, attrs)
return self
def close_w(self) -> None:
def close_w(self):
if self.w is not None:
os.close(self.w)
self.w = None
def close_r(self) -> None:
def close_r(self):
assert self.r is not None
os.close(self.r)
self.r = None
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
def __exit__(self, exc_type, exc_value, traceback):
self.close_w()
self.close_r()
def cmd_output_p(
*cmd: str,
check: bool = True,
**kwargs: Any,
) -> tuple[int, bytes, bytes | None]:
assert check is False
def cmd_output_p(*cmd, **kwargs):
assert kwargs.pop('retcode') is None
assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr']
_setdefault_kwargs(kwargs)
cmd, kwargs = _cmd_kwargs(*cmd, **kwargs)
try:
cmd = parse_shebang.normalize_cmd(cmd)
@ -174,13 +200,8 @@ if sys.platform != 'win32': # pragma: win32 no cover
return e.to_output()
with open(os.devnull) as devnull, Pty() as pty:
assert pty.r is not None
kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w})
try:
proc = subprocess.Popen(cmd, **kwargs)
except OSError as e:
return _oserror_to_output(e)
proc = subprocess.Popen(cmd, **kwargs)
pty.close_w()
buf = b''
@ -202,38 +223,22 @@ else: # pragma: no cover
cmd_output_p = cmd_output_b
def _handle_readonly(
func: Callable[[str], object],
path: str,
exc: BaseException,
) -> None:
if (
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
def rmtree(path):
"""On windows, rmtree fails for readonly dirs."""
def handle_remove_readonly(func, path, exc):
excvalue = exc[1]
if (
func in (os.rmdir, os.remove, os.unlink) and
excvalue.errno == errno.EACCES
):
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)
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:
return _handle_readonly(func, path, excinfo[1])
def rmtree(path: str) -> None:
shutil.rmtree(path, ignore_errors=False, onerror=_handle_readonly_old)
else: # pragma: >=3.12 cover
def rmtree(path: str) -> None:
"""On windows, rmtree fails for readonly dirs."""
shutil.rmtree(path, ignore_errors=False, onexc=_handle_readonly)
def win_exe(s: str) -> str:
return s if sys.platform != 'win32' else f'{s}.exe'
def parse_version(s):
"""poor man's version comparison"""
return tuple(int(p) for p in s.split('.'))

View file

@ -1,44 +1,22 @@
from __future__ import annotations
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import concurrent.futures
import contextlib
import math
import multiprocessing
import os
import subprocess
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 TypeVar
import six
from pre_commit import parse_shebang
from pre_commit.util import cmd_output_b
from pre_commit.util import cmd_output_p
TArg = TypeVar('TArg')
TRet = TypeVar('TRet')
def cpu_count() -> 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:
def _environ_size(_env=None):
environ = _env if _env is not None else getattr(os, 'environb', os.environ)
size = 8 * len(environ) # number of pointers in `envp`
for k, v in environ.items():
@ -46,9 +24,9 @@ def _environ_size(_env: MutableMapping[str, str] | None = None) -> int:
return size
def _get_platform_max_length() -> int: # pragma: no cover (platform specific)
def _get_platform_max_length(): # pragma: no cover (platform specific)
if os.name == 'posix':
maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size()
maximum = os.sysconf(str('SC_ARG_MAX')) - 2048 - _environ_size()
maximum = max(min(maximum, 2 ** 17), 2 ** 12)
return maximum
elif os.name == 'nt':
@ -58,13 +36,17 @@ def _get_platform_max_length() -> int: # pragma: no cover (platform specific)
return 2 ** 12
def _command_length(*cmd: str) -> int:
def _command_length(*cmd):
full_cmd = ' '.join(cmd)
# win32 uses the amount of characters, more details at:
# https://github.com/pre-commit/pre-commit/pull/839
if sys.platform == 'win32':
return len(full_cmd.encode('utf-16le')) // 2
# the python2.x apis require bytes, we encode as UTF-8
if six.PY2:
return len(full_cmd.encode('utf-8'))
else:
return len(full_cmd.encode('utf-16le')) // 2
else:
return len(full_cmd.encode(sys.getfilesystemencoding()))
@ -73,12 +55,7 @@ class ArgumentTooLongError(RuntimeError):
pass
def partition(
cmd: Sequence[str],
varargs: Sequence[str],
target_concurrency: int,
_max_length: int | None = None,
) -> tuple[tuple[str, ...], ...]:
def partition(cmd, varargs, target_concurrency, _max_length=None):
_max_length = _max_length or _get_platform_max_length()
# Generally, we try to partition evenly into at least `target_concurrency`
@ -88,7 +65,7 @@ def partition(
cmd = tuple(cmd)
ret = []
ret_cmd: list[str] = []
ret_cmd = []
# Reversed so arguments are in order
varargs = list(reversed(varargs))
@ -118,9 +95,7 @@ def partition(
@contextlib.contextmanager
def _thread_mapper(maxsize: int) -> Generator[
Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]],
]:
def _thread_mapper(maxsize):
if maxsize == 1:
yield map
else:
@ -128,20 +103,16 @@ def _thread_mapper(maxsize: int) -> Generator[
yield ex.map
def xargs(
cmd: tuple[str, ...],
varargs: Sequence[str],
*,
color: bool = False,
target_concurrency: int = 1,
_max_length: int = _get_platform_max_length(),
**kwargs: Any,
) -> tuple[int, bytes]:
def xargs(cmd, varargs, **kwargs):
"""A simplified implementation of xargs.
color: Make a pty if on a platform that supports it
negate: Make nonzero successful and zero a failure
target_concurrency: Target number of partitions to run concurrently
"""
color = kwargs.pop('color', False)
negate = kwargs.pop('negate', False)
target_concurrency = kwargs.pop('target_concurrency', 1)
max_length = kwargs.pop('_max_length', _get_platform_max_length())
cmd_fn = cmd_output_p if color else cmd_output_b
retcode = 0
stdout = b''
@ -151,25 +122,11 @@ def xargs(
except parse_shebang.ExecutableNotFoundError as e:
return e.to_output()[:2]
# on windows, batch files have a separate length limit than windows itself
if (
sys.platform == 'win32' and
cmd[0].lower().endswith(('.bat', '.cmd'))
): # pragma: win32 cover
# this is implementation details but the command gets translated into
# full/path/to/cmd.exe /c *cmd
cmd_exe = parse_shebang.find_executable('cmd.exe')
# 1024 is additionally subtracted to give headroom for further
# expansion inside the batch file
_max_length = 8192 - len(cmd_exe) - len(' /c ') - 1024
partitions = partition(cmd, varargs, target_concurrency, max_length)
partitions = partition(cmd, varargs, target_concurrency, _max_length)
def run_cmd_partition(
run_cmd: tuple[str, ...],
) -> tuple[int, bytes, bytes | None]:
def run_cmd_partition(run_cmd):
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)
@ -177,8 +134,17 @@ def xargs(
results = thread_map(run_cmd_partition, partitions)
for proc_retcode, proc_out, _ in results:
if abs(proc_retcode) > abs(retcode):
retcode = proc_retcode
# This is *slightly* too clever so I'll explain it.
# First the xor boolean table:
# T | F |
# +-------+
# T | F | T |
# --+-------+
# F | T | F |
# --+-------+
# When negate is True, it has the effect of flipping the return
# code. Otherwise, the returncode is unchanged.
retcode |= bool(proc_retcode) ^ negate
stdout += proc_out
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))

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