add brain
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lint commit messages against Conventional Commits.
|
||||
|
||||
Input sources (priority order):
|
||||
1) --input file (one commit subject per line)
|
||||
2) stdin lines
|
||||
3) git range via --from-ref/--to-ref
|
||||
|
||||
Use --strict for non-zero exit on violations.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
CONVENTIONAL_RE = re.compile(
|
||||
r"^(feat|fix|perf|refactor|docs|test|build|ci|chore|security|deprecated|remove)"
|
||||
r"(\([a-z0-9._/-]+\))?(!)?:\s+.{1,120}$"
|
||||
)
|
||||
|
||||
|
||||
class CLIError(Exception):
|
||||
"""Raised for expected CLI errors."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class LintReport:
|
||||
total: int
|
||||
valid: int
|
||||
invalid: int
|
||||
violations: List[str]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Validate conventional commit subjects.")
|
||||
parser.add_argument("--input", help="File with commit subjects (one per line).")
|
||||
parser.add_argument("--from-ref", help="Git ref start (exclusive).")
|
||||
parser.add_argument("--to-ref", help="Git ref end (inclusive).")
|
||||
parser.add_argument("--strict", action="store_true", help="Exit non-zero when violations exist.")
|
||||
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def lines_from_file(path: str) -> List[str]:
|
||||
try:
|
||||
return [line.strip() for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()]
|
||||
except Exception as exc:
|
||||
raise CLIError(f"Failed reading --input file: {exc}") from exc
|
||||
|
||||
|
||||
def lines_from_stdin() -> List[str]:
|
||||
if sys.stdin.isatty():
|
||||
return []
|
||||
data = sys.stdin.read()
|
||||
return [line.strip() for line in data.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def lines_from_git(args: argparse.Namespace) -> List[str]:
|
||||
if not args.to_ref:
|
||||
return []
|
||||
range_spec = f"{args.from_ref}..{args.to_ref}" if args.from_ref else args.to_ref
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["git", "log", range_spec, "--pretty=format:%s", "--no-merges"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise CLIError(f"git log failed for range '{range_spec}': {exc.stderr.strip()}") from exc
|
||||
return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def load_lines(args: argparse.Namespace) -> List[str]:
|
||||
if args.input:
|
||||
return lines_from_file(args.input)
|
||||
stdin_lines = lines_from_stdin()
|
||||
if stdin_lines:
|
||||
return stdin_lines
|
||||
git_lines = lines_from_git(args)
|
||||
if git_lines:
|
||||
return git_lines
|
||||
raise CLIError("No commit input found. Use --input, stdin, or --to-ref.")
|
||||
|
||||
|
||||
def lint(lines: List[str]) -> LintReport:
|
||||
violations: List[str] = []
|
||||
valid = 0
|
||||
|
||||
for idx, line in enumerate(lines, start=1):
|
||||
if CONVENTIONAL_RE.match(line):
|
||||
valid += 1
|
||||
continue
|
||||
violations.append(f"line {idx}: {line}")
|
||||
|
||||
return LintReport(total=len(lines), valid=valid, invalid=len(violations), violations=violations)
|
||||
|
||||
|
||||
def format_text(report: LintReport) -> str:
|
||||
lines = [
|
||||
"Conventional commit lint report",
|
||||
f"- total: {report.total}",
|
||||
f"- valid: {report.valid}",
|
||||
f"- invalid: {report.invalid}",
|
||||
]
|
||||
if report.violations:
|
||||
lines.append("Violations:")
|
||||
lines.extend([f"- {v}" for v in report.violations])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
lines = load_lines(args)
|
||||
report = lint(lines)
|
||||
|
||||
if args.format == "json":
|
||||
print(json.dumps(asdict(report), indent=2))
|
||||
else:
|
||||
print(format_text(report))
|
||||
|
||||
if args.strict and report.invalid > 0:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except CLIError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
Reference in New Issue
Block a user