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 <jim.mussared@gmail.com>
1 parent d8e163b commit ad0a259

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed
Lines changed: 18 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 173 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy