mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
build: switch hooks to prek, check 50/72
prek replaces pre-commit as the hook runner and reads the same config. A new commit-msg hook in .prek/ rejects subjects over 50 characters, subjects without a module prefix, and body lines over 72 characters. Install the hooks with `uv run prek install`, and run them with `just lint`.
This commit is contained in:
6 files changed
+249
-32
No files matched your search
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail the commit if its message breaks the 50/72 rule.
|
||||
|
||||
Subject line <= 50 chars, a blank line, then body wrapped at <= 72 chars.
|
||||
The subject also has to open with a module prefix. Wired in as a
|
||||
prek/pre-commit commit-msg hook so it is enforced mechanically instead of by
|
||||
memory.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
MAX_SUBJECT = 50
|
||||
MAX_BODY = 72
|
||||
|
||||
# `module-prefix: description`, the prefix at the very start of the subject.
|
||||
# A prefix names a file, so it spells the name as the file does: underscores
|
||||
# within a name, `/` between folders, `+` between two modules.
|
||||
SUBJECT_RE = re.compile(r"^[a-z0-9][a-z0-9_+./]*: \S")
|
||||
|
||||
# Subjects git or a rebase writes itself, which the rule is not about.
|
||||
EXEMPT_PREFIXES = ("Merge ", "Revert ", "fixup! ", "squash! ", "amend! ")
|
||||
|
||||
|
||||
def main(argv: Sequence[str]) -> int:
|
||||
if len(argv) != 2:
|
||||
sys.stderr.write(f"Usage: {argv[0]} COMMIT_MSG_FILE\n")
|
||||
return 2
|
||||
|
||||
message_path = Path(argv[1])
|
||||
with message_path.open(encoding="utf-8") as handle:
|
||||
raw_lines = handle.read().splitlines()
|
||||
|
||||
# `git commit -v` appends a diff below a scissors line; ignore it.
|
||||
cut = next(
|
||||
(
|
||||
i
|
||||
for i, line in enumerate(raw_lines)
|
||||
if ">8" in line and line[:1] == "#"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if cut is not None:
|
||||
raw_lines = raw_lines[:cut]
|
||||
|
||||
lines = [line for line in raw_lines if not line.startswith("#")]
|
||||
while lines and not lines[-1].strip():
|
||||
lines.pop()
|
||||
if not lines:
|
||||
return 0
|
||||
|
||||
errors: list[str] = []
|
||||
subject = lines[0]
|
||||
if not subject.startswith(EXEMPT_PREFIXES):
|
||||
if len(subject) > MAX_SUBJECT:
|
||||
errors.append(
|
||||
f"subject is {len(subject)} chars (max {MAX_SUBJECT})"
|
||||
)
|
||||
if not SUBJECT_RE.match(subject):
|
||||
head = subject.split(":")[0]
|
||||
if "," in head:
|
||||
errors.append(
|
||||
f"prefix `{head}` separates modules with a comma; join"
|
||||
" them with `+`"
|
||||
)
|
||||
elif "-" in head:
|
||||
errors.append(
|
||||
f"prefix `{head}` uses a dash; spell the module as its"
|
||||
f" file does (`{head.replace('-', '_')}`)"
|
||||
)
|
||||
else:
|
||||
errors.append(
|
||||
"subject must open with a lowercase module prefix, as in"
|
||||
" `mechanic: shorten quests`"
|
||||
)
|
||||
if len(lines) > 1 and lines[1].strip():
|
||||
errors.append("second line must be blank (subject, blank, body)")
|
||||
for number, line in enumerate(lines[2:], start=3):
|
||||
if len(line) > MAX_BODY:
|
||||
errors.append(
|
||||
f"body line {number} is {len(line)} chars (max {MAX_BODY})"
|
||||
)
|
||||
|
||||
if errors:
|
||||
sys.stderr.write("Commit message breaks the 50/72 rule:\n")
|
||||
for error in errors:
|
||||
sys.stderr.write(f" - {error}\n")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in new issue
Block a user