pre-commit-ex 4.5.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pre_commit/__init__.py +0 -0
- pre_commit/__main__.py +7 -0
- pre_commit/all_languages.py +50 -0
- pre_commit/clientlib.py +551 -0
- pre_commit/color.py +109 -0
- pre_commit/commands/__init__.py +0 -0
- pre_commit/commands/autoupdate.py +215 -0
- pre_commit/commands/clean.py +16 -0
- pre_commit/commands/gc.py +98 -0
- pre_commit/commands/hazmat.py +95 -0
- pre_commit/commands/hook_impl.py +272 -0
- pre_commit/commands/init_templatedir.py +39 -0
- pre_commit/commands/install_uninstall.py +167 -0
- pre_commit/commands/migrate_config.py +135 -0
- pre_commit/commands/run.py +474 -0
- pre_commit/commands/sample_config.py +18 -0
- pre_commit/commands/try_repo.py +77 -0
- pre_commit/commands/validate_config.py +18 -0
- pre_commit/commands/validate_manifest.py +18 -0
- pre_commit/constants.py +13 -0
- pre_commit/envcontext.py +62 -0
- pre_commit/error_handler.py +81 -0
- pre_commit/errors.py +5 -0
- pre_commit/file_lock.py +75 -0
- pre_commit/git.py +245 -0
- pre_commit/hook.py +60 -0
- pre_commit/lang_base.py +196 -0
- pre_commit/languages/__init__.py +0 -0
- pre_commit/languages/conda.py +77 -0
- pre_commit/languages/coursier.py +76 -0
- pre_commit/languages/dart.py +97 -0
- pre_commit/languages/docker.py +181 -0
- pre_commit/languages/docker_image.py +32 -0
- pre_commit/languages/dotnet.py +111 -0
- pre_commit/languages/fail.py +27 -0
- pre_commit/languages/golang.py +161 -0
- pre_commit/languages/haskell.py +56 -0
- pre_commit/languages/julia.py +133 -0
- pre_commit/languages/lua.py +75 -0
- pre_commit/languages/node.py +110 -0
- pre_commit/languages/perl.py +50 -0
- pre_commit/languages/pygrep.py +133 -0
- pre_commit/languages/python.py +228 -0
- pre_commit/languages/r.py +278 -0
- pre_commit/languages/ruby.py +145 -0
- pre_commit/languages/rust.py +160 -0
- pre_commit/languages/swift.py +50 -0
- pre_commit/languages/unsupported.py +10 -0
- pre_commit/languages/unsupported_script.py +32 -0
- pre_commit/logging_handler.py +42 -0
- pre_commit/main.py +472 -0
- pre_commit/meta_hooks/__init__.py +0 -0
- pre_commit/meta_hooks/check_hooks_apply.py +43 -0
- pre_commit/meta_hooks/check_useless_excludes.py +83 -0
- pre_commit/meta_hooks/identity.py +17 -0
- pre_commit/output.py +33 -0
- pre_commit/parse_shebang.py +85 -0
- pre_commit/prefix.py +18 -0
- pre_commit/repository.py +237 -0
- pre_commit/resources/__init__.py +0 -0
- pre_commit/resources/empty_template_.npmignore +1 -0
- pre_commit/resources/empty_template_Cargo.toml +7 -0
- pre_commit/resources/empty_template_LICENSE.renv +7 -0
- pre_commit/resources/empty_template_Makefile.PL +6 -0
- pre_commit/resources/empty_template_activate.R +440 -0
- pre_commit/resources/empty_template_environment.yml +9 -0
- pre_commit/resources/empty_template_go.mod +1 -0
- pre_commit/resources/empty_template_main.go +3 -0
- pre_commit/resources/empty_template_main.rs +1 -0
- pre_commit/resources/empty_template_package.json +4 -0
- pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec +12 -0
- pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec +6 -0
- pre_commit/resources/empty_template_pubspec.yaml +4 -0
- pre_commit/resources/empty_template_renv.lock +20 -0
- pre_commit/resources/empty_template_setup.py +4 -0
- pre_commit/resources/hook-tmpl +20 -0
- pre_commit/resources/rbenv.tar.gz +0 -0
- pre_commit/resources/ruby-build.tar.gz +0 -0
- pre_commit/resources/ruby-download.tar.gz +0 -0
- pre_commit/staged_files_only.py +113 -0
- pre_commit/store.py +235 -0
- pre_commit/util.py +239 -0
- pre_commit/xargs.py +184 -0
- pre_commit/yaml.py +19 -0
- pre_commit/yaml_rewrite.py +52 -0
- pre_commit_ex-4.5.1.dist-info/METADATA +63 -0
- pre_commit_ex-4.5.1.dist-info/RECORD +91 -0
- pre_commit_ex-4.5.1.dist-info/WHEEL +6 -0
- pre_commit_ex-4.5.1.dist-info/entry_points.txt +2 -0
- pre_commit_ex-4.5.1.dist-info/licenses/LICENSE +19 -0
- pre_commit_ex-4.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os.path
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
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
|
+
remote_branch: str | None = None,
|
|
73
|
+
local_branch: str | None = None,
|
|
74
|
+
from_ref: str | None = None,
|
|
75
|
+
to_ref: str | None = None,
|
|
76
|
+
pre_rebase_upstream: str | None = None,
|
|
77
|
+
pre_rebase_branch: str | None = None,
|
|
78
|
+
remote_name: str | None = None,
|
|
79
|
+
remote_url: str | None = None,
|
|
80
|
+
commit_msg_filename: str | None = None,
|
|
81
|
+
prepare_commit_message_source: str | None = None,
|
|
82
|
+
commit_object_name: str | None = None,
|
|
83
|
+
checkout_type: str | None = None,
|
|
84
|
+
is_squash_merge: str | None = None,
|
|
85
|
+
rewrite_command: str | None = None,
|
|
86
|
+
) -> argparse.Namespace:
|
|
87
|
+
return argparse.Namespace(
|
|
88
|
+
color=color,
|
|
89
|
+
hook_stage=hook_type,
|
|
90
|
+
remote_branch=remote_branch,
|
|
91
|
+
local_branch=local_branch,
|
|
92
|
+
from_ref=from_ref,
|
|
93
|
+
to_ref=to_ref,
|
|
94
|
+
pre_rebase_upstream=pre_rebase_upstream,
|
|
95
|
+
pre_rebase_branch=pre_rebase_branch,
|
|
96
|
+
remote_name=remote_name,
|
|
97
|
+
remote_url=remote_url,
|
|
98
|
+
commit_msg_filename=commit_msg_filename,
|
|
99
|
+
prepare_commit_message_source=prepare_commit_message_source,
|
|
100
|
+
commit_object_name=commit_object_name,
|
|
101
|
+
all_files=all_files,
|
|
102
|
+
checkout_type=checkout_type,
|
|
103
|
+
is_squash_merge=is_squash_merge,
|
|
104
|
+
rewrite_command=rewrite_command,
|
|
105
|
+
files=(),
|
|
106
|
+
hook=None,
|
|
107
|
+
verbose=False,
|
|
108
|
+
show_diff_on_failure=False,
|
|
109
|
+
fail_fast=False,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _rev_exists(rev: str) -> bool:
|
|
114
|
+
return not subprocess.call(('git', 'rev-list', '--quiet', rev))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _pre_push_ns(
|
|
118
|
+
color: bool,
|
|
119
|
+
args: Sequence[str],
|
|
120
|
+
stdin: bytes,
|
|
121
|
+
) -> argparse.Namespace | None:
|
|
122
|
+
remote_name = args[0]
|
|
123
|
+
remote_url = args[1]
|
|
124
|
+
|
|
125
|
+
for line in stdin.decode().splitlines():
|
|
126
|
+
parts = line.rsplit(maxsplit=3)
|
|
127
|
+
local_branch, local_sha, remote_branch, remote_sha = parts
|
|
128
|
+
if local_sha == Z40:
|
|
129
|
+
continue
|
|
130
|
+
elif remote_sha != Z40 and _rev_exists(remote_sha):
|
|
131
|
+
return _ns(
|
|
132
|
+
'pre-push', color,
|
|
133
|
+
from_ref=remote_sha, to_ref=local_sha,
|
|
134
|
+
remote_branch=remote_branch,
|
|
135
|
+
local_branch=local_branch,
|
|
136
|
+
remote_name=remote_name, remote_url=remote_url,
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
# ancestors not found in remote
|
|
140
|
+
ancestors = subprocess.check_output((
|
|
141
|
+
'git', 'rev-list', local_sha, '--topo-order', '--reverse',
|
|
142
|
+
'--not', f'--remotes={remote_name}',
|
|
143
|
+
)).decode().strip()
|
|
144
|
+
if not ancestors:
|
|
145
|
+
continue
|
|
146
|
+
else:
|
|
147
|
+
first_ancestor = ancestors.splitlines()[0]
|
|
148
|
+
cmd = ('git', 'rev-list', '--max-parents=0', local_sha)
|
|
149
|
+
roots = set(subprocess.check_output(cmd).decode().splitlines())
|
|
150
|
+
if first_ancestor in roots:
|
|
151
|
+
# pushing the whole tree including root commit
|
|
152
|
+
return _ns(
|
|
153
|
+
'pre-push', color,
|
|
154
|
+
all_files=True,
|
|
155
|
+
remote_name=remote_name, remote_url=remote_url,
|
|
156
|
+
remote_branch=remote_branch,
|
|
157
|
+
local_branch=local_branch,
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^')
|
|
161
|
+
source = subprocess.check_output(rev_cmd).decode().strip()
|
|
162
|
+
return _ns(
|
|
163
|
+
'pre-push', color,
|
|
164
|
+
from_ref=source, to_ref=local_sha,
|
|
165
|
+
remote_name=remote_name, remote_url=remote_url,
|
|
166
|
+
remote_branch=remote_branch,
|
|
167
|
+
local_branch=local_branch,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# nothing to push
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
_EXPECTED_ARG_LENGTH_BY_HOOK = {
|
|
175
|
+
'commit-msg': 1,
|
|
176
|
+
'post-checkout': 3,
|
|
177
|
+
'post-commit': 0,
|
|
178
|
+
'pre-commit': 0,
|
|
179
|
+
'pre-merge-commit': 0,
|
|
180
|
+
'post-merge': 1,
|
|
181
|
+
'post-rewrite': 1,
|
|
182
|
+
'pre-push': 2,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _check_args_length(hook_type: str, args: Sequence[str]) -> None:
|
|
187
|
+
if hook_type == 'prepare-commit-msg':
|
|
188
|
+
if len(args) < 1 or len(args) > 3:
|
|
189
|
+
raise SystemExit(
|
|
190
|
+
f'hook-impl for {hook_type} expected 1, 2, or 3 arguments '
|
|
191
|
+
f'but got {len(args)}: {args}',
|
|
192
|
+
)
|
|
193
|
+
elif hook_type == 'pre-rebase':
|
|
194
|
+
if len(args) < 1 or len(args) > 2:
|
|
195
|
+
raise SystemExit(
|
|
196
|
+
f'hook-impl for {hook_type} expected 1 or 2 arguments '
|
|
197
|
+
f'but got {len(args)}: {args}',
|
|
198
|
+
)
|
|
199
|
+
elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK:
|
|
200
|
+
expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type]
|
|
201
|
+
if len(args) != expected:
|
|
202
|
+
arguments_s = 'argument' if expected == 1 else 'arguments'
|
|
203
|
+
raise SystemExit(
|
|
204
|
+
f'hook-impl for {hook_type} expected {expected} {arguments_s} '
|
|
205
|
+
f'but got {len(args)}: {args}',
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
raise AssertionError(f'unexpected hook type: {hook_type}')
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _run_ns(
|
|
212
|
+
hook_type: str,
|
|
213
|
+
color: bool,
|
|
214
|
+
args: Sequence[str],
|
|
215
|
+
stdin: bytes,
|
|
216
|
+
) -> argparse.Namespace | None:
|
|
217
|
+
_check_args_length(hook_type, args)
|
|
218
|
+
if hook_type == 'pre-push':
|
|
219
|
+
return _pre_push_ns(color, args, stdin)
|
|
220
|
+
elif hook_type in 'commit-msg':
|
|
221
|
+
return _ns(hook_type, color, commit_msg_filename=args[0])
|
|
222
|
+
elif hook_type == 'prepare-commit-msg' and len(args) == 1:
|
|
223
|
+
return _ns(hook_type, color, commit_msg_filename=args[0])
|
|
224
|
+
elif hook_type == 'prepare-commit-msg' and len(args) == 2:
|
|
225
|
+
return _ns(
|
|
226
|
+
hook_type, color, commit_msg_filename=args[0],
|
|
227
|
+
prepare_commit_message_source=args[1],
|
|
228
|
+
)
|
|
229
|
+
elif hook_type == 'prepare-commit-msg' and len(args) == 3:
|
|
230
|
+
return _ns(
|
|
231
|
+
hook_type, color, commit_msg_filename=args[0],
|
|
232
|
+
prepare_commit_message_source=args[1], commit_object_name=args[2],
|
|
233
|
+
)
|
|
234
|
+
elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}:
|
|
235
|
+
return _ns(hook_type, color)
|
|
236
|
+
elif hook_type == 'post-checkout':
|
|
237
|
+
return _ns(
|
|
238
|
+
hook_type, color,
|
|
239
|
+
from_ref=args[0], to_ref=args[1], checkout_type=args[2],
|
|
240
|
+
)
|
|
241
|
+
elif hook_type == 'post-merge':
|
|
242
|
+
return _ns(hook_type, color, is_squash_merge=args[0])
|
|
243
|
+
elif hook_type == 'post-rewrite':
|
|
244
|
+
return _ns(hook_type, color, rewrite_command=args[0])
|
|
245
|
+
elif hook_type == 'pre-rebase' and len(args) == 1:
|
|
246
|
+
return _ns(hook_type, color, pre_rebase_upstream=args[0])
|
|
247
|
+
elif hook_type == 'pre-rebase' and len(args) == 2:
|
|
248
|
+
return _ns(
|
|
249
|
+
hook_type, color, pre_rebase_upstream=args[0],
|
|
250
|
+
pre_rebase_branch=args[1],
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
raise AssertionError(f'unexpected hook type: {hook_type}')
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def hook_impl(
|
|
257
|
+
store: Store,
|
|
258
|
+
*,
|
|
259
|
+
config: str,
|
|
260
|
+
color: bool,
|
|
261
|
+
hook_type: str,
|
|
262
|
+
hook_dir: str,
|
|
263
|
+
skip_on_missing_config: bool,
|
|
264
|
+
args: Sequence[str],
|
|
265
|
+
) -> int:
|
|
266
|
+
retv, stdin = _run_legacy(hook_type, hook_dir, args)
|
|
267
|
+
_validate_config(retv, config, skip_on_missing_config)
|
|
268
|
+
ns = _run_ns(hook_type, color, args, stdin)
|
|
269
|
+
if ns is None:
|
|
270
|
+
return retv
|
|
271
|
+
else:
|
|
272
|
+
return retv | run(config, store, ns)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os.path
|
|
5
|
+
|
|
6
|
+
from pre_commit.commands.install_uninstall import install
|
|
7
|
+
from pre_commit.store import Store
|
|
8
|
+
from pre_commit.util import CalledProcessError
|
|
9
|
+
from pre_commit.util import cmd_output
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger('pre_commit')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def init_templatedir(
|
|
15
|
+
config_file: str,
|
|
16
|
+
store: Store,
|
|
17
|
+
directory: str,
|
|
18
|
+
hook_types: list[str] | None,
|
|
19
|
+
skip_on_missing_config: bool = True,
|
|
20
|
+
) -> int:
|
|
21
|
+
install(
|
|
22
|
+
config_file,
|
|
23
|
+
store,
|
|
24
|
+
hook_types=hook_types,
|
|
25
|
+
overwrite=True,
|
|
26
|
+
skip_on_missing_config=skip_on_missing_config,
|
|
27
|
+
git_dir=directory,
|
|
28
|
+
)
|
|
29
|
+
try:
|
|
30
|
+
_, out, _ = cmd_output('git', 'config', 'init.templateDir')
|
|
31
|
+
except CalledProcessError:
|
|
32
|
+
configured_path = None
|
|
33
|
+
else:
|
|
34
|
+
configured_path = os.path.realpath(os.path.expanduser(out.strip()))
|
|
35
|
+
dest = os.path.realpath(directory)
|
|
36
|
+
if configured_path != dest:
|
|
37
|
+
logger.warning('`init.templateDir` not set to the target directory')
|
|
38
|
+
logger.warning(f'maybe `git config --global init.templateDir {dest}`?')
|
|
39
|
+
return 0
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os.path
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from pre_commit import git
|
|
10
|
+
from pre_commit import output
|
|
11
|
+
from pre_commit.clientlib import InvalidConfigError
|
|
12
|
+
from pre_commit.clientlib import load_config
|
|
13
|
+
from pre_commit.repository import all_hooks
|
|
14
|
+
from pre_commit.repository import install_hook_envs
|
|
15
|
+
from pre_commit.store import Store
|
|
16
|
+
from pre_commit.util import make_executable
|
|
17
|
+
from pre_commit.util import resource_text
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# This is used to identify the hook file we install
|
|
23
|
+
PRIOR_HASHES = (
|
|
24
|
+
b'4d9958c90bc262f47553e2c073f14cfe',
|
|
25
|
+
b'd8ee923c46731b42cd95cc869add4062',
|
|
26
|
+
b'49fd668cb42069aa1b6048464be5d395',
|
|
27
|
+
b'79f09a650522a87b0da915d0d983b2de',
|
|
28
|
+
b'e358c9dae00eac5d06b38dfdb1e33a8c',
|
|
29
|
+
)
|
|
30
|
+
CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03'
|
|
31
|
+
TEMPLATE_START = '# start templated\n'
|
|
32
|
+
TEMPLATE_END = '# end templated\n'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _hook_types(cfg_filename: str, hook_types: list[str] | None) -> list[str]:
|
|
36
|
+
if hook_types is not None:
|
|
37
|
+
return hook_types
|
|
38
|
+
else:
|
|
39
|
+
try:
|
|
40
|
+
cfg = load_config(cfg_filename)
|
|
41
|
+
except InvalidConfigError:
|
|
42
|
+
return ['pre-commit']
|
|
43
|
+
else:
|
|
44
|
+
return cfg['default_install_hook_types']
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _hook_paths(
|
|
48
|
+
hook_type: str,
|
|
49
|
+
git_dir: str | None = None,
|
|
50
|
+
) -> tuple[str, str]:
|
|
51
|
+
git_dir = git_dir if git_dir is not None else git.get_git_common_dir()
|
|
52
|
+
pth = os.path.join(git_dir, 'hooks', hook_type)
|
|
53
|
+
return pth, f'{pth}.legacy'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_our_script(filename: str) -> bool:
|
|
57
|
+
if not os.path.exists(filename): # pragma: win32 no cover (symlink)
|
|
58
|
+
return False
|
|
59
|
+
with open(filename, 'rb') as f:
|
|
60
|
+
contents = f.read()
|
|
61
|
+
return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _install_hook_script(
|
|
65
|
+
config_file: str,
|
|
66
|
+
hook_type: str,
|
|
67
|
+
overwrite: bool = False,
|
|
68
|
+
skip_on_missing_config: bool = False,
|
|
69
|
+
git_dir: str | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir)
|
|
72
|
+
|
|
73
|
+
os.makedirs(os.path.dirname(hook_path), exist_ok=True)
|
|
74
|
+
|
|
75
|
+
# If we have an existing hook, move it to pre-commit.legacy
|
|
76
|
+
if os.path.lexists(hook_path) and not is_our_script(hook_path):
|
|
77
|
+
shutil.move(hook_path, legacy_path)
|
|
78
|
+
|
|
79
|
+
# If we specify overwrite, we simply delete the legacy file
|
|
80
|
+
if overwrite and os.path.exists(legacy_path):
|
|
81
|
+
os.remove(legacy_path)
|
|
82
|
+
elif os.path.exists(legacy_path):
|
|
83
|
+
output.write_line(
|
|
84
|
+
f'Running in migration mode with existing hooks at {legacy_path}\n'
|
|
85
|
+
f'Use -f to use only pre-commit.',
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}']
|
|
89
|
+
if skip_on_missing_config:
|
|
90
|
+
args.append('--skip-on-missing-config')
|
|
91
|
+
|
|
92
|
+
with open(hook_path, 'w') as hook_file:
|
|
93
|
+
contents = resource_text('hook-tmpl')
|
|
94
|
+
before, rest = contents.split(TEMPLATE_START)
|
|
95
|
+
_, after = rest.split(TEMPLATE_END)
|
|
96
|
+
|
|
97
|
+
# on windows always use `/bin/sh` since `bash` might not be on PATH
|
|
98
|
+
# though we use bash-specific features `sh` on windows is actually
|
|
99
|
+
# bash in "POSIXLY_CORRECT" mode which still supports the features we
|
|
100
|
+
# use: subshells / arrays
|
|
101
|
+
if sys.platform == 'win32': # pragma: win32 cover
|
|
102
|
+
hook_file.write('#!/bin/sh\n')
|
|
103
|
+
|
|
104
|
+
hook_file.write(before + TEMPLATE_START)
|
|
105
|
+
hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n')
|
|
106
|
+
args_s = shlex.join(args)
|
|
107
|
+
hook_file.write(f'ARGS=({args_s})\n')
|
|
108
|
+
hook_file.write(TEMPLATE_END + after)
|
|
109
|
+
make_executable(hook_path)
|
|
110
|
+
|
|
111
|
+
output.write_line(f'pre-commit installed at {hook_path}')
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def install(
|
|
115
|
+
config_file: str,
|
|
116
|
+
store: Store,
|
|
117
|
+
hook_types: list[str] | None,
|
|
118
|
+
overwrite: bool = False,
|
|
119
|
+
hooks: bool = False,
|
|
120
|
+
skip_on_missing_config: bool = False,
|
|
121
|
+
git_dir: str | None = None,
|
|
122
|
+
) -> int:
|
|
123
|
+
if git_dir is None and git.has_core_hookpaths_set():
|
|
124
|
+
logger.error(
|
|
125
|
+
'Cowardly refusing to install hooks with `core.hooksPath` set.\n'
|
|
126
|
+
'hint: `git config --unset-all core.hooksPath`',
|
|
127
|
+
)
|
|
128
|
+
return 1
|
|
129
|
+
|
|
130
|
+
for hook_type in _hook_types(config_file, hook_types):
|
|
131
|
+
_install_hook_script(
|
|
132
|
+
config_file, hook_type,
|
|
133
|
+
overwrite=overwrite,
|
|
134
|
+
skip_on_missing_config=skip_on_missing_config,
|
|
135
|
+
git_dir=git_dir,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if hooks:
|
|
139
|
+
install_hooks(config_file, store)
|
|
140
|
+
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def install_hooks(config_file: str, store: Store) -> int:
|
|
145
|
+
install_hook_envs(all_hooks(load_config(config_file), store), store)
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _uninstall_hook_script(hook_type: str) -> None:
|
|
150
|
+
hook_path, legacy_path = _hook_paths(hook_type)
|
|
151
|
+
|
|
152
|
+
# If our file doesn't exist or it isn't ours, gtfo.
|
|
153
|
+
if not os.path.exists(hook_path) or not is_our_script(hook_path):
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
os.remove(hook_path)
|
|
157
|
+
output.write_line(f'{hook_type} uninstalled')
|
|
158
|
+
|
|
159
|
+
if os.path.exists(legacy_path):
|
|
160
|
+
os.replace(legacy_path, hook_path)
|
|
161
|
+
output.write_line(f'Restored previous hooks to {hook_path}')
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def uninstall(config_file: str, hook_types: list[str] | None) -> int:
|
|
165
|
+
for hook_type in _hook_types(config_file, hook_types):
|
|
166
|
+
_uninstall_hook_script(hook_type)
|
|
167
|
+
return 0
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import itertools
|
|
5
|
+
import textwrap
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
import cfgv
|
|
9
|
+
import yaml
|
|
10
|
+
from yaml.nodes import ScalarNode
|
|
11
|
+
|
|
12
|
+
from pre_commit.clientlib import InvalidConfigError
|
|
13
|
+
from pre_commit.yaml import yaml_compose
|
|
14
|
+
from pre_commit.yaml import yaml_load
|
|
15
|
+
from pre_commit.yaml_rewrite import MappingKey
|
|
16
|
+
from pre_commit.yaml_rewrite import MappingValue
|
|
17
|
+
from pre_commit.yaml_rewrite import match
|
|
18
|
+
from pre_commit.yaml_rewrite import SequenceItem
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_header_line(line: str) -> bool:
|
|
22
|
+
return line.startswith(('#', '---')) or not line.strip()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _migrate_map(contents: str) -> str:
|
|
26
|
+
if isinstance(yaml_load(contents), list):
|
|
27
|
+
# Find the first non-header line
|
|
28
|
+
lines = contents.splitlines(True)
|
|
29
|
+
i = 0
|
|
30
|
+
# Only loop on non empty configuration file
|
|
31
|
+
while i < len(lines) and _is_header_line(lines[i]):
|
|
32
|
+
i += 1
|
|
33
|
+
|
|
34
|
+
header = ''.join(lines[:i])
|
|
35
|
+
rest = ''.join(lines[i:])
|
|
36
|
+
|
|
37
|
+
# If they are using the "default" flow style of yaml, this operation
|
|
38
|
+
# will yield a valid configuration
|
|
39
|
+
try:
|
|
40
|
+
trial_contents = f'{header}repos:\n{rest}'
|
|
41
|
+
yaml_load(trial_contents)
|
|
42
|
+
contents = trial_contents
|
|
43
|
+
except yaml.YAMLError:
|
|
44
|
+
contents = f'{header}repos:\n{textwrap.indent(rest, " " * 4)}'
|
|
45
|
+
|
|
46
|
+
return contents
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _preserve_style(n: ScalarNode, *, s: str) -> str:
|
|
50
|
+
style = n.style or ''
|
|
51
|
+
return f'{style}{s}{style}'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _fix_stage(n: ScalarNode) -> str:
|
|
55
|
+
return _preserve_style(n, s=f'pre-{n.value}')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _migrate_composed(contents: str) -> str:
|
|
59
|
+
tree = yaml_compose(contents)
|
|
60
|
+
rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = []
|
|
61
|
+
|
|
62
|
+
# sha -> rev
|
|
63
|
+
sha_to_rev_replace = functools.partial(_preserve_style, s='rev')
|
|
64
|
+
sha_to_rev_matcher = (
|
|
65
|
+
MappingValue('repos'),
|
|
66
|
+
SequenceItem(),
|
|
67
|
+
MappingKey('sha'),
|
|
68
|
+
)
|
|
69
|
+
for node in match(tree, sha_to_rev_matcher):
|
|
70
|
+
rewrites.append((node, sha_to_rev_replace))
|
|
71
|
+
|
|
72
|
+
# python_venv -> python
|
|
73
|
+
language_matcher = (
|
|
74
|
+
MappingValue('repos'),
|
|
75
|
+
SequenceItem(),
|
|
76
|
+
MappingValue('hooks'),
|
|
77
|
+
SequenceItem(),
|
|
78
|
+
MappingValue('language'),
|
|
79
|
+
)
|
|
80
|
+
python_venv_replace = functools.partial(_preserve_style, s='python')
|
|
81
|
+
for node in match(tree, language_matcher):
|
|
82
|
+
if node.value == 'python_venv':
|
|
83
|
+
rewrites.append((node, python_venv_replace))
|
|
84
|
+
|
|
85
|
+
# stages rewrites
|
|
86
|
+
default_stages_matcher = (MappingValue('default_stages'), SequenceItem())
|
|
87
|
+
default_stages_match = match(tree, default_stages_matcher)
|
|
88
|
+
hook_stages_matcher = (
|
|
89
|
+
MappingValue('repos'),
|
|
90
|
+
SequenceItem(),
|
|
91
|
+
MappingValue('hooks'),
|
|
92
|
+
SequenceItem(),
|
|
93
|
+
MappingValue('stages'),
|
|
94
|
+
SequenceItem(),
|
|
95
|
+
)
|
|
96
|
+
hook_stages_match = match(tree, hook_stages_matcher)
|
|
97
|
+
for node in itertools.chain(default_stages_match, hook_stages_match):
|
|
98
|
+
if node.value in {'commit', 'push', 'merge-commit'}:
|
|
99
|
+
rewrites.append((node, _fix_stage))
|
|
100
|
+
|
|
101
|
+
rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index)
|
|
102
|
+
|
|
103
|
+
src_parts = []
|
|
104
|
+
end: int | None = None
|
|
105
|
+
for node, func in rewrites:
|
|
106
|
+
src_parts.append(contents[node.end_mark.index:end])
|
|
107
|
+
src_parts.append(func(node))
|
|
108
|
+
end = node.start_mark.index
|
|
109
|
+
src_parts.append(contents[:end])
|
|
110
|
+
src_parts.reverse()
|
|
111
|
+
return ''.join(src_parts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def migrate_config(config_file: str, quiet: bool = False) -> int:
|
|
115
|
+
with open(config_file) as f:
|
|
116
|
+
orig_contents = contents = f.read()
|
|
117
|
+
|
|
118
|
+
with cfgv.reraise_as(InvalidConfigError):
|
|
119
|
+
with cfgv.validate_context(f'File {config_file}'):
|
|
120
|
+
try:
|
|
121
|
+
yaml_load(orig_contents)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise cfgv.ValidationError(str(e))
|
|
124
|
+
|
|
125
|
+
contents = _migrate_map(contents)
|
|
126
|
+
contents = _migrate_composed(contents)
|
|
127
|
+
|
|
128
|
+
if contents != orig_contents:
|
|
129
|
+
with open(config_file, 'w') as f:
|
|
130
|
+
f.write(contents)
|
|
131
|
+
|
|
132
|
+
print('Configuration has been migrated.')
|
|
133
|
+
elif not quiet:
|
|
134
|
+
print('Configuration is already migrated.')
|
|
135
|
+
return 0
|