Skip to content

Commit 74fcf16

Browse files
authored
Merge pull request pre-commit#699 from pre-commit/rewrite_hook_template
Rewrite the hook template in python
2 parents 64ff767 + 5c90c1a commit 74fcf16

File tree

5 files changed

+223
-142
lines changed

5 files changed

+223
-142
lines changed

pre_commit/commands/install_uninstall.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import io
55
import os.path
6-
import pipes
76
import sys
87

98
from pre_commit import output
@@ -21,6 +20,8 @@
2120
'e358c9dae00eac5d06b38dfdb1e33a8c',
2221
)
2322
CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03'
23+
TEMPLATE_START = '# start templated\n'
24+
TEMPLATE_END = '# end templated\n'
2425

2526

2627
def is_our_script(filename):
@@ -50,32 +51,27 @@ def install(
5051
elif os.path.exists(legacy_path):
5152
output.write_line(
5253
'Running in migration mode with existing hooks at {}\n'
53-
'Use -f to use only pre-commit.'.format(
54-
legacy_path,
55-
),
54+
'Use -f to use only pre-commit.'.format(legacy_path),
5655
)
5756

58-
with io.open(hook_path, 'w') as pre_commit_file_obj:
59-
if hook_type == 'pre-push':
60-
with io.open(resource_filename('pre-push-tmpl')) as f:
61-
hook_specific_contents = f.read()
62-
elif hook_type == 'commit-msg':
63-
with io.open(resource_filename('commit-msg-tmpl')) as f:
64-
hook_specific_contents = f.read()
65-
elif hook_type == 'pre-commit':
66-
hook_specific_contents = ''
67-
else:
68-
raise AssertionError('Unknown hook type: {}'.format(hook_type))
69-
70-
skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false'
71-
contents = io.open(resource_filename('hook-tmpl')).read().format(
72-
sys_executable=pipes.quote(sys.executable),
73-
hook_type=hook_type,
74-
hook_specific=hook_specific_contents,
75-
config_file=runner.config_file,
76-
skip_on_missing_conf=skip_on_missing_conf,
77-
)
78-
pre_commit_file_obj.write(contents)
57+
params = {
58+
'CONFIG': runner.config_file,
59+
'HOOK_TYPE': hook_type,
60+
'INSTALL_PYTHON': sys.executable,
61+
'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf,
62+
}
63+
64+
with io.open(hook_path, 'w') as hook_file:
65+
with io.open(resource_filename('hook-tmpl')) as f:
66+
contents = f.read()
67+
before, rest = contents.split(TEMPLATE_START)
68+
to_template, after = rest.split(TEMPLATE_END)
69+
70+
hook_file.write(before + TEMPLATE_START)
71+
for line in to_template.splitlines():
72+
var = line.split()[0]
73+
hook_file.write('{} = {!r}\n'.format(var, params[var]))
74+
hook_file.write(TEMPLATE_END + after)
7975
make_executable(hook_path)
8076

8177
output.write_line('pre-commit installed at {}'.format(hook_path))

pre_commit/resources/commit-msg-tmpl

Lines changed: 0 additions & 1 deletion
This file was deleted.

pre_commit/resources/hook-tmpl

100644100755
Lines changed: 167 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,167 @@
1-
#!/usr/bin/env bash
2-
# This is a randomish md5 to identify this script
3-
# 138fd403232d2ddd5efb44317e38bf03
4-
5-
pushd "$(dirname "$0")" >& /dev/null
6-
HERE="$(pwd)"
7-
popd >& /dev/null
8-
9-
retv=0
10-
args=""
11-
12-
ENV_PYTHON={sys_executable}
13-
SKIP_ON_MISSING_CONF={skip_on_missing_conf}
14-
15-
if which pre-commit >& /dev/null; then
16-
exe="pre-commit"
17-
run_args=""
18-
elif "$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null; then
19-
exe="$ENV_PYTHON"
20-
run_args="-m pre_commit.main"
21-
elif python -c 'import pre_commit.main' >& /dev/null; then
22-
exe="python"
23-
run_args="-m pre_commit.main"
24-
else
25-
echo '`pre-commit` not found. Did you forget to activate your virtualenv?'
26-
exit 1
27-
fi
28-
29-
# Run the legacy pre-commit if it exists
30-
if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy "$@"; then
31-
retv=1
32-
fi
33-
34-
CONF_FILE="$(git rev-parse --show-toplevel)/{config_file}"
35-
if [ ! -f "$CONF_FILE" ]; then
36-
if [ "$SKIP_ON_MISSING_CONF" = true -o ! -z "$PRE_COMMIT_ALLOW_NO_CONFIG" ]; then
37-
echo '`{config_file}` config file not found. Skipping `pre-commit`.'
38-
exit $retv
39-
else
40-
echo 'No {config_file} file was found'
41-
echo '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`'
42-
echo '- To permanently silence this, install pre-commit with the `--allow-missing-config` option'
43-
echo '- To uninstall pre-commit run `pre-commit uninstall`'
44-
exit 1
45-
fi
46-
fi
47-
48-
{hook_specific}
49-
50-
# Run pre-commit
51-
if ! "$exe" $run_args run $args --config {config_file}; then
52-
retv=1
53-
fi
54-
55-
exit $retv
1+
#!/usr/bin/env python
2+
"""File generated by pre-commit: https://pre-commit.com"""
3+
from __future__ import print_function
4+
5+
import distutils.spawn
6+
import os
7+
import subprocess
8+
import sys
9+
10+
HERE = os.path.dirname(os.path.abspath(__file__))
11+
Z40 = '0' * 40
12+
ID_HASH = '138fd403232d2ddd5efb44317e38bf03'
13+
# start templated
14+
CONFIG = None
15+
HOOK_TYPE = None
16+
INSTALL_PYTHON = None
17+
SKIP_ON_MISSING_CONFIG = None
18+
# end templated
19+
20+
21+
class EarlyExit(RuntimeError):
22+
pass
23+
24+
25+
class FatalError(RuntimeError):
26+
pass
27+
28+
29+
def _norm_exe(exe):
30+
"""Necessary for shebang support on windows.
31+
32+
roughly lifted from `identify.identify.parse_shebang`
33+
"""
34+
with open(exe, 'rb') as f:
35+
if f.read(2) != b'#!':
36+
return ()
37+
try:
38+
first_line = f.readline().decode('UTF-8')
39+
except UnicodeDecodeError:
40+
return ()
41+
42+
cmd = first_line.split()
43+
if cmd[0] == '/usr/bin/env':
44+
del cmd[0]
45+
return tuple(cmd)
46+
47+
48+
def _run_legacy():
49+
if HOOK_TYPE == 'pre-push':
50+
stdin = getattr(sys.stdin, 'buffer', sys.stdin).read()
51+
else:
52+
stdin = None
53+
54+
legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE))
55+
if os.access(legacy_hook, os.X_OK):
56+
cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:])
57+
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None)
58+
proc.communicate(stdin)
59+
return proc.returncode, stdin
60+
else:
61+
return 0, stdin
62+
63+
64+
def _validate_config():
65+
cmd = ('git', 'rev-parse', '--show-toplevel')
66+
top_level = subprocess.check_output(cmd).decode('UTF-8').strip()
67+
cfg = os.path.join(top_level, CONFIG)
68+
if os.path.isfile(cfg):
69+
pass
70+
elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'):
71+
print(
72+
'`{}` config file not found. '
73+
'Skipping `pre-commit`.'.format(CONFIG),
74+
)
75+
raise EarlyExit()
76+
else:
77+
raise FatalError(
78+
'No {} file was found\n'
79+
'- To temporarily silence this, run '
80+
'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'
81+
'- To permanently silence this, install pre-commit with the '
82+
'--allow-missing-config option\n'
83+
'- To uninstall pre-commit run '
84+
'`pre-commit uninstall`'.format(CONFIG),
85+
)
86+
87+
88+
def _exe():
89+
with open(os.devnull, 'wb') as devnull:
90+
for exe in (INSTALL_PYTHON, sys.executable):
91+
try:
92+
if not subprocess.call(
93+
(exe, '-c', 'import pre_commit.main'),
94+
stdout=devnull, stderr=devnull,
95+
):
96+
return (exe, '-m', 'pre_commit.main', 'run')
97+
except OSError:
98+
pass
99+
100+
if distutils.spawn.find_executable('pre-commit'):
101+
return ('pre-commit', 'run')
102+
103+
raise FatalError(
104+
'`pre-commit` not found. Did you forget to activate your virtualenv?',
105+
)
106+
107+
108+
def _pre_push(stdin):
109+
remote = sys.argv[1]
110+
111+
opts = ()
112+
for line in stdin.decode('UTF-8').splitlines():
113+
_, local_sha, _, remote_sha = line.split()
114+
if local_sha == Z40:
115+
continue
116+
elif remote_sha != Z40:
117+
opts = ('--origin', local_sha, '--source', remote_sha)
118+
else:
119+
# First ancestor not found in remote
120+
first_ancestor = subprocess.check_output((
121+
'git', 'rev-list', '--max-count=1', '--topo-order',
122+
'--reverse', local_sha, '--not', '--remotes={}'.format(remote),
123+
)).decode().strip()
124+
if not first_ancestor:
125+
continue
126+
else:
127+
cmd = ('git', 'rev-list', '--max-parents=0', local_sha)
128+
roots = set(subprocess.check_output(cmd).decode().splitlines())
129+
if first_ancestor in roots:
130+
# pushing the whole tree including root commit
131+
opts = ('--all-files',)
132+
else:
133+
cmd = ('git', 'rev-parse', '{}^'.format(first_ancestor))
134+
source = subprocess.check_output(cmd).decode().strip()
135+
opts = ('--origin', local_sha, '--source', source)
136+
137+
if opts:
138+
return opts
139+
else:
140+
# An attempt to push an empty changeset
141+
raise EarlyExit()
142+
143+
144+
def _opts(stdin):
145+
fns = {
146+
'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]),
147+
'pre-commit': lambda _: (),
148+
'pre-push': _pre_push,
149+
}
150+
stage = HOOK_TYPE.replace('pre-', '')
151+
return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin)
152+
153+
154+
def main():
155+
retv, stdin = _run_legacy()
156+
try:
157+
_validate_config()
158+
return retv | subprocess.call(_exe() + _opts(stdin))
159+
except EarlyExit:
160+
return retv
161+
except FatalError as e:
162+
print(e.args[0])
163+
return 1
164+
165+
166+
if __name__ == '__main__':
167+
exit(main())

pre_commit/resources/pre-push-tmpl

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)