mirror of
https://github.com/penpot/penpot.git
synced 2026-11-13 05:22:03 +00:00
🔧 Enable checking commit messages from files (#12951)
Adds a new flag --message-file to check-commit AI-assisted-by: deepseek-flash
This commit is contained in:
parent
d829e47a04
commit
ef725c0cc2
@ -13,6 +13,7 @@ Usage:
|
||||
./scripts/check-commit
|
||||
./scripts/check-commit --commit HEAD~1
|
||||
./scripts/check-commit -c abc1234
|
||||
./scripts/check-commit --message-file .git/COMMIT_EDITMSG
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@ -60,15 +61,23 @@ FORBIDDEN_TRAILER_PATTERN = re.compile(r"^\s*Co-authored-by\s*:", re.IGNORECASE)
|
||||
|
||||
URL_PATTERN = re.compile(r"https?://\S+")
|
||||
|
||||
# Git appends the staged diff after this marker for `git commit -v` and
|
||||
# discards everything after it during cleanup.
|
||||
SCISSORS_PATTERN = re.compile(
|
||||
r"^.\s------------------------ >8 ------------------------$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def run_git(args):
|
||||
def run_git(args, input_text=None):
|
||||
"""Run a git command and return (returncode, stdout, stderr)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git"] + args,
|
||||
input=input_text,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
@ -91,6 +100,39 @@ def get_commit_message(commit_ref):
|
||||
return out.rstrip("\n")
|
||||
|
||||
|
||||
def get_message_from_file(path):
|
||||
"""Return the commit message stored in *path*.
|
||||
|
||||
The commit-msg hook receives the message before git removes comment
|
||||
lines, so strip them the same way git does before validating.
|
||||
"""
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
raw = fh.read()
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
print(f"ERROR: could not read message file {path}: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
scissors = SCISSORS_PATTERN.search(raw)
|
||||
if scissors:
|
||||
raw = raw[:scissors.start()]
|
||||
|
||||
rc, out, err = run_git(["stripspace", "--strip-comments"], input_text=raw)
|
||||
if rc != 0:
|
||||
print(
|
||||
f"ERROR: could not clean message file {path}: {err.strip()}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
message = out.rstrip("\n")
|
||||
if not message.strip():
|
||||
print(f"ERROR: message file {path} has no commit message", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Validators
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@ -243,17 +285,26 @@ def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check a commit message against Penpot commit guidelines."
|
||||
)
|
||||
parser.add_argument(
|
||||
source = parser.add_mutually_exclusive_group()
|
||||
source.add_argument(
|
||||
"-c", "--commit",
|
||||
default="HEAD",
|
||||
metavar="REF",
|
||||
help="Commit to check (default: HEAD)",
|
||||
)
|
||||
source.add_argument(
|
||||
"-m", "--message-file",
|
||||
metavar="FILE",
|
||||
help="Read the commit message from FILE instead of a commit",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
commit_ref = args.commit
|
||||
message = get_commit_message(commit_ref)
|
||||
|
||||
print(f"Checking commit {commit_ref} ...\n")
|
||||
if args.message_file is not None:
|
||||
message = get_message_from_file(args.message_file)
|
||||
print(f"Checking commit message in {args.message_file} ...\n")
|
||||
else:
|
||||
commit_ref = args.commit if args.commit is not None else "HEAD"
|
||||
message = get_commit_message(commit_ref)
|
||||
print(f"Checking commit {commit_ref} ...\n")
|
||||
|
||||
validators = [
|
||||
("Regex pattern", check_regex),
|
||||
|
||||
@ -5,15 +5,20 @@ Run with:
|
||||
|
||||
python3 scripts/test_check_commit.py
|
||||
|
||||
Covers the body line-wrapping validator added to enforce the commit body
|
||||
wrap rule documented in .serena/memories/workflow/creating-commits.md.
|
||||
Covers the commit message rules enforced by scripts/check-commit and documented
|
||||
in .serena/memories/workflow/creating-commits.md.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
# Loading scripts/check-commit would otherwise emit scripts/__pycache__/.
|
||||
sys.dont_write_bytecode = True
|
||||
@ -180,5 +185,71 @@ class SubjectRulesRegressionTests(unittest.TestCase):
|
||||
self.assertTrue(ok, error)
|
||||
|
||||
|
||||
class MessageFileTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmpdir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmpdir.cleanup)
|
||||
env = unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"GIT_CONFIG_COUNT": "1",
|
||||
"GIT_CONFIG_KEY_0": "core.commentChar",
|
||||
"GIT_CONFIG_VALUE_0": "#",
|
||||
},
|
||||
)
|
||||
env.start()
|
||||
self.addCleanup(env.stop)
|
||||
|
||||
def write_message(self, text):
|
||||
path = pathlib.Path(self.tmpdir.name) / "COMMIT_EDITMSG"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return str(path)
|
||||
|
||||
def test_reads_message_from_file(self):
|
||||
path = self.write_message(":bug: Fix crash\n\nBody.\n")
|
||||
self.assertEqual(
|
||||
check_commit.get_message_from_file(path),
|
||||
":bug: Fix crash\n\nBody.",
|
||||
)
|
||||
|
||||
def test_ignores_comment_lines(self):
|
||||
path = self.write_message(
|
||||
":bug: Fix crash\n\nBody.\n\n"
|
||||
"# Please enter the commit message for your changes.\n"
|
||||
"# " + "long comment " * 20 + "\n"
|
||||
)
|
||||
message = check_commit.get_message_from_file(path)
|
||||
self.assertEqual(message, ":bug: Fix crash\n\nBody.")
|
||||
ok, error = check_commit.check_body_line_length(message)
|
||||
self.assertTrue(ok, error)
|
||||
|
||||
def test_comment_only_file_exits(self):
|
||||
path = self.write_message("# Nothing but a comment\n")
|
||||
with contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit):
|
||||
check_commit.get_message_from_file(path)
|
||||
|
||||
def test_ignores_diff_after_scissors_line(self):
|
||||
path = self.write_message(
|
||||
":bug: Fix crash\n\nBody.\n\n"
|
||||
"# ------------------------ >8 ------------------------\n"
|
||||
"diff --git a/f b/f\n"
|
||||
"+" + "long diff line " * 10 + "\n"
|
||||
)
|
||||
message = check_commit.get_message_from_file(path)
|
||||
self.assertEqual(message, ":bug: Fix crash\n\nBody.")
|
||||
|
||||
|
||||
class CommandLineTests(unittest.TestCase):
|
||||
def test_empty_message_file_path_is_rejected(self):
|
||||
argv = ["check-commit", "--message-file", ""]
|
||||
with unittest.mock.patch.object(sys, "argv", argv):
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
with contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as raised:
|
||||
check_commit.main()
|
||||
self.assertEqual(raised.exception.code, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user