1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import json
import subprocess
import sys
from pathlib import Path
from sys import stderr
from typing import Any
import pygit2
from distgit.hooks import HookResult, PreCommit
"""
Pre-commit hook: run Ruff lint and format checks with machine-readable JSON on stdout.
Uses ``ruff check --output-format=json`` and
``ruff format --check --preview --output-format=json`` (preview is required for JSON on ``format``).
Parses JSON and prints a short summary to stderr on failure.
"""
def _ruff_base() -> list[str]:
return [sys.executable, "-m", "ruff"]
def _parse_diagnostic_list(raw: str) -> list[dict[str, Any]]:
s = raw.strip()
if not s:
return []
data = json.loads(s)
if isinstance(data, list):
return [x for x in data if isinstance(x, dict)]
return []
def _format_location(loc: object) -> str:
if not isinstance(loc, dict):
return "?:?"
row = loc.get("row", "?")
col = loc.get("column", "?")
return f"{row}:{col}"
def _summarize_diagnostics(label: str, rows: list[dict[str, Any]]) -> str:
lines = [f"{label}:"]
for row in rows:
fn = row.get("filename", "?")
msg = row.get("message", row.get("code", "?"))
loc = row.get("location")
lines.append(f" {fn}:{_format_location(loc)}: {msg}")
return "\n".join(lines)
def _run_ruff_json(
args: list[str],
cwd: Path,
) -> tuple[int, list[dict[str, Any]], str]:
proc = subprocess.run(
_ruff_base() + args,
cwd=cwd,
capture_output=True,
text=True,
)
stderr_extra = proc.stderr or ""
try:
rows = _parse_diagnostic_list(proc.stdout)
except json.JSONDecodeError:
return (
1,
[],
f"invalid JSON from ruff (stdout):\n{proc.stdout!r}\nstderr:\n{stderr_extra}",
)
return proc.returncode, rows, stderr_extra
class PreCommitRuff(PreCommit):
def __init__(self, repo: pygit2.Repository, paths: list[str] | None = None):
super().__init__(repo)
self.paths = paths if paths is not None else ["."]
def run(self) -> HookResult:
workdir = self.repo.workdir
if workdir is None:
stderr.write("PreCommitRuff: bare repository has no working tree; skipping ruff.\n")
return HookResult.SUCCESS
root = Path(workdir)
rc, rows, extra = _run_ruff_json(["check", "--output-format=json", *self.paths], root)
if rc != 0:
stderr.write(_summarize_diagnostics("ruff check", rows) + "\n")
if extra.strip():
stderr.write(extra)
return HookResult.FAILURE
rc, rows, extra = _run_ruff_json(
["format", "--check", "--preview", "--output-format=json", *self.paths],
root,
)
if rc != 0:
stderr.write(_summarize_diagnostics("ruff format", rows) + "\n")
if extra.strip():
stderr.write(extra)
return HookResult.FAILURE
return HookResult.SUCCESS