diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..9408e44d6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: asottile +open_collective: pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80fa14bbe..72ce7bf42 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,16 +21,16 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.7.1 + rev: v2.9.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.7.3 + rev: v2.7.4 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.5 + rev: v2.3.6 hooks: - id: reorder-python-imports args: [--py3-plus] diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1013f82..4c7032b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +2.9.0 - 2020-11-21 +================== + +### Features +- Add `types_or` which allows matching multiple disparate `types` in a hook + - #1677 by @MarcoGorelli. + - #607 by @asottile. +- Add Github Sponsors / Open Collective links + - https://github.com/sponsors/asottile + - https://opencollective.com/pre-commit + +### Fixes +- Improve cleanup for `language: dotnet` + - #1678 by @rkm. +- Fix "xargs" when running windows batch files + - #1686 PR by @asottile. + - #1604 issue by @apietrzak. + - #1604 issue by @ufwtlsb. +- Fix conflict with external `rbenv` and `language_version: default` + - #1700 PR by @asottile. + - #1699 issue by @abuxton. +- Improve performance of `git status` / `git diff` commands by ignoring + submodules + - #1704 PR by @Vynce. + - #1701 issue by @Vynce. + 2.8.2 - 2020-10-30 ================== diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 87679bfa6..0b8582bce 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -61,6 +61,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), + cfgv.Optional('types_or', cfgv.check_array(check_type_tag), ['file']), cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), cfgv.Optional( diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 87f6d53d2..7320bb426 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -84,9 +84,7 @@ def _check_hooks_still_exist_at_rev( ) -REV_LINE_RE = re.compile( - r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$', re.DOTALL, -) +REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') def _original_lines( diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d580ff178..a155f6b05 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,4 +1,5 @@ import re +import textwrap import yaml @@ -6,27 +7,22 @@ from pre_commit.util import yaml_load -def _indent(s: str) -> str: - lines = s.splitlines(True) - return ''.join(' ' * 4 + line if line.strip() else line for line in lines) - - def _is_header_line(line: str) -> bool: return line.startswith(('#', '---')) or not line.strip() def _migrate_map(contents: str) -> str: - # 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 + 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 - header = ''.join(lines[:i]) - rest = ''.join(lines[i:]) + header = ''.join(lines[:i]) + rest = ''.join(lines[i:]) - if isinstance(yaml_load(contents), list): # If they are using the "default" flow style of yaml, this operation # will yield a valid configuration try: @@ -34,7 +30,7 @@ def _migrate_map(contents: str) -> str: yaml_load(trial_contents) contents = trial_contents except yaml.YAMLError: - contents = f'{header}repos:\n{_indent(rest)}' + contents = f'{header}repos:\n{textwrap.indent(rest, " " * 4)}' return contents diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0d335e285..508b61a34 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -83,20 +83,28 @@ def by_types( self, names: Sequence[str], types: Collection[str], + types_or: Collection[str], exclude_types: Collection[str], ) -> List[str]: - types, exclude_types = frozenset(types), frozenset(exclude_types) + types = frozenset(types) + types_or = frozenset(types_or) + exclude_types = frozenset(exclude_types) ret = [] for filename in names: tags = self._types_for_file(filename) - if tags >= types and not tags & exclude_types: + if tags >= types and tags & types_or and not tags & exclude_types: ret.append(filename) return ret def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: names = self.filenames names = filter_by_include_exclude(names, hook.files, hook.exclude) - names = self.by_types(names, hook.types, hook.exclude_types) + names = self.by_types( + names, + hook.types, + hook.types_or, + hook.exclude_types, + ) return tuple(names) @classmethod @@ -250,7 +258,9 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _get_diff() -> bytes: - _, out, _ = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) + _, out, _ = cmd_output_b( + 'git', 'diff', '--no-ext-diff', '--ignore-submodules', retcode=None, + ) return out diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 4ab0d8cb9..92d975d09 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -8,11 +8,7 @@ from typing import Tuple from typing import Union - -class _Unset(enum.Enum): - UNSET = 1 - - +_Unset = enum.Enum('_Unset', 'UNSET') UNSET = _Unset.UNSET diff --git a/pre_commit/git.py b/pre_commit/git.py index 13ba664c8..8e22dcf0f 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -130,7 +130,9 @@ def get_staged_files(cwd: Optional[str] = None) -> List[str]: def intent_to_add_files() -> List[str]: - _, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z') + _, stdout, _ = cmd_output( + 'git', 'status', '--ignore-submodules', '--porcelain', '-z', + ) parts = list(reversed(zsplit(stdout))) intent_to_add = [] while parts: diff --git a/pre_commit/hook.py b/pre_commit/hook.py index b65ac42b0..ea773942b 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -22,6 +22,7 @@ class Hook(NamedTuple): files: str exclude: str types: Sequence[str] + types_or: Sequence[str] exclude_types: Sequence[str] additional_dependencies: Sequence[str] args: Sequence[str] diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index a8abc8611..094d2f1ce 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -12,7 +12,6 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure -from pre_commit.util import rmtree ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' @@ -76,9 +75,9 @@ def install_environment( ), ) - # Cleanup build output - for d in ('bin', 'obj', build_dir): - rmtree(prefix.path(d)) + # Clean the git dir, ignoring the environment dir + clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') + helpers.run_setup_cmd(prefix, clean_cmd) def run_hook( diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 1a0f0c7e3..81bc95436 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -52,7 +52,6 @@ def get_env_patch( else: # pragma: win32 no cover patches += ( ('RBENV_ROOT', venv), - ('RBENV_VERSION', language_version), ( 'PATH', ( os.path.join(venv, 'gems', 'bin'), os.pathsep, @@ -61,6 +60,9 @@ def get_env_patch( ), ), ) + if language_version not in {'system', 'default'}: # pragma: win32 no cover + patches += (('RBENV_VERSION', language_version),) + return patches diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index db6865c6c..12be03f8a 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -47,8 +47,10 @@ def check_useless_excludes(config_file: str) -> int: # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) names = classifier.filenames - types, exclude_types = hook['types'], hook['exclude_types'] - names = classifier.by_types(names, types, exclude_types) + types = hook['types'] + types_or = hook['types_or'] + exclude_types = hook['exclude_types'] + names = classifier.by_types(names, types, types_or, exclude_types) include, exclude = hook['files'], hook['exclude'] if not exclude_matches_any(names, include, exclude): print( diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 7538b54f6..60a057c19 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -137,6 +137,16 @@ 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') + _max_length = 8192 - len(cmd_exe) - len(' /c ') + partitions = partition(cmd, varargs, target_concurrency, _max_length) def run_cmd_partition( diff --git a/setup.cfg b/setup.cfg index 32160b9ea..9b15fe1e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.8.2 +version = 2.9.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown diff --git a/testing/resources/exclude_types_repo/bin/hook.sh b/testing/resources/exclude_types_repo/bin/hook.sh index bdade5132..a828db4d2 100755 --- a/testing/resources/exclude_types_repo/bin/hook.sh +++ b/testing/resources/exclude_types_repo/bin/hook.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -echo $@ +echo "$@" exit 1 diff --git a/testing/resources/failing_hook_repo/bin/hook.sh b/testing/resources/failing_hook_repo/bin/hook.sh index 229ccaf41..7dcffebe8 100755 --- a/testing/resources/failing_hook_repo/bin/hook.sh +++ b/testing/resources/failing_hook_repo/bin/hook.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash echo 'Fail' -echo $@ +echo "$@" exit 1 diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh index 5af177a83..a9f1dcd91 100755 --- a/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh +++ b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -echo $@ +echo "$@" diff --git a/testing/resources/script_hooks_repo/bin/hook.sh b/testing/resources/script_hooks_repo/bin/hook.sh index 6565ee40a..cbc4b3544 100755 --- a/testing/resources/script_hooks_repo/bin/hook.sh +++ b/testing/resources/script_hooks_repo/bin/hook.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -echo $@ +echo "$@" echo 'Hello World' diff --git a/testing/resources/types_or_repo/.pre-commit-hooks.yaml b/testing/resources/types_or_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..a4ea920d6 --- /dev/null +++ b/testing/resources/types_or_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: python-cython-files + name: Python and Cython files + entry: bin/hook.sh + language: script + types: [file] + types_or: [python, cython] diff --git a/testing/resources/types_or_repo/bin/hook.sh b/testing/resources/types_or_repo/bin/hook.sh new file mode 100755 index 000000000..a828db4d2 --- /dev/null +++ b/testing/resources/types_or_repo/bin/hook.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo "$@" +exit 1 diff --git a/testing/resources/types_repo/bin/hook.sh b/testing/resources/types_repo/bin/hook.sh index bdade5132..a828db4d2 100755 --- a/testing/resources/types_repo/bin/hook.sh +++ b/testing/resources/types_repo/bin/hook.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -echo $@ +echo "$@" exit 1 diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index bd89c1dba..b2bad6014 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,9 +1,12 @@ import shlex +from unittest import mock import pytest +import yaml import pre_commit.constants as C from pre_commit import git +from pre_commit import util from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError @@ -173,6 +176,11 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) +def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): + with mock.patch.object(util, 'Dumper', yaml.SafeDumper): + test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + + def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): fmt = ( 'repos:\n' diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 6a049d5f6..f5c89d044 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -2,24 +2,9 @@ import pre_commit.constants as C from pre_commit.clientlib import InvalidConfigError -from pre_commit.commands.migrate_config import _indent from pre_commit.commands.migrate_config import migrate_config -@pytest.mark.parametrize( - ('s', 'expected'), - ( - ('', ''), - ('a', ' a'), - ('foo\nbar', ' foo\n bar'), - ('foo\n\nbar\n', ' foo\n\n bar\n'), - ('\n\n\n', '\n\n\n'), - ), -) -def test_indent(s, expected): - assert _indent(s) == expected - - def test_migrate_config_normal_format(tmpdir, capsys): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 00b471282..34f3a3753 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -219,6 +219,19 @@ def test_types_hook_repository(cap_out, store, tempdir_factory): assert b'bar.notpy' not in printed +def test_types_or_hook_repository(cap_out, store, tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'types_or_repo') + with cwd(git_path): + stage_a_file('bar.notpy') + stage_a_file('bar.pxd') + stage_a_file('bar.py') + ret, printed = _do_run(cap_out, store, git_path, run_opts()) + assert ret == 1 + assert b'bar.notpy' not in printed + assert b'bar.pxd' in printed + assert b'bar.py' in printed + + def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') with cwd(git_path): diff --git a/tests/repository_test.py b/tests/repository_test.py index 3d5093df6..8d771458f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -901,6 +901,7 @@ def test_manifest_hooks(tempdir_factory, store): 'post-commit', 'manual', 'post-checkout', 'push', ), types=['file'], + types_or=['file'], verbose=False, ) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 4f6136ede..7e83ef590 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -195,3 +195,12 @@ def test_xargs_color_true_makes_tty(): ) assert retcode == 0 assert out == b'True\n' + + +@pytest.mark.xfail(os.name == 'posix', reason='nt only') +@pytest.mark.parametrize('filename', ('t.bat', 't.cmd', 'T.CMD')) +def test_xargs_with_batch_files(tmpdir, filename): + f = tmpdir.join(filename) + f.write('echo it works\n') + retcode, out = xargs.xargs((str(f),), ('x',) * 8192) + assert retcode == 0, (retcode, out)