Merge branch 'master' into interrupt_exit_code

This commit is contained in:
int3l 2020-09-16 23:39:13 +03:00 committed by GitHub
commit 4ef3f957e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 605 additions and 216 deletions

View file

@ -12,8 +12,10 @@ import cfgv
from identify.identify import ALL_TAGS
import pre_commit.constants as C
from pre_commit.color import add_color_option
from pre_commit.error_handler import FatalError
from pre_commit.languages.all import all_languages
from pre_commit.logging_handler import logging_handler
from pre_commit.util import parse_version
from pre_commit.util import yaml_load
@ -43,6 +45,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help=filenames_help)
parser.add_argument('-V', '--version', action='version', version=C.VERSION)
add_color_option(parser)
return parser
@ -92,14 +95,16 @@ load_manifest = functools.partial(
def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int:
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
with logging_handler(args.color):
ret = 0
for filename in args.filenames:
try:
load_manifest(filename)
except InvalidManifestError as e:
print(e)
ret = 1
return ret
LOCAL = 'local'
@ -290,7 +295,11 @@ class InvalidConfigError(FatalError):
def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]:
data = yaml_load(contents)
if isinstance(data, list):
# TODO: Once happy, issue a deprecation warning and instructions
logger.warning(
'normalizing pre-commit configuration to a top-level map. '
'support for top level list will be removed in a future version. '
'run: `pre-commit migrate-config` to automatically fix this.',
)
return {'repos': data}
else:
return data
@ -307,11 +316,13 @@ load_config = functools.partial(
def validate_config_main(argv: Optional[Sequence[str]] = None) -> int:
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
with logging_handler(args.color):
ret = 0
for filename in args.filenames:
try:
load_config(filename)
except InvalidConfigError as e:
print(e)
ret = 1
return ret

View file

@ -1,3 +1,4 @@
import argparse
import os
import sys
@ -95,3 +96,12 @@ def use_color(setting: str) -> bool:
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

@ -15,10 +15,15 @@ def init_templatedir(
store: Store,
directory: str,
hook_types: Sequence[str],
skip_on_missing_config: bool = True,
) -> int:
install(
config_file, store, hook_types=hook_types,
overwrite=True, skip_on_missing_config=True, git_dir=directory,
config_file,
store,
hook_types=hook_types,
overwrite=True,
skip_on_missing_config=skip_on_missing_config,
git_dir=directory,
)
try:
_, out, _ = cmd_output('git', 'config', 'init.templateDir')

View file

@ -55,7 +55,7 @@ def is_our_script(filename: str) -> bool:
def shebang() -> str:
if sys.platform == 'win32':
py = SYS_EXE
py, _ = os.path.splitext(SYS_EXE)
else:
exe_choices = [
f'python{sys.version_info[0]}.{sys.version_info[1]}',
@ -165,7 +165,7 @@ def _uninstall_hook_script(hook_type: str) -> None:
output.write_line(f'{hook_type} uninstalled')
if os.path.exists(legacy_path):
os.rename(legacy_path, hook_path)
os.replace(legacy_path, hook_path)
output.write_line(f'Restored previous hooks to {hook_path}')

View file

@ -72,13 +72,7 @@ def filter_by_include_exclude(
class Classifier:
def __init__(self, filenames: Sequence[str]) -> None:
# 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]
def __init__(self, filenames: Collection[str]) -> None:
self.filenames = [f for f in filenames if os.path.lexists(f)]
@functools.lru_cache(maxsize=None)
@ -105,6 +99,22 @@ class Classifier:
names = self.by_types(names, hook.types, hook.exclude_types)
return tuple(names)
@classmethod
def from_config(
cls,
filenames: Collection[str],
include: str,
exclude: str,
) -> 'Classifier':
# 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)
def _get_skips(environ: EnvironT) -> Set[str]:
skips = environ.get('SKIP', '')
@ -124,9 +134,10 @@ def _run_single_hook(
hook: Hook,
skips: Set[str],
cols: int,
diff_before: bytes,
verbose: bool,
use_color: bool,
) -> bool:
) -> Tuple[bool, bytes]:
filenames = classifier.filenames_for_hook(hook)
if hook.id in skips or hook.alias in skips:
@ -141,6 +152,7 @@ def _run_single_hook(
)
duration = None
retcode = 0
diff_after = diff_before
files_modified = False
out = b''
elif not filenames and not hook.always_run:
@ -156,21 +168,20 @@ def _run_single_hook(
)
duration = None
retcode = 0
diff_after = diff_before
files_modified = False
out = b''
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))
diff_cmd = ('git', 'diff', '--no-ext-diff')
diff_before = cmd_output_b(*diff_cmd, retcode=None)
if not hook.pass_filenames:
filenames = ()
time_before = time.time()
language = languages[hook.language]
retcode, out = language.run_hook(hook, filenames, use_color)
duration = round(time.time() - time_before, 2) or 0
diff_after = cmd_output_b(*diff_cmd, retcode=None)
diff_after = _get_diff()
# if the hook makes changes, fail the commit
files_modified = diff_before != diff_after
@ -202,7 +213,7 @@ def _run_single_hook(
output.write_line_b(out.strip(), logfile_name=hook.log_file)
output.write_line()
return files_modified or bool(retcode)
return files_modified or bool(retcode), diff_after
def _compute_cols(hooks: Sequence[Hook]) -> int:
@ -238,6 +249,11 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]:
return git.get_staged_files()
def _get_diff() -> bytes:
_, out, _ = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None)
return out
def _run_hooks(
config: Dict[str, Any],
hooks: Sequence[Hook],
@ -247,19 +263,20 @@ def _run_hooks(
"""Actually run the hooks."""
skips = _get_skips(environ)
cols = _compute_cols(hooks)
filenames = filter_by_include_exclude(
classifier = Classifier.from_config(
_all_filenames(args), config['files'], config['exclude'],
)
classifier = Classifier(filenames)
retval = 0
prior_diff = _get_diff()
for hook in hooks:
retval |= _run_single_hook(
classifier, hook, skips, cols,
current_retval, prior_diff = _run_single_hook(
classifier, hook, skips, cols, prior_diff,
verbose=args.verbose, use_color=args.color,
)
retval |= current_retval
if retval and config['fail_fast']:
break
if retval and args.show_diff_on_failure and git.has_diff():
if retval and args.show_diff_on_failure and prior_diff:
if args.all_files:
output.write_line(
'pre-commit hook(s) made changes.\n'

View file

@ -23,10 +23,17 @@ def _log_and_exit(
) -> None:
error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc)
output.write_line_b(error_msg)
log_path = os.path.join(Store().directory, 'pre-commit.log')
output.write_line(f'Check the log at {log_path}')
with open(log_path, 'wb') as log:
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 = 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
_log_line = functools.partial(output.write_line, stream=log)
_log_line_b = functools.partial(output.write_line_b, stream=log)
@ -50,7 +57,7 @@ def _log_and_exit(
_log_line('```')
_log_line()
_log_line('```')
_log_line(formatted)
_log_line(formatted.rstrip())
_log_line('```')
raise SystemExit(code)

View file

@ -21,13 +21,13 @@ if os.name == 'nt': # pragma: no cover (windows)
) -> Generator[None, None, None]:
try:
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
except OSError:
blocked_cb()
while True:
try:
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) # type: ignore # noqa: E501
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region)
except OSError as e:
# Locking violation. Returned when the _LK_LOCK or _LK_RLCK
# flag is specified and the file cannot be locked after 10
@ -46,7 +46,7 @@ if os.name == 'nt': # pragma: no cover (windows)
# "Regions should be locked only briefly and should be unlocked
# before closing a file or exiting the program."
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region)
else: # pragma: win32 no cover
import fcntl

View file

@ -158,7 +158,8 @@ def init_repo(path: str, remote: str) -> None:
remote = os.path.abspath(remote)
env = no_git_env()
cmd_output_b('git', 'init', path, env=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)

View file

@ -7,9 +7,7 @@ import pre_commit.constants as C
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix
from pre_commit.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'
@ -26,21 +24,6 @@ def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover
return f'pre-commit-{md5sum}'
def docker_is_running() -> bool: # pragma: win32 no cover
try:
cmd_output_b('docker', 'ps')
except CalledProcessError:
return False
else:
return True
def assert_docker_available() -> None: # pragma: win32 no cover
assert docker_is_running(), (
'Docker is either not running or not configured in this environment'
)
def build_docker_image(
prefix: Prefix,
*,
@ -63,7 +46,6 @@ def install_environment(
) -> None: # pragma: win32 no cover
helpers.assert_version_default('docker', version)
helpers.assert_no_additional_deps('docker', additional_dependencies)
assert_docker_available()
directory = prefix.path(
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
@ -101,7 +83,6 @@ def run_hook(
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]: # pragma: win32 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(hook.prefix, pull=False)

View file

@ -3,7 +3,6 @@ from typing import Tuple
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.languages.docker import assert_docker_available
from pre_commit.languages.docker import docker_cmd
ENVIRONMENT_DIR = None
@ -17,6 +16,5 @@ def run_hook(
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]: # pragma: win32 no cover
assert_docker_available()
cmd = docker_cmd() + hook.cmd
return helpers.run_xargs(hook, cmd, file_args, color=color)

View file

@ -10,6 +10,7 @@ import pre_commit.constants as C
from pre_commit import parse_shebang
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.hook import Hook
from pre_commit.languages import helpers
@ -20,7 +21,6 @@ from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'node_env'
healthy = helpers.basic_healthy
@functools.lru_cache(maxsize=1)
@ -56,6 +56,8 @@ def get_env_patch(venv: str) -> PatchesT:
('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'))),
)
@ -70,6 +72,12 @@ def in_env(
yield
def healthy(prefix: Prefix, language_version: str) -> bool:
with in_env(prefix, language_version):
retcode, _, _ = cmd_output_b('node', '--version', retcode=None)
return retcode == 0
def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None:

View file

@ -114,11 +114,6 @@ def get_default_version() -> str: # pragma: no cover (platform dependent)
if _find_by_py_launcher(exe):
return exe
# Give a best-effort try for windows
default_folder_name = exe.replace('.', '')
if os.path.exists(fr'C:\{default_folder_name}\python.exe'):
return exe
# We tried!
return C.DEFAULT
@ -155,12 +150,6 @@ def norm_version(version: str) -> str:
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'):
default_folder_name = version.replace('.', '')
return fr'C:\{default_folder_name}\python.exe'
# Otherwise assume it is a path
return os.path.expanduser(version)
@ -191,7 +180,8 @@ def healthy(prefix: Prefix, language_version: str) -> bool:
return (
'version_info' in cfg and
_version_info(py_exe) == cfg['version_info'] and (
# always use uncached lookup here in case we replaced an unhealthy env
_version_info.__wrapped__(py_exe) == cfg['version_info'] and (
'base-executable' not in cfg or
_version_info(cfg['base-executable']) == cfg['version_info']
)

View file

@ -1,4 +1,5 @@
import contextlib
import functools
import os.path
import shutil
import tarfile
@ -7,6 +8,7 @@ from typing import Sequence
from typing import Tuple
import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET
@ -19,33 +21,51 @@ from pre_commit.util import clean_path_on_failure
from pre_commit.util import resource_bytesio
ENVIRONMENT_DIR = 'rbenv'
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy
@functools.lru_cache(maxsize=1)
def get_default_version() -> str:
if all(parse_shebang.find_executable(exe) for exe in ('ruby', 'gem')):
return 'system'
else:
return C.DEFAULT
def get_env_patch(
venv: str,
language_version: str,
) -> PatchesT: # pragma: win32 no cover
) -> PatchesT:
patches: PatchesT = (
('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 != C.DEFAULT:
patches += (('RBENV_VERSION', language_version),)
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),
('RBENV_VERSION', language_version),
(
'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'),
),
),
)
return patches
@contextlib.contextmanager # pragma: win32 no cover
@contextlib.contextmanager
def in_env(
prefix: Prefix,
language_version: str,
@ -65,7 +85,7 @@ def _extract_resource(filename: str, dest: str) -> None:
def _install_rbenv(
prefix: Prefix,
version: str = C.DEFAULT,
version: str,
) -> None: # pragma: win32 no cover
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
@ -92,21 +112,22 @@ def _install_ruby(
def install_environment(
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None: # pragma: win32 no cover
) -> None:
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
helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-'))
if version != C.DEFAULT:
if version != 'system': # pragma: win32 no cover
_install_rbenv(prefix, version)
with in_env(prefix, version):
# Need to call this before installing so rbenv's directories
# are set up
helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-'))
# XXX: this will *always* fail if `version == C.DEFAULT`
_install_ruby(prefix, version)
# Need to call this after installing to set up the shims
helpers.run_setup_cmd(prefix, ('rbenv', 'rehash'))
# Need to call this after installing to set up the shims
helpers.run_setup_cmd(prefix, ('rbenv', 'rehash'))
with in_env(prefix, version):
helpers.run_setup_cmd(
prefix, ('gem', 'build', *prefix.star('.gemspec')),
)
@ -123,6 +144,6 @@ def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]: # pragma: win32 no cover
) -> Tuple[int, bytes]:
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -8,8 +8,8 @@ from typing import Sequence
from typing import Union
import pre_commit.constants as C
from pre_commit import color
from pre_commit import git
from pre_commit.color import add_color_option
from pre_commit.commands.autoupdate import autoupdate
from pre_commit.commands.clean import clean
from pre_commit.commands.gc import gc
@ -41,15 +41,6 @@ os.environ.pop('__PYVENV_LAUNCHER__', None)
COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'}
def _add_color_option(parser: argparse.ArgumentParser) -> None:
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: argparse.ArgumentParser) -> None:
parser.add_argument(
'-c', '--config', default=C.CONFIG_FILE,
@ -195,7 +186,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
'autoupdate',
help="Auto-update pre-commit config to the latest repos' versions.",
)
_add_color_option(autoupdate_parser)
add_color_option(autoupdate_parser)
_add_config_option(autoupdate_parser)
autoupdate_parser.add_argument(
'--bleeding-edge', action='store_true',
@ -216,11 +207,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
clean_parser = subparsers.add_parser(
'clean', help='Clean out pre-commit files.',
)
_add_color_option(clean_parser)
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_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')
@ -230,7 +221,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER)
gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.')
_add_color_option(gc_parser)
add_color_option(gc_parser)
_add_config_option(gc_parser)
init_templatedir_parser = subparsers.add_parser(
@ -240,17 +231,23 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
'`git config init.templateDir`.'
),
)
_add_color_option(init_templatedir_parser)
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 = subparsers.add_parser(
'install', help='Install the pre-commit script.',
)
_add_color_option(install_parser)
add_color_option(install_parser)
_add_config_option(install_parser)
install_parser.add_argument(
'-f', '--overwrite', action='store_true',
@ -280,32 +277,32 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
'useful.'
),
)
_add_color_option(install_hooks_parser)
add_color_option(install_hooks_parser)
_add_config_option(install_hooks_parser)
migrate_config_parser = subparsers.add_parser(
'migrate-config',
help='Migrate list configuration to new map configuration.',
)
_add_color_option(migrate_config_parser)
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)
add_color_option(run_parser)
_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_color_option(sample_config_parser)
_add_config_option(sample_config_parser)
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_color_option(try_repo_parser)
_add_config_option(try_repo_parser)
try_repo_parser.add_argument(
'repo', help='Repository to source hooks from.',
@ -322,7 +319,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
uninstall_parser = subparsers.add_parser(
'uninstall', help='Uninstall the pre-commit script.',
)
_add_color_option(uninstall_parser)
add_color_option(uninstall_parser)
_add_config_option(uninstall_parser)
_add_hook_type_option(uninstall_parser)
@ -383,6 +380,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
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)

