pre-commit/pre_commit/commands/gc.py
anthony sottile 6ee2b2dfb0 wip
2026-03-19 13:45:12 -04:00

115 lines
3.5 KiB
Python

from __future__ import annotations
import json
import os.path
from typing import Any
import pre_commit.constants as C
from pre_commit import output
from pre_commit.clientlib import InvalidConfigError
from pre_commit.clientlib import InvalidManifestError
from pre_commit.clientlib import load_config
from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META
from pre_commit.store import Store
from pre_commit.util import rmtree
def _mark_used(
config: dict[str, Any],
repo: dict[str, Any],
manifests: dict[tuple[str, str], dict[str, Any]],
unused_manifests: set[tuple[str, str]],
unused_installs: set[str],
) -> None:
if repo['repo'] == META:
return
elif repo['repo'] == LOCAL:
for hook in repo['hooks']:
deps = hook['additional_dependencies']
unused_installs.discard((
repo['repo'], C.LOCAL_REPO_VERSION,
repo['language'],
store.db_repo_name(repo['repo'], deps),
C.LOCAL_REPO_VERSION,
))
else:
key = (repo['repo'], repo['rev'])
path = all_repos.get(key)
# can't inspect manifest if it isn't cloned
if path is None:
return
try:
manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE))
except InvalidManifestError:
return
else:
unused_repos.discard(key)
by_id = manifest['hooks']
for hook in repo['hooks']:
if hook['id'] not in by_id:
continue
deps = hook.get(
'additional_dependencies',
by_id[hook['id']]['additional_dependencies'],
)
unused_repos.discard((
store.db_repo_name(repo['repo'], deps), repo['rev'],
))
def _gc(store: Store) -> int:
with store.exclusive_lock(), store.connect() as db:
installs_rows = db.execute('SELECT key, path FROM installs').fetchall()
all_installs = dict(installs_rows)
unused_installs = set(all_installs)
manifests_query = 'SELECT repo, rev, manifest FROM manifests'
manifests = {
(repo, rev): json.loads(manifest)
for repo, rev, manifest in db.execute(manifests_query).fetchall()
}
unused_manifests = set(manifests)
configs_rows = db.execute('SELECT path FROM configs').fetchall()
configs = [path for path, in configs_rows]
dead_configs = []
for config_path in configs:
try:
config = load_config(config_path)
except InvalidConfigError:
dead_configs.append(config_path)
continue
else:
for repo in config['repos']:
_mark_used(
config,
repo,
manifests,
unused_manifests,
unused_installs,
)
paths = [(path,) for path in dead_configs]
db.executemany('DELETE FROM configs WHERE path = ?', paths)
db.executemany(
'DELETE FROM repos WHERE repo = ? and ref = ?',
sorted(unused_repos),
)
for k in unused_repos:
rmtree(all_repos[k])
return len(unused_repos)
def gc(store: Store) -> int:
installs, clones = _gc(store)
output.write_line(f'{clones} clone(s) removed.')
output.write_line(f'{installs} installs(s) removed.')
return 0