Skip to content

Commit ad0a259

Browse files
jimmodpgeorge
authored andcommitted
tools/verifygitlog.py: Add git commit message checking.
This adds verifygitlog.py from the main repo, adds it to GitHub workflows, and also pre-commit. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <[email protected]>
1 parent d8e163b commit ad0a259

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Check commit message formatting
2+
3+
on: [push, pull_request]
4+
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: '100'
16+
- uses: actions/setup-python@v4
17+
- name: Check commit message formatting
18+
run: source tools/ci.sh && ci_commit_formatting_run

.pre-commit-config.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ repos:
55
name: MicroPython codeformat.py for changed files
66
entry: tools/codeformat.py -v -f
77
language: python
8+
- id: verifygitlog
9+
name: MicroPython git commit message format checker
10+
entry: tools/verifygitlog.py --check-file --ignore-rebase
11+
language: python
12+
verbose: true
13+
stages: [commit-msg]
814
- repo: https://github.com/charliermarsh/ruff-pre-commit
915
rev: v0.1.0
1016
hooks:

tools/ci.sh

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ function ci_code_formatting_run {
1515
tools/codeformat.py -v
1616
}
1717

18+
########################################################################################
19+
# commit formatting
20+
21+
function ci_commit_formatting_run {
22+
git remote add upstream https://github.com/micropython/micropython-lib.git
23+
git fetch --depth=100 upstream master
24+
# If the common ancestor commit hasn't been found, fetch more.
25+
git merge-base upstream/master HEAD || git fetch --unshallow upstream master
26+
# For a PR, upstream/master..HEAD ends with a merge commit into master, exclude that one.
27+
tools/verifygitlog.py -v upstream/master..HEAD --no-merges
28+
}
29+
1830
########################################################################################
1931
# build packages
2032

