blob: caaa0aa4367778d172908c9286a608cb7cd1bd87 [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Checks for Git commit messages."""
load("./common.star", "cipd_platform_name", "get_fuchsia_dir", "os_exec")
_FOOTERS = [
# keep-sorted start
"Build-Only",
"Bypass-Vuln-Scanning",
"Change-Id",
"Cq-Do-Not-Cancel-Tryjobs",
"Cq-Exclude-Trybots",
"Cq-Include-Trybots",
"Depends-On",
"Depends-on",
"Multiply",
"Run-All-Tests",
"Want-Review",
# keep-sorted end
]
_FOOTER_RE = r"^(" + "|".join(_FOOTERS) + r")\s*:"
def _check_commit_message(ctx):
"""Lints Git commit messages on the current branch against style guidelines."""
commits = ctx.scm.commits()
if not commits:
return
fuchsia_dir = get_fuchsia_dir(ctx)
python_bin = "%s/prebuilt/third_party/python3/%s/bin/python3" % (
fuchsia_dir,
cipd_platform_name(ctx),
)
checker_path = "%s/scripts/shac/commit_msg_checker.py" % fuchsia_dir
for commit in commits:
msg_file = ctx.io.tempfile(commit.message, name = "commit_message.txt")
res = os_exec(
ctx,
[python_bin, checker_path, "--message-file", msg_file, "--json"],
raise_on_failure = False,
).wait()
if not res.stdout or not res.stdout.strip().startswith("["):
continue
findings = json.decode(res.stdout)
for finding in findings:
msg = finding.get("message", "")
kwargs = {
"level": finding.get("level", "warning"),
"commit": commit,
}
if finding.get("line"):
kwargs["line"] = finding["line"]
ctx.emit.commit_message_finding(message = msg, **kwargs)
def _commit_msg_footers(ctx):
"""Enforces that machine-parsed Git footers are in the last paragraph."""
for commit in ctx.scm.commits():
lines = commit.message.rstrip().splitlines()
# Find the start of the last non-empty paragraph. If there are no blank
# lines, the entire message is the subject paragraph (not a footer
# block), so check all lines.
last_para_start = len(lines)
for i in range(len(lines) - 1, -1, -1):
if not lines[i].strip():
last_para_start = i + 1
break
# Skip commits that have already been submitted by Gerrit (e.g. in
# postsubmit CI or on a detached HEAD at origin/main) or generated by
# rollers (which place Multiply: above the footer block).
if any([
line.startswith("Reviewed-on:") or line.startswith("Roller-URL:")
for line in lines[last_para_start:]
]):
continue
for i in range(last_para_start):
match = ctx.re.match(_FOOTER_RE, lines[i])
if match:
ctx.emit.commit_message_finding(
level = "error",
commit = commit,
line = i + 1,
message = 'Footer "%s" must be in the last paragraph of the commit message to take effect.' % match.groups[1],
)
def register_commit_msg_checks():
shac.register_check(shac.check(_check_commit_message, name = "commit_msg"))
shac.register_check(_commit_msg_footers)