Skip to content

Commit 95b8d71

Browse files
committed
Move most of the actual hook script into pre-commit hook-impl
1 parent 9315221 commit 95b8d71

File tree

10 files changed

+471
-201
lines changed

10 files changed

+471
-201
lines changed

pre_commit/commands/hook_impl.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import argparse
2+
import os.path
3+
import subprocess
4+
import sys
5+
from typing import Optional
6+
from typing import Sequence
7+
from typing import Tuple
8+
9+
from pre_commit.commands.run import run
10+
from pre_commit.envcontext import envcontext
11+
from pre_commit.parse_shebang import normalize_cmd
12+
from pre_commit.store import Store
13+
14+
Z40 = '0' * 40
15+
16+
17+
def _run_legacy(
18+
hook_type: str,
19+
hook_dir: str,
20+
args: Sequence[str],
21+
) -> Tuple[int, bytes]:
22+
if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'):
23+
raise SystemExit(
24+
f"bug: pre-commit's script is installed in migration mode\n"
25+
f'run `pre-commit install -f --hook-type {hook_type}` to fix '
26+
f'this\n\n'
27+
f'Please report this bug at '
28+
f'https://github.com/pre-commit/pre-commit/issues',
29+
)
30+
31+
if hook_type == 'pre-push':
32+
stdin = sys.stdin.buffer.read()
33+
else:
34+
stdin = b''
35+
36+
# not running in legacy mode
37+
legacy_hook = os.path.join(hook_dir, f'{hook_type}.legacy')
38+
if not os.access(legacy_hook, os.X_OK):
39+
return 0, stdin
40+
41+
with envcontext((('PRE_COMMIT_RUNNING_LEGACY', '1'),)):
42+
cmd = normalize_cmd((legacy_hook, *args))
43+
return subprocess.run(cmd, input=stdin).returncode, stdin
44+
45+
46+
def _validate_config(
47+
retv: int,
48+
config: str,
49+
skip_on_missing_config: bool,
50+
) -> None:
51+
if not os.path.isfile(config):
52+
if skip_on_missing_config or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'):
53+
print(f'`{config}` config file not found. Skipping `pre-commit`.')
54+
raise SystemExit(retv)
55+
else:
56+
print(
57+
f'No {config} file was found\n'
58+
f'- To temporarily silence this, run '
59+
f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'
60+
f'- To permanently silence this, install pre-commit with the '
61+
f'--allow-missing-config option\n'
62+
f'- To uninstall pre-commit run `pre-commit uninstall`',
63+
)
64+
raise SystemExit(1)
65+
66+
67+
def _ns(
68+
hook_type: str,
69+
color: bool,
70+
*,
71+
all_files: bool = False,
72+
origin: Optional[str] = None,
73+
source: Optional[str] = None,
74+
remote_name: Optional[str] = None,
75+
remote_url: Optional[str] = None,
76+
commit_msg_filename: Optional[str] = None,
77+
) -> argparse.Namespace:
78+
return argparse.Namespace(
79+
color=color,
80+
hook_stage=hook_type.replace('pre-', ''),
81+
origin=origin,
82+
source=source,
83+
remote_name=remote_name,
84+
remote_url=remote_url,
85+
commit_msg_filename=commit_msg_filename,
86+
all_files=all_files,
87+
files=(),
88+
hook=None,
89+
verbose=False,
90+
show_diff_on_failure=False,
91+
)
92+
93+
94+
def _rev_exists(rev: str) -> bool:
95+
return not subprocess.call(('git', 'rev-list', '--quiet', rev))
96+
97+
98+
def _pre_push_ns(
99+
color: bool,
100+
args: Sequence[str],
101+
stdin: bytes,
102+
) -> Optional[argparse.Namespace]:
103+
remote_name = args[0]
104+
remote_url = args[1]
105+
106+
for line in stdin.decode().splitlines():
107+
_, local_sha, _, remote_sha = line.split()
108+
if local_sha == Z40:
109+
continue
110+
elif remote_sha != Z40 and _rev_exists(remote_sha):
111+
return _ns(
112+
'pre-push', color,
113+
origin=local_sha, source=remote_sha,
114+
remote_name=remote_name, remote_url=remote_url,
115+
)
116+
else:
117+
# ancestors not found in remote
118+
ancestors = subprocess.check_output((
119+
'git', 'rev-list', local_sha, '--topo-order', '--reverse',
120+
'--not', f'--remotes={remote_name}',
121+
)).decode().strip()
122+
if not ancestors:
123+
continue
124+
else:
125+
first_ancestor = ancestors.splitlines()[0]
126+
cmd = ('git', 'rev-list', '--max-parents=0', local_sha)
127+
roots = set(subprocess.check_output(cmd).decode().splitlines())
128+
if first_ancestor in roots:
129+
# pushing the whole tree including root commit
130+
return _ns(
131+
'pre-push', color,
132+
all_files=True,
133+
remote_name=remote_name, remote_url=remote_url,
134+
)
135+
else:
136+
rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^')
137+
source = subprocess.check_output(rev_cmd).decode().strip()
138+
return _ns(
139+
'pre-push', color,
140+
origin=local_sha, source=source,
141+
remote_name=remote_name, remote_url=remote_url,
142+
)
143+
144+
# nothing to push
145+
return None
146+
147+
148+
def _run_ns(
149+
hook_type: str,
150+
color: bool,
151+
args: Sequence[str],
152+
stdin: bytes,
153+
) -> Optional[argparse.Namespace]:
154+
if hook_type == 'pre-push':
155+
return _pre_push_ns(color, args, stdin)
156+
elif hook_type in {'prepare-commit-msg', 'commit-msg'}:
157+
return _ns(hook_type, color, commit_msg_filename=args[0])
158+
elif hook_type in {'pre-merge-commit', 'pre-commit'}:
159+
return _ns(hook_type, color)
160+
else:
161+
raise AssertionError(f'unexpected hook type: {hook_type}')
162+
163+
164+
def hook_impl(
165+
store: Store,
166+
*,
167+
config: str,
168+
color: bool,
169+
hook_type: str,
170+
hook_dir: str,
171+
skip_on_missing_config: bool,
172+
args: Sequence[str],
173+
) -> int:
174+
retv, stdin = _run_legacy(hook_type, hook_dir, args)
175+
_validate_config(retv, config, skip_on_missing_config)
176+
ns = _run_ns(hook_type, color, args, stdin)
177+
if ns is None:
178+
return retv
179+
else:
180+
return retv | run(config, store, ns)

