From eebab08ecd6c247fa464f40934f852d63daba9bb Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 14 Jul 2022 15:16:23 +0200 Subject: Merging upstream version 2.20.0. Signed-off-by: Daniel Baumann --- .pre-commit-config.yaml | 8 ++-- CHANGELOG.md | 20 +++++++++ CONTRIBUTING.md | 2 +- pre_commit/commands/hook_impl.py | 18 +++++++- pre_commit/commands/run.py | 10 +++++ pre_commit/languages/helpers.py | 5 +-- pre_commit/languages/ruby.py | 1 + pre_commit/main.py | 97 +++++++++++++++++++--------------------- pre_commit/parse_shebang.py | 5 +-- pre_commit/util.py | 4 +- setup.cfg | 2 +- testing/fixtures.py | 2 +- testing/util.py | 4 ++ tests/commands/hook_impl_test.py | 36 +++++++++++++++ tests/commands/run_test.py | 7 ++- tests/conftest.py | 18 -------- tests/error_handler_test.py | 6 +++ tests/repository_test.py | 7 +++ 18 files changed, 165 insertions(+), 87 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7791f76..94a35a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.1.0 + rev: v3.3.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.34.0 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.950 + rev: v0.961 hooks: - id: mypy additional_dependencies: [types-all] diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6d8b6..03a7c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +2.20.0 - 2022-07-10 +=================== + +### Features +- Expose `source` and `object-name` (positional args) of `prepare-commit-msg` + hook as `PRE_COMMIT_COMIT_MSG_SOURCE` and `PRE_COMMIT_COMMIT_OBJECT_NAME`. + - #2407 PR by @M-Whitaker. + - #2406 issue by @M-Whitaker. + +### Fixes +- Fix `language: ruby` installs when `--user-install` is set in gemrc. + - #2394 PR by @narpfel. + - #2393 issue by @narpfel. +- Adjust pty setup for solaris. + - #2390 PR by @gaige. + - #2389 issue by @gaige. +- Remove unused `--config` option from `gc`, `sample-config`, + `validate-config`, `validate-manifest` sub-commands. + - #2429 PR by @asottile. + 2.19.0 - 2022-05-05 =================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa1678c..310c17e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ to implement. The current implemented languages are at varying levels: - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) -"third class" is usually the easiest to implement first and is perfectly +"second class" is usually the easiest to implement first and is perfectly acceptable. Ideally the language works on the supported platforms for pre-commit (linux, diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index f315c04..f5995e9 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -76,6 +76,8 @@ def _ns( 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, @@ -90,6 +92,8 @@ def _ns( 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, @@ -202,8 +206,20 @@ def _run_ns( _check_args_length(hook_type, args) if hook_type == 'pre-push': return _pre_push_ns(color, args, stdin) - elif hook_type in {'commit-msg', 'prepare-commit-msg'}: + 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': diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 37f989b..ad3d766 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -361,6 +361,16 @@ def run( ): return 0 + # Expose prepare_commit_message_source / commit_object_name + # as environment variables for the hooks + if args.prepare_commit_message_source: + environ['PRE_COMMIT_COMMIT_MSG_SOURCE'] = ( + args.prepare_commit_message_source + ) + + if args.commit_object_name: + environ['PRE_COMMIT_COMMIT_OBJECT_NAME'] = args.commit_object_name + # Expose from-ref / to-ref as environment variables for hooks to consume if args.from_ref and args.to_ref: # legacy names diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 05a7165..0be08b5 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,9 +5,9 @@ import os import random import re from typing import Any +from typing import NoReturn from typing import overload from typing import Sequence -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit import parse_shebang @@ -16,9 +16,6 @@ from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs -if TYPE_CHECKING: - from typing import NoReturn - FIXED_RANDOM_SEED = 1542676187 SHIMS_RE = re.compile(r'[/\\]shims[/\\]') diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 6c5cff2..8955dd0 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -138,6 +138,7 @@ def install_environment( ( 'gem', 'install', '--no-document', '--no-format-executable', + '--no-user-install', *prefix.star('.gem'), *additional_dependencies, ), ) diff --git a/pre_commit/main.py b/pre_commit/main.py index 6d2814b..b4fa966 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -107,6 +107,20 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--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)' + ), + ) + parser.add_argument( + '--commit-object-name', + help=( + 'Commit object name ' + '(typically the third argument to .git/hooks/prepare-commit-msg)' + ), + ) parser.add_argument( '--remote-name', help='Remote name used by `git push`.', ) @@ -167,11 +181,15 @@ def main(argv: Sequence[str] | None = None) -> int: subparsers = parser.add_subparsers(dest='command') - autoupdate_parser = subparsers.add_parser( + 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', 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( '--bleeding-edge', action='store_true', @@ -189,34 +207,17 @@ def main(argv: Sequence[str] | None = None) -> int: help='Only update this repository -- may be specified multiple times.', ) - clean_parser = subparsers.add_parser( - 'clean', help='Clean out pre-commit files.', - ) - add_color_option(clean_parser) - _add_config_option(clean_parser) - - 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) + _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.') - init_templatedir_parser = subparsers.add_parser( + init_templatedir_parser = _add_cmd( '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.', @@ -229,10 +230,7 @@ def main(argv: Sequence[str] | None = None) -> int: ) _add_hook_type_option(init_templatedir_parser) - install_parser = subparsers.add_parser( - 'install', help='Install the pre-commit script.', - ) - add_color_option(install_parser) + install_parser = _add_cmd('install', help='Install the pre-commit script.') _add_config_option(install_parser) install_parser.add_argument( '-f', '--overwrite', action='store_true', @@ -254,7 +252,7 @@ def main(argv: Sequence[str] | None = None) -> int: ), ) - install_hooks_parser = subparsers.add_parser( + install_hooks_parser = _add_cmd( 'install-hooks', help=( 'Install hook environments for all environments in the config ' @@ -262,32 +260,24 @@ def main(argv: Sequence[str] | None = None) -> int: 'useful.' ), ) - add_color_option(install_hooks_parser) _add_config_option(install_hooks_parser) - migrate_config_parser = subparsers.add_parser( + migrate_config_parser = _add_cmd( 'migrate-config', help='Migrate list configuration to new map configuration.', ) - add_color_option(migrate_config_parser) _add_config_option(migrate_config_parser) - run_parser = subparsers.add_parser('run', help='Run hooks.') - add_color_option(run_parser) + run_parser = _add_cmd('run', help='Run hooks.') _add_config_option(run_parser) _add_run_options(run_parser) - sample_config_parser = subparsers.add_parser( - 'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file', - ) - add_color_option(sample_config_parser) - _add_config_option(sample_config_parser) + _add_cmd('sample-config', help=f'Produce a sample {C.CONFIG_FILE} file') - try_repo_parser = subparsers.add_parser( + try_repo_parser = _add_cmd( '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.', @@ -301,32 +291,39 @@ def main(argv: Sequence[str] | None = None) -> int: ) _add_run_options(try_repo_parser) - uninstall_parser = subparsers.add_parser( + uninstall_parser = _add_cmd( '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 = subparsers.add_parser( + validate_config_parser = _add_cmd( 'validate-config', help='Validate .pre-commit-config.yaml files', ) - add_color_option(validate_config_parser) - _add_config_option(validate_config_parser) validate_config_parser.add_argument('filenames', nargs='*') - validate_manifest_parser = subparsers.add_parser( + validate_manifest_parser = _add_cmd( 'validate-manifest', help='Validate .pre-commit-hooks.yaml files', ) - add_color_option(validate_manifest_parser) - _add_config_option(validate_manifest_parser) 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'] @@ -340,11 +337,11 @@ def main(argv: Sequence[str] | None = None) -> int: with error_handler(), logging_handler(args.color): git.check_for_cygwin_mismatch() + store = Store() + if args.command not in COMMANDS_NO_GIT: _adjust_args_and_chdir(args) - - store = Store() - store.mark_config_used(args.config) + store.mark_config_used(args.config) if args.command == 'autoupdate': return autoupdate( diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3fd3129..3ac933c 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -2,13 +2,10 @@ from __future__ import annotations import os.path from typing import Mapping -from typing import TYPE_CHECKING +from typing import NoReturn from identify.identify import parse_shebang_from_file -if TYPE_CHECKING: - from typing import NoReturn - class ExecutableNotFoundError(OSError): def to_output(self) -> tuple[int, bytes, None]: diff --git a/pre_commit/util.py b/pre_commit/util.py index 40c53e5..8c296f4 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -168,10 +168,10 @@ if os.name != 'nt': # pragma: win32 no cover self.r, self.w = openpty() # tty flags normally change \n to \r\n - attrs = termios.tcgetattr(self.r) + attrs = termios.tcgetattr(self.w) assert isinstance(attrs[1], int) attrs[1] &= ~(termios.ONLCR | termios.OPOST) - termios.tcsetattr(self.r, termios.TCSANOW, attrs) + termios.tcsetattr(self.w, termios.TCSANOW, attrs) return self diff --git a/setup.cfg b/setup.cfg index 93a485c..ae214f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.19.0 +version = 2.20.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/fixtures.py b/testing/fixtures.py index ef5a041..5182a08 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -38,7 +38,7 @@ def copy_tree_to_path(src_dir, dest_dir): def git_dir(tempdir_factory): path = tempdir_factory.get() - cmd_output('git', 'init', path) + cmd_output('git', '-c', 'init.defaultBranch=master', 'init', path) return path diff --git a/testing/util.py b/testing/util.py index 0dd1784..e807f04 100644 --- a/testing/util.py +++ b/testing/util.py @@ -76,6 +76,8 @@ def run_opts( hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', + prepare_commit_message_source='', + commit_object_name='', checkout_type='', is_squash_merge='', rewrite_command='', @@ -97,6 +99,8 @@ def run_opts( hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, + prepare_commit_message_source=prepare_commit_message_source, + commit_object_name=commit_object_name, checkout_type=checkout_type, is_squash_merge=is_squash_merge, rewrite_command=rewrite_command, diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 3e20874..aa321da 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -154,6 +154,42 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_prepare_commit_msg_one_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG',), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + + +def test_run_ns_prepare_commit_msg_two_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG', 'message'), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + assert ns.prepare_commit_message_source == 'message' + + +def test_run_ns_prepare_commit_msg_three_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG', 'message', 'HEAD'), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + assert ns.prepare_commit_message_source == 'message' + assert ns.commit_object_name == 'HEAD' + + def test_run_ns_post_commit(): ns = hook_impl._run_ns('post-commit', True, (), b'') assert ns is not None diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 085b063..2634c0c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -810,7 +810,12 @@ def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): cap_out, store, prepare_commit_msg_repo, - {'hook_stage': 'prepare-commit-msg', 'commit_msg_filename': filename}, + { + 'hook_stage': 'prepare-commit-msg', + 'commit_msg_filename': filename, + 'prepare_commit_message_source': 'commit', + 'commit_object_name': 'HEAD', + }, expected_outputs=[b'Add "Signed off by:"', b'Passed'], expected_ret=0, stage=False, diff --git a/tests/conftest.py b/tests/conftest.py index b68a1d0..40c0c05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,24 +21,6 @@ from testing.util import cwd from testing.util import git_commit -@pytest.fixture(autouse=True) -def no_warnings(recwarn): - yield - warnings = [] - for warning in recwarn: # pragma: no cover - message = str(warning.message) - # ImportWarning: Not importing directory '...' missing __init__(.py) - if not ( - isinstance(warning.message, ImportWarning) and - message.startswith('Not importing directory ') and - ' missing __init__' in message - ): - warnings.append( - f'{warning.filename}:{warning.lineno} {message}', - ) - assert not warnings - - @pytest.fixture def tempdir_factory(tmpdir): class TmpdirFactory: diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 31c71d2..47e2afa 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -45,9 +45,11 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_fatal_error\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'(pre_commit\.errors\.)?FatalError: just a test\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) @@ -69,9 +71,11 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_uncaught_error\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'ValueError: another test\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) @@ -93,9 +97,11 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_keyboardinterrupt\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'KeyboardInterrupt\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) diff --git a/tests/repository_test.py b/tests/repository_test.py index 3729ab1..11d452c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -332,6 +332,13 @@ def test_run_a_ruby_hook(tempdir_factory, store): ) +def test_run_a_ruby_hook_with_user_install_set(tempdir_factory, store, tmpdir): + gemrc = tmpdir.join('gemrc') + gemrc.write('gem: --user-install\n') + with envcontext((('GEMRC', str(gemrc)),)): + test_run_a_ruby_hook(tempdir_factory, store) + + @xfailif_windows # pragma: win32 no cover def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( -- cgit v1.2.3