diff options
Diffstat (limited to 'pre_commit')
-rw-r--r-- | pre_commit/clientlib.py | 15 | ||||
-rw-r--r-- | pre_commit/commands/autoupdate.py | 8 | ||||
-rw-r--r-- | pre_commit/commands/migrate_config.py | 26 | ||||
-rw-r--r-- | pre_commit/commands/run.py | 22 | ||||
-rw-r--r-- | pre_commit/envcontext.py | 6 | ||||
-rw-r--r-- | pre_commit/git.py | 32 | ||||
-rw-r--r-- | pre_commit/hook.py | 1 | ||||
-rw-r--r-- | pre_commit/languages/dotnet.py | 7 | ||||
-rw-r--r-- | pre_commit/languages/python.py | 2 | ||||
-rw-r--r-- | pre_commit/languages/ruby.py | 4 | ||||
-rw-r--r-- | pre_commit/meta_hooks/check_useless_excludes.py | 6 | ||||
-rw-r--r-- | pre_commit/util.py | 2 | ||||
-rw-r--r-- | pre_commit/xargs.py | 10 |
13 files changed, 92 insertions, 49 deletions
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 87679bf..20d4492 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -61,6 +61,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( 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), []), cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), cfgv.Optional( @@ -111,6 +112,18 @@ LOCAL = 'local' META = 'meta' +class OptionalSensibleRegex(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', + ) + + class MigrateShaToRev: key = 'rev' @@ -226,6 +239,8 @@ CONFIG_HOOK_DICT = cfgv.Map( for item in MANIFEST_HOOK_DICT.items if item.key != 'id' ), + OptionalSensibleRegex('files', cfgv.check_string), + OptionalSensibleRegex('exclude', cfgv.check_string), ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 87f6d53..33a3473 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -79,14 +79,12 @@ def _check_hooks_still_exist_at_rev( hooks_missing = hooks - {hook['id'] for hook in manifest} if hooks_missing: raise RepositoryCannotBeUpdatedError( - f'Cannot update because the tip of HEAD is missing these hooks:\n' - f'{", ".join(sorted(hooks_missing))}', + f'Cannot update because the update target is missing these ' + f'hooks:\n{", ".join(sorted(hooks_missing))}', ) -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 d580ff1..a155f6b 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.clientlib import load_config 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 0d335e2..1e8fad2 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -83,20 +83,32 @@ class Classifier: 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 + (not types_or or 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 +262,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 4ab0d8c..92d975d 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -8,11 +8,7 @@ from typing import Optional 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 13ba664..5096274 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -47,21 +47,26 @@ def no_git_env( 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 = cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() + root = os.path.realpath( + cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), + ) + git_dir = os.path.realpath(get_git_dir()) except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) - else: - if root == '': # pragma: no cover (old git) - raise FatalError( - 'git toplevel unexpectedly empty! make sure you are not ' - 'inside the `.git` directory of your repository.', - ) - else: - return root + if os.path.commonpath((root, git_dir)) == git_dir: + raise FatalError( + 'git toplevel unexpectedly empty! make sure you are not ' + 'inside the `.git` directory of your repository.', + ) + return root def get_git_dir(git_root: str = '.') -> str: @@ -130,7 +135,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: @@ -199,7 +206,10 @@ def check_for_cygwin_mismatch() -> None: """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' - toplevel = get_root() + try: + toplevel = get_root() + except FatalError: # skip the check if we're not in a git repo + return is_cygwin_git = toplevel.startswith('/') if is_cygwin_python ^ is_cygwin_git: diff --git a/pre_commit/hook.py b/pre_commit/hook.py index b65ac42..ea77394 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 a8abc86..094d2f1 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -12,7 +12,6 @@ from pre_commit.hook import Hook 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/python.py b/pre_commit/languages/python.py index 65f521c..43b7280 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -36,7 +36,7 @@ def _version_info(exe: str) -> str: def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: ret = {} - with open(filename) as f: + with open(filename, encoding='UTF-8') as f: for line in f: try: k, v = line.split('=') diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 1a0f0c7..81bc954 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 db6865c..12be03f 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/util.py b/pre_commit/util.py index f4cf704..b5f40ad 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -255,7 +255,7 @@ def rmtree(path: str) -> None: excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and - excvalue.errno == errno.EACCES + excvalue.errno in {errno.EACCES, errno.EPERM} ): for p in (path, os.path.dirname(path)): os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 7538b54..60a057c 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( |