pre_commit/commands/install_uninstall.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def shebang() -> str:
6060
f'python{sys.version_info[0]}',
6161
]
6262
for path, exe in itertools.product(path_choices, exe_choices):
63-
if os.path.exists(os.path.join(path, exe)):
63+
if os.access(os.path.join(path, exe), os.X_OK):
6464
py = exe
6565
break
6666
else:
@@ -92,12 +92,10 @@ def _install_hook_script(
9292
f'Use -f to use only pre-commit.',
9393
)
9494

95-
params = {
96-
'CONFIG': config_file,
97-
'HOOK_TYPE': hook_type,
98-
'INSTALL_PYTHON': sys.executable,
99-
'SKIP_ON_MISSING_CONFIG': skip_on_missing_config,
100-
}
95+
args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}']
96+
if skip_on_missing_config:
97+
args.append('--skip-on-missing-config')
98+
params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args}
10199

102100
with open(hook_path, 'w') as hook_file:
103101
contents = resource_text('hook-tmpl')

pre_commit/languages/python.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def _find_by_sys_executable() -> Optional[str]:
5757
def _norm(path: str) -> Optional[str]:
5858
_, exe = os.path.split(path.lower())
5959
exe, _, _ = exe.partition('.exe')
60-
if find_executable(exe) and exe not in {'python', 'pythonw'}:
60+
if exe not in {'python', 'pythonw'} and find_executable(exe):
6161
return exe
6262
return None
6363

pre_commit/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from pre_commit.commands.autoupdate import autoupdate
1414
from pre_commit.commands.clean import clean
1515
from pre_commit.commands.gc import gc
16+
from pre_commit.commands.hook_impl import hook_impl
1617
from pre_commit.commands.init_templatedir import init_templatedir
1718
from pre_commit.commands.install_uninstall import install
1819
from pre_commit.commands.install_uninstall import install_hooks
@@ -197,6 +198,16 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
197198
_add_color_option(clean_parser)
198199
_add_config_option(clean_parser)
199200

201+
hook_impl_parser = subparsers.add_parser('hook-impl')
202+
_add_color_option(hook_impl_parser)
203+
_add_config_option(hook_impl_parser)
204+
hook_impl_parser.add_argument('--hook-type')
205+
hook_impl_parser.add_argument('--hook-dir')
206+
hook_impl_parser.add_argument(
207+
'--skip-on-missing-config', action='store_true',
208+
)
209+
hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER)
210+
200211
gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.')
201212
_add_color_option(gc_parser)
202213
_add_config_option(gc_parser)
@@ -329,6 +340,16 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
329340
return clean(store)
330341
elif args.command == 'gc':
331342
return gc(store)
343+
elif args.command == 'hook-impl':
344+
return hook_impl(
345+
store,
346+
config=args.config,
347+
color=args.color,
348+
hook_type=args.hook_type,
349+
hook_dir=args.hook_dir,
350+
skip_on_missing_config=args.skip_on_missing_config,
351+
args=args.rest[1:],
352+
)
332353
elif args.command == 'install':
333354
return install(
334355
args.config, store,

pre_commit/parse_shebang.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@ def find_executable(
2929
environ = _environ if _environ is not None else os.environ
3030

3131
if 'PATHEXT' in environ:
32-
possible_exe_names = tuple(
33-
exe + ext.lower() for ext in environ['PATHEXT'].split(os.pathsep)
34-
) + (exe,)
35-
32+
exts = environ['PATHEXT'].split(os.pathsep)
33+
possible_exe_names = tuple(f'{exe}{ext}' for ext in exts) + (exe,)
3634
else:
3735
possible_exe_names = (exe,)
3836

0 commit comments

Comments
 (0)