tools/verifygitlog.py

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
3+
# This is an exact duplicate of verifygitlog.py from the main repo.
4+
5+
import re
6+
import subprocess
7+
import sys
8+
9+
verbosity = 0 # Show what's going on, 0 1 or 2.
10+
suggestions = 1 # Set to 0 to not include lengthy suggestions in error messages.
11+
12+
ignore_prefixes = []
13+
14+
15+
def verbose(*args):
16+
if verbosity:
17+
print(*args)
18+
19+
20+
def very_verbose(*args):
21+
if verbosity > 1:
22+
print(*args)
23+
24+
25+
class ErrorCollection:
26+
# Track errors and warnings as the program runs
27+
def __init__(self):
28+
self.has_errors = False
29+
self.has_warnings = False
30+
self.prefix = ""
31+
32+
def error(self, text):
33+
print("error: {}{}".format(self.prefix, text))
34+
self.has_errors = True
35+
36+
def warning(self, text):
37+
print("warning: {}{}".format(self.prefix, text))
38+
self.has_warnings = True
39+
40+
41+
def git_log(pretty_format, *args):
42+
# Delete pretty argument from user args so it doesn't interfere with what we do.
43+
args = ["git", "log"] + [arg for arg in args if "--pretty" not in args]
44+
args.append("--pretty=format:" + pretty_format)
45+
very_verbose("git_log", *args)
46+
# Generator yielding each output line.
47+
for line in subprocess.Popen(args, stdout=subprocess.PIPE).stdout:
48+
yield line.decode().rstrip("\r\n")
49+
50+
51+
def diagnose_subject_line(subject_line, subject_line_format, err):
52+
err.error("Subject line: " + subject_line)
53+
if not subject_line.endswith("."):
54+
err.error('* must end with "."')
55+
if not re.match(r"^[^!]+: ", subject_line):
56+
err.error('* must start with "path: "')
57+
if re.match(r"^[^!]+: *$", subject_line):
58+
err.error("* must contain a subject after the path.")
59+
m = re.match(r"^[^!]+: ([a-z][^ ]*)", subject_line)
60+
if m:
61+
err.error('* first word of subject ("{}") must be capitalised.'.format(m.group(1)))
62+
if re.match(r"^[^!]+: [^ ]+$", subject_line):
63+
err.error("* subject must contain more than one word.")
64+
err.error("* must match: " + repr(subject_line_format))
65+
err.error('* Example: "py/runtime: Add support for foo to bar."')
66+
67+
68+
def verify(sha, err):
69+
verbose("verify", sha)
70+
err.prefix = "commit " + sha + ": "
71+
72+
# Author and committer email.
73+
for line in git_log("%ae%n%ce", sha, "-n1"):
74+
very_verbose("email", line)
75+
if "noreply" in line:
76+
err.error("Unwanted email address: " + line)
77+
78+
# Message body.
79+
raw_body = list(git_log("%B", sha, "-n1"))
80+
verify_message_body(raw_body, err)
81+
82+
83+
def verify_message_body(raw_body, err):
84+
if not raw_body:
85+
err.error("Message is empty")
86+
return
87+
88+
# Subject line.
89+
subject_line = raw_body[0]
90+
for prefix in ignore_prefixes:
91+
if subject_line.startswith(prefix):
92+
verbose("Skipping ignored commit message")
93+
return
94+
very_verbose("subject_line", subject_line)
95+
subject_line_format = r"^[^!]+: [A-Z]+.+ .+\.$"
96+
if not re.match(subject_line_format, subject_line):
97+
diagnose_subject_line(subject_line, subject_line_format, err)
98+
if len(subject_line) >= 73:
99+
err.error("Subject line must be 72 or fewer characters: " + subject_line)
100+
101+
# Second one divides subject and body.
102+
if len(raw_body) > 1 and raw_body[1]:
103+
err.error("Second message line must be empty: " + raw_body[1])
104+
105+
# Message body lines.
106+
for line in raw_body[2:]:
107+
# Long lines with URLs are exempt from the line length rule.
108+
if len(line) >= 76 and "://" not in line:
109+
err.error("Message lines should be 75 or less characters: " + line)
110+
111+
if not raw_body[-1].startswith("Signed-off-by: ") or "@" not in raw_body[-1]:
112+
err.error('Message must be signed-off. Use "git commit -s".')
113+
114+
115+
def run(args):
116+
verbose("run", *args)
117+
118+
err = ErrorCollection()
119+
120+
if "--check-file" in args:
121+
filename = args[-1]
122+
verbose("checking commit message from", filename)
123+
with open(args[-1]) as f:
124+
# Remove comment lines as well as any empty lines at the end.
125+
lines = [line.rstrip("\r\n") for line in f if not line.startswith("#")]
126+
while not lines[-1]:
127+
lines.pop()
128+
verify_message_body(lines, err)
129+
else: # Normal operation, pass arguments to git log
130+
for sha in git_log("%h", *args):
131+
verify(sha, err)
132+
133+
if err.has_errors or err.has_warnings:
134+
if suggestions:
135+
print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md")
136+
else:
137+
print("ok")
138+
if err.has_errors:
139+
sys.exit(1)
140+
141+
142+
def show_help():
143+
print("usage: verifygitlog.py [-v -n -h --check-file] ...")
144+
print("-v : increase verbosity, can be specified multiple times")
145+
print("-n : do not print multi-line suggestions")
146+
print("-h : print this help message and exit")
147+
print(
148+
"--check-file : Pass a single argument which is a file containing a candidate commit message"
149+
)
150+
print(
151+
"--ignore-rebase : Skip checking commits with git rebase autosquash prefixes or WIP as a prefix"
152+
)
153+
print("... : arguments passed to git log to retrieve commits to verify")
154+
print(" see https://www.git-scm.com/docs/git-log")
155+
print(" passing no arguments at all will verify all commits")
156+
print("examples:")
157+
print("verifygitlog.py -n10 # Check last 10 commits")
158+
print("verifygitlog.py -v master..HEAD # Check commits since master")
159+
160+
161+
if __name__ == "__main__":
162+
args = sys.argv[1:]
163+
verbosity = args.count("-v")
164+
suggestions = args.count("-n") == 0
165+
if "--ignore-rebase" in args:
166+
args.remove("--ignore-rebase")
167+
ignore_prefixes = ["squash!", "fixup!", "amend!", "WIP"]
168+
169+
if "-h" in args:
170+
show_help()
171+
else:
172+
args = [arg for arg in args if arg not in ["-v", "-n", "-h"]]
173+
run(args)

0 commit comments

Comments
 (0)