mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-17 08:14:42 +04:00
Rewrite the hook template in python
This commit is contained in:
parent
64ff7677fa
commit
5c90c1a68f
5 changed files with 213 additions and 132 deletions
|
|
@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os.path
|
import os.path
|
||||||
import pipes
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
|
|
@ -21,6 +20,8 @@ PRIOR_HASHES = (
|
||||||
'e358c9dae00eac5d06b38dfdb1e33a8c',
|
'e358c9dae00eac5d06b38dfdb1e33a8c',
|
||||||
)
|
)
|
||||||
CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03'
|
CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03'
|
||||||
|
TEMPLATE_START = '# start templated\n'
|
||||||
|
TEMPLATE_END = '# end templated\n'
|
||||||
|
|
||||||
|
|
||||||
def is_our_script(filename):
|
def is_our_script(filename):
|
||||||
|
|
@ -50,32 +51,27 @@ def install(
|
||||||
elif os.path.exists(legacy_path):
|
elif os.path.exists(legacy_path):
|
||||||
output.write_line(
|
output.write_line(
|
||||||
'Running in migration mode with existing hooks at {}\n'
|
'Running in migration mode with existing hooks at {}\n'
|
||||||
'Use -f to use only pre-commit.'.format(
|
'Use -f to use only pre-commit.'.format(legacy_path),
|
||||||
legacy_path,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with io.open(hook_path, 'w') as pre_commit_file_obj:
|
params = {
|
||||||
if hook_type == 'pre-push':
|
'CONFIG': runner.config_file,
|
||||||
with io.open(resource_filename('pre-push-tmpl')) as f:
|
'HOOK_TYPE': hook_type,
|
||||||
hook_specific_contents = f.read()
|
'INSTALL_PYTHON': sys.executable,
|
||||||
elif hook_type == 'commit-msg':
|
'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf,
|
||||||
with io.open(resource_filename('commit-msg-tmpl')) as f:
|
}
|
||||||
hook_specific_contents = f.read()
|
|
||||||
elif hook_type == 'pre-commit':
|
|
||||||
hook_specific_contents = ''
|
|
||||||
else:
|
|
||||||
raise AssertionError('Unknown hook type: {}'.format(hook_type))
|
|
||||||
|
|
||||||
skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false'
|
with io.open(hook_path, 'w') as hook_file:
|
||||||
contents = io.open(resource_filename('hook-tmpl')).read().format(
|
with io.open(resource_filename('hook-tmpl')) as f:
|
||||||
sys_executable=pipes.quote(sys.executable),
|
contents = f.read()
|
||||||
hook_type=hook_type,
|
before, rest = contents.split(TEMPLATE_START)
|
||||||
hook_specific=hook_specific_contents,
|
to_template, after = rest.split(TEMPLATE_END)
|
||||||
config_file=runner.config_file,
|
|
||||||
skip_on_missing_conf=skip_on_missing_conf,
|
hook_file.write(before + TEMPLATE_START)
|
||||||
)
|
for line in to_template.splitlines():
|
||||||
pre_commit_file_obj.write(contents)
|
var = line.split()[0]
|
||||||
|
hook_file.write('{} = {!r}\n'.format(var, params[var]))
|
||||||
|
hook_file.write(TEMPLATE_END + after)
|
||||||
make_executable(hook_path)
|
make_executable(hook_path)
|
||||||
|
|
||||||
output.write_line('pre-commit installed at {}'.format(hook_path))
|
output.write_line('pre-commit installed at {}'.format(hook_path))
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
args="--hook-stage=commit-msg --commit-msg-filename=$1"
|
|
||||||
204
pre_commit/resources/hook-tmpl
Normal file → Executable file
204
pre_commit/resources/hook-tmpl
Normal file → Executable file
|
|
@ -1,55 +1,167 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env python
|
||||||
# This is a randomish md5 to identify this script
|
"""File generated by pre-commit: https://pre-commit.com"""
|
||||||
# 138fd403232d2ddd5efb44317e38bf03
|
from __future__ import print_function
|
||||||
|
|
||||||
pushd "$(dirname "$0")" >& /dev/null
|
import distutils.spawn
|
||||||
HERE="$(pwd)"
|
import os
|
||||||
popd >& /dev/null
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
retv=0
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
args=""
|
Z40 = '0' * 40
|
||||||
|
ID_HASH = '138fd403232d2ddd5efb44317e38bf03'
|
||||||
|
# start templated
|
||||||
|
CONFIG = None
|
||||||
|
HOOK_TYPE = None
|
||||||
|
INSTALL_PYTHON = None
|
||||||
|
SKIP_ON_MISSING_CONFIG = None
|
||||||
|
# end templated
|
||||||
|
|
||||||
ENV_PYTHON={sys_executable}
|
|
||||||
SKIP_ON_MISSING_CONF={skip_on_missing_conf}
|
|
||||||
|
|
||||||
if which pre-commit >& /dev/null; then
|
class EarlyExit(RuntimeError):
|
||||||
exe="pre-commit"
|
pass
|
||||||
run_args=""
|
|
||||||
elif "$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null; then
|
|
||||||
exe="$ENV_PYTHON"
|
|
||||||
run_args="-m pre_commit.main"
|
|
||||||
elif python -c 'import pre_commit.main' >& /dev/null; then
|
|
||||||
exe="python"
|
|
||||||
run_args="-m pre_commit.main"
|
|
||||||
else
|
|
||||||
echo '`pre-commit` not found. Did you forget to activate your virtualenv?'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run the legacy pre-commit if it exists
|
|
||||||
if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy "$@"; then
|
|
||||||
retv=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
CONF_FILE="$(git rev-parse --show-toplevel)/{config_file}"
|
class FatalError(RuntimeError):
|
||||||
if [ ! -f "$CONF_FILE" ]; then
|
pass
|
||||||
if [ "$SKIP_ON_MISSING_CONF" = true -o ! -z "$PRE_COMMIT_ALLOW_NO_CONFIG" ]; then
|
|
||||||
echo '`{config_file}` config file not found. Skipping `pre-commit`.'
|
|
||||||
exit $retv
|
|
||||||
else
|
|
||||||
echo 'No {config_file} file was found'
|
|
||||||
echo '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`'
|
|
||||||
echo '- To permanently silence this, install pre-commit with the `--allow-missing-config` option'
|
|
||||||
echo '- To uninstall pre-commit run `pre-commit uninstall`'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
{hook_specific}
|
|
||||||
|
|
||||||
# Run pre-commit
|
def _norm_exe(exe):
|
||||||
if ! "$exe" $run_args run $args --config {config_file}; then
|
"""Necessary for shebang support on windows.
|
||||||
retv=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $retv
|
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 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 _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:
|
||||||
|
opts = ('--origin', local_sha, '--source', remote_sha)
|
||||||
|
else:
|
||||||
|
# First ancestor not found in remote
|
||||||
|
first_ancestor = subprocess.check_output((
|
||||||
|
'git', 'rev-list', '--max-count=1', '--topo-order',
|
||||||
|
'--reverse', local_sha, '--not', '--remotes={}'.format(remote),
|
||||||
|
)).decode().strip()
|
||||||
|
if not first_ancestor:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
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 = {
|
||||||
|
'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)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(main())
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
z40=0000000000000000000000000000000000000000
|
|
||||||
while read local_ref local_sha remote_ref remote_sha
|
|
||||||
do
|
|
||||||
if [ "$local_sha" != $z40 ]; then
|
|
||||||
if [ "$remote_sha" = $z40 ]; then
|
|
||||||
# First ancestor not found in remote
|
|
||||||
first_ancestor=$(git rev-list --topo-order --reverse "$local_sha" --not --remotes="$1" | head -n 1)
|
|
||||||
if [ -n "$first_ancestor" ]; then
|
|
||||||
# Check that the ancestor has at least one parent
|
|
||||||
git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
# Pushing the whole tree, including the root commit, so run on all files
|
|
||||||
args="--all-files"
|
|
||||||
else
|
|
||||||
source=$(git rev-parse "$first_ancestor"^)
|
|
||||||
args="--origin $local_sha --source $source"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
args="--origin $local_sha --source $remote_sha"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$args" != "" ]; then
|
|
||||||
args="$args --hook-stage push"
|
|
||||||
else
|
|
||||||
# If args is empty, then an attempt to push on an empty
|
|
||||||
# changeset is being made. In this case, just exit cleanly
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os.path
|
import os.path
|
||||||
import pipes
|
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
@ -49,35 +48,11 @@ def test_is_previous_pre_commit(tmpdir):
|
||||||
def test_install_pre_commit(tempdir_factory):
|
def test_install_pre_commit(tempdir_factory):
|
||||||
path = git_dir(tempdir_factory)
|
path = git_dir(tempdir_factory)
|
||||||
runner = Runner(path, C.CONFIG_FILE)
|
runner = Runner(path, C.CONFIG_FILE)
|
||||||
ret = install(runner)
|
assert not install(runner)
|
||||||
assert ret == 0
|
|
||||||
assert os.path.exists(runner.pre_commit_path)
|
|
||||||
pre_commit_contents = io.open(runner.pre_commit_path).read()
|
|
||||||
pre_commit_script = resource_filename('hook-tmpl')
|
|
||||||
expected_contents = io.open(pre_commit_script).read().format(
|
|
||||||
sys_executable=pipes.quote(sys.executable),
|
|
||||||
hook_type='pre-commit',
|
|
||||||
hook_specific='',
|
|
||||||
config_file=runner.config_file,
|
|
||||||
skip_on_missing_conf='false',
|
|
||||||
)
|
|
||||||
assert pre_commit_contents == expected_contents
|
|
||||||
assert os.access(runner.pre_commit_path, os.X_OK)
|
assert os.access(runner.pre_commit_path, os.X_OK)
|
||||||
|
|
||||||
ret = install(runner, hook_type='pre-push')
|
assert not install(runner, hook_type='pre-push')
|
||||||
assert ret == 0
|
assert os.access(runner.pre_push_path, os.X_OK)
|
||||||
assert os.path.exists(runner.pre_push_path)
|
|
||||||
pre_push_contents = io.open(runner.pre_push_path).read()
|
|
||||||
pre_push_tmpl = resource_filename('pre-push-tmpl')
|
|
||||||
pre_push_template_contents = io.open(pre_push_tmpl).read()
|
|
||||||
expected_contents = io.open(pre_commit_script).read().format(
|
|
||||||
sys_executable=pipes.quote(sys.executable),
|
|
||||||
hook_type='pre-push',
|
|
||||||
hook_specific=pre_push_template_contents,
|
|
||||||
config_file=runner.config_file,
|
|
||||||
skip_on_missing_conf='false',
|
|
||||||
)
|
|
||||||
assert pre_push_contents == expected_contents
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_hooks_directory_not_present(tempdir_factory):
|
def test_install_hooks_directory_not_present(tempdir_factory):
|
||||||
|
|
@ -242,7 +217,7 @@ def test_environment_not_sourced(tempdir_factory):
|
||||||
path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
|
path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
|
||||||
with cwd(path):
|
with cwd(path):
|
||||||
# Patch the executable to simulate rming virtualenv
|
# Patch the executable to simulate rming virtualenv
|
||||||
with mock.patch.object(sys, 'executable', '/bin/false'):
|
with mock.patch.object(sys, 'executable', '/does-not-exist'):
|
||||||
assert install(Runner(path, C.CONFIG_FILE)) == 0
|
assert install(Runner(path, C.CONFIG_FILE)) == 0
|
||||||
|
|
||||||
# Use a specific homedir to ignore --user installs
|
# Use a specific homedir to ignore --user installs
|
||||||
|
|
@ -262,7 +237,7 @@ def test_environment_not_sourced(tempdir_factory):
|
||||||
)
|
)
|
||||||
assert ret == 1
|
assert ret == 1
|
||||||
assert stdout == ''
|
assert stdout == ''
|
||||||
assert stderr == (
|
assert stderr.replace('\r\n', '\n') == (
|
||||||
'`pre-commit` not found. '
|
'`pre-commit` not found. '
|
||||||
'Did you forget to activate your virtualenv?\n'
|
'Did you forget to activate your virtualenv?\n'
|
||||||
)
|
)
|
||||||
|
|
@ -593,6 +568,36 @@ def test_pre_push_integration_empty_push(tempdir_factory):
|
||||||
assert retc == 0
|
assert retc == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_pre_push_legacy(tempdir_factory):
|
||||||
|
upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
|
||||||
|
path = tempdir_factory.get()
|
||||||
|
cmd_output('git', 'clone', upstream, path)
|
||||||
|
with cwd(path):
|
||||||
|
runner = Runner(path, C.CONFIG_FILE)
|
||||||
|
|
||||||
|
hook_path = runner.get_hook_path('pre-push')
|
||||||
|
mkdirp(os.path.dirname(hook_path))
|
||||||
|
with io.open(hook_path, 'w') as hook_file:
|
||||||
|
hook_file.write(
|
||||||
|
'#!/usr/bin/env bash\n'
|
||||||
|
'set -eu\n'
|
||||||
|
'read lr ls rr rs\n'
|
||||||
|
'test -n "$lr" -a -n "$ls" -a -n "$rr" -a -n "$rs"\n'
|
||||||
|
'echo legacy\n',
|
||||||
|
)
|
||||||
|
make_executable(hook_path)
|
||||||
|
|
||||||
|
install(runner, hook_type='pre-push')
|
||||||
|
assert _get_commit_output(tempdir_factory)[0] == 0
|
||||||
|
|
||||||
|
retc, output = _get_push_output(tempdir_factory)
|
||||||
|
assert retc == 0
|
||||||
|
first_line, _, third_line = output.splitlines()[:3]
|
||||||
|
assert first_line == 'legacy'
|
||||||
|
assert third_line.startswith('Bash hook')
|
||||||
|
assert third_line.endswith('Passed')
|
||||||
|
|
||||||
|
|
||||||
def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory):
|
def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory):
|
||||||
install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg')
|
install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg')
|
||||||
retc, out = _get_commit_output(tempdir_factory)
|
retc, out = _get_commit_output(tempdir_factory)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue