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
pre_commit/color.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
if sys.platform == 'win32': # pragma: no cover (windows)
|
|
8
|
+
def _enable() -> None:
|
|
9
|
+
from ctypes import POINTER
|
|
10
|
+
from ctypes import windll
|
|
11
|
+
from ctypes import WinError
|
|
12
|
+
from ctypes import WINFUNCTYPE
|
|
13
|
+
from ctypes.wintypes import BOOL
|
|
14
|
+
from ctypes.wintypes import DWORD
|
|
15
|
+
from ctypes.wintypes import HANDLE
|
|
16
|
+
|
|
17
|
+
STD_ERROR_HANDLE = -12
|
|
18
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
|
|
19
|
+
|
|
20
|
+
def bool_errcheck(result, func, args):
|
|
21
|
+
if not result:
|
|
22
|
+
raise WinError()
|
|
23
|
+
return args
|
|
24
|
+
|
|
25
|
+
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(
|
|
26
|
+
('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
|
|
30
|
+
('GetConsoleMode', windll.kernel32),
|
|
31
|
+
((1, 'hConsoleHandle'), (2, 'lpMode')),
|
|
32
|
+
)
|
|
33
|
+
GetConsoleMode.errcheck = bool_errcheck
|
|
34
|
+
|
|
35
|
+
SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
|
|
36
|
+
('SetConsoleMode', windll.kernel32),
|
|
37
|
+
((1, 'hConsoleHandle'), (1, 'dwMode')),
|
|
38
|
+
)
|
|
39
|
+
SetConsoleMode.errcheck = bool_errcheck
|
|
40
|
+
|
|
41
|
+
# As of Windows 10, the Windows console supports (some) ANSI escape
|
|
42
|
+
# sequences, but it needs to be enabled using `SetConsoleMode` first.
|
|
43
|
+
#
|
|
44
|
+
# More info on the escape sequences supported:
|
|
45
|
+
# https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx
|
|
46
|
+
stderr = GetStdHandle(STD_ERROR_HANDLE)
|
|
47
|
+
flags = GetConsoleMode(stderr)
|
|
48
|
+
SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
_enable()
|
|
52
|
+
except OSError:
|
|
53
|
+
terminal_supports_color = False
|
|
54
|
+
else:
|
|
55
|
+
terminal_supports_color = True
|
|
56
|
+
else: # pragma: win32 no cover
|
|
57
|
+
terminal_supports_color = True
|
|
58
|
+
|
|
59
|
+
RED = '\033[41m'
|
|
60
|
+
GREEN = '\033[42m'
|
|
61
|
+
YELLOW = '\033[43;30m'
|
|
62
|
+
TURQUOISE = '\033[46;30m'
|
|
63
|
+
SUBTLE = '\033[2m'
|
|
64
|
+
NORMAL = '\033[m'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def format_color(text: str, color: str, use_color_setting: bool) -> str:
|
|
68
|
+
"""Format text with color.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
text - Text to be formatted with color if `use_color`
|
|
72
|
+
color - The color start string
|
|
73
|
+
use_color_setting - Whether or not to color
|
|
74
|
+
"""
|
|
75
|
+
if use_color_setting:
|
|
76
|
+
return f'{color}{text}{NORMAL}'
|
|
77
|
+
else:
|
|
78
|
+
return text
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
COLOR_CHOICES = ('auto', 'always', 'never')
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def use_color(setting: str) -> bool:
|
|
85
|
+
"""Choose whether to use color based on the command argument.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
setting - Either `auto`, `always`, or `never`
|
|
89
|
+
"""
|
|
90
|
+
if setting not in COLOR_CHOICES:
|
|
91
|
+
raise ValueError(setting)
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
setting == 'always' or (
|
|
95
|
+
setting == 'auto' and
|
|
96
|
+
sys.stderr.isatty() and
|
|
97
|
+
terminal_supports_color and
|
|
98
|
+
os.getenv('TERM') != 'dumb'
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def add_color_option(parser: argparse.ArgumentParser) -> None:
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
'--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'),
|
|
106
|
+
type=use_color,
|
|
107
|
+
metavar='{' + ','.join(COLOR_CHOICES) + '}',
|
|
108
|
+
help='Whether to use color in output. Defaults to `%(default)s`.',
|
|
109
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import os.path
|
|
5
|
+
import re
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import Any
|
|
9
|
+
from typing import NamedTuple
|
|
10
|
+
|
|
11
|
+
import pre_commit.constants as C
|
|
12
|
+
from pre_commit import git
|
|
13
|
+
from pre_commit import output
|
|
14
|
+
from pre_commit import xargs
|
|
15
|
+
from pre_commit.clientlib import InvalidManifestError
|
|
16
|
+
from pre_commit.clientlib import load_config
|
|
17
|
+
from pre_commit.clientlib import load_manifest
|
|
18
|
+
from pre_commit.clientlib import LOCAL
|
|
19
|
+
from pre_commit.clientlib import META
|
|
20
|
+
from pre_commit.commands.migrate_config import migrate_config
|
|
21
|
+
from pre_commit.util import CalledProcessError
|
|
22
|
+
from pre_commit.util import cmd_output
|
|
23
|
+
from pre_commit.util import cmd_output_b
|
|
24
|
+
from pre_commit.yaml import yaml_dump
|
|
25
|
+
from pre_commit.yaml import yaml_load
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RevInfo(NamedTuple):
|
|
29
|
+
repo: str
|
|
30
|
+
rev: str
|
|
31
|
+
frozen: str | None = None
|
|
32
|
+
hook_ids: frozenset[str] = frozenset()
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_config(cls, config: dict[str, Any]) -> RevInfo:
|
|
36
|
+
return cls(config['repo'], config['rev'])
|
|
37
|
+
|
|
38
|
+
def update(self, tags_only: bool, freeze: bool) -> RevInfo:
|
|
39
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
40
|
+
_git = ('git', *git.NO_FS_MONITOR, '-C', tmp)
|
|
41
|
+
|
|
42
|
+
if tags_only:
|
|
43
|
+
tag_opt = '--abbrev=0'
|
|
44
|
+
else:
|
|
45
|
+
tag_opt = '--exact'
|
|
46
|
+
tag_cmd = (*_git, 'describe', 'FETCH_HEAD', '--tags', tag_opt)
|
|
47
|
+
|
|
48
|
+
git.init_repo(tmp, self.repo)
|
|
49
|
+
cmd_output_b(*_git, 'config', 'extensions.partialClone', 'true')
|
|
50
|
+
cmd_output_b(
|
|
51
|
+
*_git, 'fetch', 'origin', 'HEAD',
|
|
52
|
+
'--quiet', '--filter=blob:none', '--tags',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
rev = cmd_output(*tag_cmd)[1].strip()
|
|
57
|
+
except CalledProcessError:
|
|
58
|
+
rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip()
|
|
59
|
+
else:
|
|
60
|
+
if tags_only:
|
|
61
|
+
rev = git.get_best_candidate_tag(rev, tmp)
|
|
62
|
+
|
|
63
|
+
frozen = None
|
|
64
|
+
if freeze:
|
|
65
|
+
exact = cmd_output(*_git, 'rev-parse', rev)[1].strip()
|
|
66
|
+
if exact != rev:
|
|
67
|
+
rev, frozen = exact, rev
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# workaround for windows -- see #2865
|
|
71
|
+
cmd_output_b(*_git, 'show', f'{rev}:{C.MANIFEST_FILE}')
|
|
72
|
+
cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE)
|
|
73
|
+
except CalledProcessError:
|
|
74
|
+
pass # this will be caught by manifest validating code
|
|
75
|
+
try:
|
|
76
|
+
manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE))
|
|
77
|
+
except InvalidManifestError as e:
|
|
78
|
+
raise RepositoryCannotBeUpdatedError(f'[{self.repo}] {e}')
|
|
79
|
+
else:
|
|
80
|
+
hook_ids = frozenset(hook['id'] for hook in manifest)
|
|
81
|
+
|
|
82
|
+
return self._replace(rev=rev, frozen=frozen, hook_ids=hook_ids)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RepositoryCannotBeUpdatedError(RuntimeError):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _check_hooks_still_exist_at_rev(
|
|
90
|
+
repo_config: dict[str, Any],
|
|
91
|
+
info: RevInfo,
|
|
92
|
+
) -> None:
|
|
93
|
+
# See if any of our hooks were deleted with the new commits
|
|
94
|
+
hooks = {hook['id'] for hook in repo_config['hooks']}
|
|
95
|
+
hooks_missing = hooks - info.hook_ids
|
|
96
|
+
if hooks_missing:
|
|
97
|
+
raise RepositoryCannotBeUpdatedError(
|
|
98
|
+
f'[{info.repo}] Cannot update because the update target is '
|
|
99
|
+
f'missing these hooks: {", ".join(sorted(hooks_missing))}',
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _update_one(
|
|
104
|
+
i: int,
|
|
105
|
+
repo: dict[str, Any],
|
|
106
|
+
*,
|
|
107
|
+
tags_only: bool,
|
|
108
|
+
freeze: bool,
|
|
109
|
+
) -> tuple[int, RevInfo, RevInfo]:
|
|
110
|
+
old = RevInfo.from_config(repo)
|
|
111
|
+
new = old.update(tags_only=tags_only, freeze=freeze)
|
|
112
|
+
_check_hooks_still_exist_at_rev(repo, new)
|
|
113
|
+
return i, old, new
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$')
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _original_lines(
|
|
120
|
+
path: str,
|
|
121
|
+
rev_infos: list[RevInfo | None],
|
|
122
|
+
retry: bool = False,
|
|
123
|
+
) -> tuple[list[str], list[int]]:
|
|
124
|
+
"""detect `rev:` lines or reformat the file"""
|
|
125
|
+
with open(path, newline='') as f:
|
|
126
|
+
original = f.read()
|
|
127
|
+
|
|
128
|
+
lines = original.splitlines(True)
|
|
129
|
+
idxs = [i for i, line in enumerate(lines) if REV_LINE_RE.match(line)]
|
|
130
|
+
if len(idxs) == len(rev_infos):
|
|
131
|
+
return lines, idxs
|
|
132
|
+
elif retry:
|
|
133
|
+
raise AssertionError('could not find rev lines')
|
|
134
|
+
else:
|
|
135
|
+
with open(path, 'w') as f:
|
|
136
|
+
f.write(yaml_dump(yaml_load(original)))
|
|
137
|
+
return _original_lines(path, rev_infos, retry=True)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None:
|
|
141
|
+
lines, idxs = _original_lines(path, rev_infos)
|
|
142
|
+
|
|
143
|
+
for idx, rev_info in zip(idxs, rev_infos):
|
|
144
|
+
if rev_info is None:
|
|
145
|
+
continue
|
|
146
|
+
match = REV_LINE_RE.match(lines[idx])
|
|
147
|
+
assert match is not None
|
|
148
|
+
new_rev_s = yaml_dump({'rev': rev_info.rev}, default_style=match[3])
|
|
149
|
+
new_rev = new_rev_s.split(':', 1)[1].strip()
|
|
150
|
+
if rev_info.frozen is not None:
|
|
151
|
+
comment = f' # frozen: {rev_info.frozen}'
|
|
152
|
+
elif match[5].strip().startswith('# frozen:'):
|
|
153
|
+
comment = ''
|
|
154
|
+
else:
|
|
155
|
+
comment = match[5]
|
|
156
|
+
lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[6]}'
|
|
157
|
+
|
|
158
|
+
with open(path, 'w', newline='') as f:
|
|
159
|
+
f.write(''.join(lines))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def autoupdate(
|
|
163
|
+
config_file: str,
|
|
164
|
+
tags_only: bool,
|
|
165
|
+
freeze: bool,
|
|
166
|
+
repos: Sequence[str] = (),
|
|
167
|
+
jobs: int = 1,
|
|
168
|
+
) -> int:
|
|
169
|
+
"""Auto-update the pre-commit config to the latest versions of repos."""
|
|
170
|
+
migrate_config(config_file, quiet=True)
|
|
171
|
+
changed = False
|
|
172
|
+
retv = 0
|
|
173
|
+
|
|
174
|
+
config_repos = [
|
|
175
|
+
repo for repo in load_config(config_file)['repos']
|
|
176
|
+
if repo['repo'] not in {LOCAL, META}
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
rev_infos: list[RevInfo | None] = [None] * len(config_repos)
|
|
180
|
+
jobs = jobs or xargs.cpu_count() # 0 => number of cpus
|
|
181
|
+
jobs = min(jobs, len(repos) or len(config_repos)) # max 1-per-thread
|
|
182
|
+
jobs = max(jobs, 1) # at least one thread
|
|
183
|
+
with concurrent.futures.ThreadPoolExecutor(jobs) as exe:
|
|
184
|
+
futures = [
|
|
185
|
+
exe.submit(
|
|
186
|
+
_update_one,
|
|
187
|
+
i, repo, tags_only=tags_only, freeze=freeze,
|
|
188
|
+
)
|
|
189
|
+
for i, repo in enumerate(config_repos)
|
|
190
|
+
if not repos or repo['repo'] in repos
|
|
191
|
+
]
|
|
192
|
+
for future in concurrent.futures.as_completed(futures):
|
|
193
|
+
try:
|
|
194
|
+
i, old, new = future.result()
|
|
195
|
+
except RepositoryCannotBeUpdatedError as e:
|
|
196
|
+
output.write_line(str(e))
|
|
197
|
+
retv = 1
|
|
198
|
+
else:
|
|
199
|
+
if new.rev != old.rev:
|
|
200
|
+
changed = True
|
|
201
|
+
if new.frozen:
|
|
202
|
+
new_s = f'{new.frozen} (frozen)'
|
|
203
|
+
else:
|
|
204
|
+
new_s = new.rev
|
|
205
|
+
msg = f'updating {old.rev} -> {new_s}'
|
|
206
|
+
rev_infos[i] = new
|
|
207
|
+
else:
|
|
208
|
+
msg = 'already up to date!'
|
|
209
|
+
|
|
210
|
+
output.write_line(f'[{old.repo}] {msg}')
|
|
211
|
+
|
|
212
|
+
if changed:
|
|
213
|
+
_write_new_config(config_file, rev_infos)
|
|
214
|
+
|
|
215
|
+
return retv
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os.path
|
|
4
|
+
|
|
5
|
+
from pre_commit import output
|
|
6
|
+
from pre_commit.store import Store
|
|
7
|
+
from pre_commit.util import rmtree
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def clean(store: Store) -> int:
|
|
11
|
+
legacy_path = os.path.expanduser('~/.pre-commit')
|
|
12
|
+
for directory in (store.directory, legacy_path):
|
|
13
|
+
if os.path.exists(directory):
|
|
14
|
+
rmtree(directory)
|
|
15
|
+
output.write_line(f'Cleaned {directory}.')
|
|
16
|
+
return 0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os.path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import pre_commit.constants as C
|
|
7
|
+
from pre_commit import output
|
|
8
|
+
from pre_commit.clientlib import InvalidConfigError
|
|
9
|
+
from pre_commit.clientlib import InvalidManifestError
|
|
10
|
+
from pre_commit.clientlib import load_config
|
|
11
|
+
from pre_commit.clientlib import load_manifest
|
|
12
|
+
from pre_commit.clientlib import LOCAL
|
|
13
|
+
from pre_commit.clientlib import META
|
|
14
|
+
from pre_commit.store import Store
|
|
15
|
+
from pre_commit.util import rmtree
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _mark_used_repos(
|
|
19
|
+
store: Store,
|
|
20
|
+
all_repos: dict[tuple[str, str], str],
|
|
21
|
+
unused_repos: set[tuple[str, str]],
|
|
22
|
+
repo: dict[str, Any],
|
|
23
|
+
) -> None:
|
|
24
|
+
if repo['repo'] == META:
|
|
25
|
+
return
|
|
26
|
+
elif repo['repo'] == LOCAL:
|
|
27
|
+
for hook in repo['hooks']:
|
|
28
|
+
deps = hook.get('additional_dependencies')
|
|
29
|
+
unused_repos.discard((
|
|
30
|
+
store.db_repo_name(repo['repo'], deps),
|
|
31
|
+
C.LOCAL_REPO_VERSION,
|
|
32
|
+
))
|
|
33
|
+
else:
|
|
34
|
+
key = (repo['repo'], repo['rev'])
|
|
35
|
+
path = all_repos.get(key)
|
|
36
|
+
# can't inspect manifest if it isn't cloned
|
|
37
|
+
if path is None:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE))
|
|
42
|
+
except InvalidManifestError:
|
|
43
|
+
return
|
|
44
|
+
else:
|
|
45
|
+
unused_repos.discard(key)
|
|
46
|
+
by_id = {hook['id']: hook for hook in manifest}
|
|
47
|
+
|
|
48
|
+
for hook in repo['hooks']:
|
|
49
|
+
if hook['id'] not in by_id:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
deps = hook.get(
|
|
53
|
+
'additional_dependencies',
|
|
54
|
+
by_id[hook['id']]['additional_dependencies'],
|
|
55
|
+
)
|
|
56
|
+
unused_repos.discard((
|
|
57
|
+
store.db_repo_name(repo['repo'], deps), repo['rev'],
|
|
58
|
+
))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _gc(store: Store) -> int:
|
|
62
|
+
with store.exclusive_lock(), store.connect() as db:
|
|
63
|
+
store._create_configs_table(db)
|
|
64
|
+
|
|
65
|
+
repos = db.execute('SELECT repo, ref, path FROM repos').fetchall()
|
|
66
|
+
all_repos = {(repo, ref): path for repo, ref, path in repos}
|
|
67
|
+
unused_repos = set(all_repos)
|
|
68
|
+
|
|
69
|
+
configs_rows = db.execute('SELECT path FROM configs').fetchall()
|
|
70
|
+
configs = [path for path, in configs_rows]
|
|
71
|
+
|
|
72
|
+
dead_configs = []
|
|
73
|
+
for config_path in configs:
|
|
74
|
+
try:
|
|
75
|
+
config = load_config(config_path)
|
|
76
|
+
except InvalidConfigError:
|
|
77
|
+
dead_configs.append(config_path)
|
|
78
|
+
continue
|
|
79
|
+
else:
|
|
80
|
+
for repo in config['repos']:
|
|
81
|
+
_mark_used_repos(store, all_repos, unused_repos, repo)
|
|
82
|
+
|
|
83
|
+
paths = [(path,) for path in dead_configs]
|
|
84
|
+
db.executemany('DELETE FROM configs WHERE path = ?', paths)
|
|
85
|
+
|
|
86
|
+
db.executemany(
|
|
87
|
+
'DELETE FROM repos WHERE repo = ? and ref = ?',
|
|
88
|
+
sorted(unused_repos),
|
|
89
|
+
)
|
|
90
|
+
for k in unused_repos:
|
|
91
|
+
rmtree(all_repos[k])
|
|
92
|
+
|
|
93
|
+
return len(unused_repos)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def gc(store: Store) -> int:
|
|
97
|
+
output.write_line(f'{_gc(store)} repo(s) removed.')
|
|
98
|
+
return 0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from pre_commit.parse_shebang import normalize_cmd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_parsers(parser: argparse.ArgumentParser) -> None:
|
|
11
|
+
subparsers = parser.add_subparsers(dest='tool')
|
|
12
|
+
|
|
13
|
+
cd_parser = subparsers.add_parser(
|
|
14
|
+
'cd', help='cd to a subdir and run the command',
|
|
15
|
+
)
|
|
16
|
+
cd_parser.add_argument('subdir')
|
|
17
|
+
cd_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
|
18
|
+
|
|
19
|
+
ignore_exit_code_parser = subparsers.add_parser(
|
|
20
|
+
'ignore-exit-code', help='run the command but ignore the exit code',
|
|
21
|
+
)
|
|
22
|
+
ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
|
23
|
+
|
|
24
|
+
n1_parser = subparsers.add_parser(
|
|
25
|
+
'n1', help='run the command once per filename',
|
|
26
|
+
)
|
|
27
|
+
n1_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[
|
|
31
|
+
tuple[str, ...],
|
|
32
|
+
tuple[str, ...],
|
|
33
|
+
]:
|
|
34
|
+
for idx, val in enumerate(reversed(cmd)):
|
|
35
|
+
if val == '--':
|
|
36
|
+
split = len(cmd) - idx
|
|
37
|
+
break
|
|
38
|
+
else:
|
|
39
|
+
raise SystemExit('hazmat entry must end with `--`')
|
|
40
|
+
|
|
41
|
+
return cmd[:split - 1], cmd[split:]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cd(subdir: str, cmd: tuple[str, ...]) -> int:
|
|
45
|
+
cmd, filenames = _cmd_filenames(cmd)
|
|
46
|
+
|
|
47
|
+
prefix = f'{subdir}/'
|
|
48
|
+
new_filenames = []
|
|
49
|
+
for filename in filenames:
|
|
50
|
+
if not filename.startswith(prefix):
|
|
51
|
+
raise SystemExit(f'unexpected file without {prefix=}: {filename}')
|
|
52
|
+
else:
|
|
53
|
+
new_filenames.append(filename.removeprefix(prefix))
|
|
54
|
+
|
|
55
|
+
cmd = normalize_cmd(cmd)
|
|
56
|
+
return subprocess.call((*cmd, *new_filenames), cwd=subdir)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ignore_exit_code(cmd: tuple[str, ...]) -> int:
|
|
60
|
+
cmd = normalize_cmd(cmd)
|
|
61
|
+
subprocess.call(cmd)
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def n1(cmd: tuple[str, ...]) -> int:
|
|
66
|
+
cmd, filenames = _cmd_filenames(cmd)
|
|
67
|
+
cmd = normalize_cmd(cmd)
|
|
68
|
+
ret = 0
|
|
69
|
+
for filename in filenames:
|
|
70
|
+
ret |= subprocess.call((*cmd, filename))
|
|
71
|
+
return ret
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def impl(args: argparse.Namespace) -> int:
|
|
75
|
+
args.cmd = tuple(args.cmd)
|
|
76
|
+
if args.tool == 'cd':
|
|
77
|
+
return cd(args.subdir, args.cmd)
|
|
78
|
+
elif args.tool == 'ignore-exit-code':
|
|
79
|
+
return ignore_exit_code(args.cmd)
|
|
80
|
+
elif args.tool == 'n1':
|
|
81
|
+
return n1(args.cmd)
|
|
82
|
+
else:
|
|
83
|
+
raise NotImplementedError(f'unexpected tool: {args.tool}')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
87
|
+
parser = argparse.ArgumentParser()
|
|
88
|
+
add_parsers(parser)
|
|
89
|
+
args = parser.parse_args(argv)
|
|
90
|
+
|
|
91
|
+
return impl(args)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == '__main__':
|
|
95
|
+
raise SystemExit(main())
|