View file

@ -11,10 +11,13 @@ from pre_commit.store import Store
def check_all_hooks_match_files(config_file: str) -> int:
classifier = Classifier(git.get_all_files())
config = load_config(config_file)
classifier = Classifier.from_config(
git.get_all_files(), config['files'], config['exclude'],
)
retv = 0
for hook in all_hooks(load_config(config_file), Store()):
for hook in all_hooks(config, Store()):
if hook.always_run or hook.language == 'fail':
continue
elif not classifier.filenames_for_hook(hook):

View file

@ -28,11 +28,14 @@ def exclude_matches_any(
def check_useless_excludes(config_file: str) -> int:
config = load_config(config_file)
classifier = Classifier(git.get_all_files())
filenames = git.get_all_files()
classifier = Classifier.from_config(
filenames, config['files'], config['exclude'],
)
retv = 0
exclude = config['exclude']
if not exclude_matches_any(classifier.filenames, '', exclude):
if not exclude_matches_any(filenames, '', exclude):
print(
f'The global exclude pattern {exclude!r} does not match any files',
)

View file

@ -48,7 +48,7 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None:
with open(staging, 'w') as state_file:
state_file.write(json.dumps(state))
# Move the file into place atomically to indicate we've installed
os.rename(staging, state_filename)
os.replace(staging, state_filename)
def _hook_installed(hook: Hook) -> bool:
@ -82,6 +82,12 @@ def _hook_install(hook: Hook) -> None:
lang.install_environment(
hook.prefix, hook.language_version, hook.additional_dependencies,
)
if not lang.healthy(hook.prefix, hook.language_version):
raise AssertionError(
f'BUG: expected environment for {hook.language} to be healthy() '
f'immediately after install, please open an issue describing '
f'your environment',
)
# Write our state to indicate we're installed
_write_state(hook.prefix, venv, _state(hook.additional_dependencies))

View file

@ -43,6 +43,10 @@ class Store:
def __init__(self, directory: Optional[str] = None) -> 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)
@ -75,7 +79,7 @@ class Store:
self._create_config_table(db)
# Atomic file move
os.rename(tmpfile, self.db_path)
os.replace(tmpfile, self.db_path)
@contextlib.contextmanager
def exclusive_lock(self) -> Generator[None, None, None]:
@ -218,6 +222,8 @@ class Store:
)
def mark_config_used(self, path: str) -> None:
if self.readonly: # pragma: win32 no cover
return
path = os.path.realpath(path)
# don't insert config files that do not exist
if not os.path.exists(path):