Skip to content

Commit 0e35c4d

Browse files
projectgusdpgeorge
authored andcommitted
tools: Add pre-commit support.
Tweak the existing codeformat.py and verifygitlog.py to allow them to be easily called by pre-commit. (This turned out to be easier than using any existing pre-commit hooks, without making subtle changes in the formatting.) This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <[email protected]>
1 parent bdac827 commit 0e35c4d

File tree

4 files changed

+122
-35
lines changed

4 files changed

+122
-35
lines changed

.pre-commit-config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
repos:
2+
- repo: local
3+
hooks:
4+
- id: codeformat
5+
name: MicroPython codeformat.py for changed files
6+
entry: tools/codeformat.py -v -f
7+
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]

CODECONVENTIONS.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,38 @@ the tool the files that changed and it will only reformat those.
6969
v0.71 or v0.72 for MicroPython. Different uncrustify versions produce slightly
7070
different formatting, and the configuration file formats are often incompatible.
7171

72+
Automatic Pre-Commit Hooks
73+
==========================
74+
75+
To have code formatting and commit message conventions automatically checked
76+
using [pre-commit](https://pre-commit.com/), run the following commands in your
77+
local MicroPython directory:
78+
79+
```
80+
$ pip install pre-commit
81+
82+
$ pre-commit install
83+
84+
$ pre-commit install --hook-type commit-msg
85+
```
86+
87+
pre-commit will now automatically run during `git commit` for both code and
88+
commit message formatting.
89+
90+
The same formatting checks will be run by CI for any Pull Request submitted to
91+
MicroPython. Pre-commit allows you to see any failure more quickly, and in many
92+
cases will automatically correct it in your local working copy.
93+
94+
Tips:
95+
96+
* To skip pre-commit checks on a single commit, use `git commit -n` (for
97+
`--no-verify`).
98+
* To ignore the pre-commit message format check temporarily, start the commit
99+
message subject line with "WIP" (for "Work In Progress").
100+
101+
(It is also possible to install pre-commit using Brew or other sources, see
102+
[the docs](https://pre-commit.com/index.html#install) for details.)
103+
72104
Python code conventions
73105
=======================
74106

tools/codeformat.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ def main():
151151
cmd_parser.add_argument("-c", action="store_true", help="Format C code only")
152152
cmd_parser.add_argument("-p", action="store_true", help="Format Python code only")
153153
cmd_parser.add_argument("-v", action="store_true", help="Enable verbose output")
154+
cmd_parser.add_argument(
155+
"-f",
156+
action="store_true",
157+
help="Filter files provided on the command line against the default list of files to check.",
158+
)
154159
cmd_parser.add_argument("files", nargs="*", help="Run on specific globs")
155160
args = cmd_parser.parse_args()
156161

@@ -162,6 +167,16 @@ def main():
162167
files = []
163168
if args.files:
164169
files = list_files(args.files)
170+
if args.f:
171+
# Filter against the default list of files. This is a little fiddly
172+
# because we need to apply both the inclusion globs given in PATHS
173+
# as well as the EXCLUSIONS, and use absolute paths
174+
files = set(os.path.abspath(f) for f in files)
175+
all_files = set(list_files(PATHS, EXCLUSIONS, TOP))
176+
if args.v: # In verbose mode, log any files we're skipping
177+
for f in files - all_files:
178+
print("Not checking: {}".format(f))
179+
files = list(files & all_files)
165180
else:
166181
files = list_files(PATHS, EXCLUSIONS, TOP)
167182

tools/verifygitlog.py

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
verbosity = 0 # Show what's going on, 0 1 or 2.
88
suggestions = 1 # Set to 0 to not include lengthy suggestions in error messages.
99

10+
ignore_prefixes = []
11+
1012

1113
def verbose(*args):
1214
if verbosity:
@@ -18,6 +20,22 @@ def very_verbose(*args):
1820
print(*args)
1921

2022

23+
class ErrorCollection:
24+
# Track errors and warnings as the program runs
25+
def __init__(self):
26+
self.has_errors = False
27+
self.has_warnings = False
28+
self.prefix = ""
29+
30+
def error(self, text):
31+
print("error: {}{}".format(self.prefix, text))
32+
self.has_errors = True
33+
34+
def warning(self, text):
35+
print("warning: {}{}".format(self.prefix, text))
36+
self.has_warnings = True
37+
38+
2139
def git_log(pretty_format, *args):
2240
# Delete pretty argument from user args so it doesn't interfere with what we do.
2341
args = ["git", "log"] + [arg for arg in args if "--pretty" not in args]
@@ -28,83 +46,88 @@ def git_log(pretty_format, *args):
2846
yield line.decode().rstrip("\r\n")
2947

3048

31-
def verify(sha):
49+
def verify(sha, err):
3250
verbose("verify", sha)
33-
errors = []
34-
warnings = []
35-
36-
def error_text(err):
37-
return "commit " + sha + ": " + err
38-
39-
def error(err):
40-
errors.append(error_text(err))
41-
42-
def warning(err):
43-
warnings.append(error_text(err))
51+
err.prefix = "commit " + sha + ": "
4452

4553
# Author and committer email.
4654
for line in git_log("%ae%n%ce", sha, "-n1"):
4755
very_verbose("email", line)
4856
if "noreply" in line:
49-
error("Unwanted email address: " + line)
57+
err.error("Unwanted email address: " + line)
5058

5159
# Message body.
5260
raw_body = list(git_log("%B", sha, "-n1"))
61+
verify_message_body(raw_body, err)
62+
63+
64+
def verify_message_body(raw_body, err):
5365
if not raw_body:
54-
error("Message is empty")
55-
return errors, warnings
66+
err.error("Message is empty")
67+
return
5668

5769
# Subject line.
5870
subject_line = raw_body[0]
71+
for prefix in ignore_prefixes:
72+
if subject_line.startswith(prefix):
73+
verbose("Skipping ignored commit message")
74+
return
5975
very_verbose("subject_line", subject_line)
6076
subject_line_format = r"^[^!]+: [A-Z]+.+ .+\.$"
6177
if not re.match(subject_line_format, subject_line):
62-
error("Subject line should match " + repr(subject_line_format) + ": " + subject_line)
78+
err.error("Subject line should match " + repr(subject_line_format) + ": " + subject_line)
6379
if len(subject_line) >= 73:
64-
error("Subject line should be 72 or less characters: " + subject_line)
80+
err.error("Subject line should be 72 or less characters: " + subject_line)
6581

6682
# Second one divides subject and body.
6783
if len(raw_body) > 1 and raw_body[1]:
68-
error("Second message line should be empty: " + raw_body[1])
84+
err.error("Second message line should be empty: " + raw_body[1])
6985

7086
# Message body lines.
7187
for line in raw_body[2:]:
7288
# Long lines with URLs are exempt from the line length rule.
7389
if len(line) >= 76 and "://" not in line:
74-
error("Message lines should be 75 or less characters: " + line)
90+
err.error("Message lines should be 75 or less characters: " + line)
7591

7692
if not raw_body[-1].startswith("Signed-off-by: ") or "@" not in raw_body[-1]:
77-
warning("Message should be signed-off")
78-
79-
return errors, warnings
93+
err.warning("Message should be signed-off")
8094

8195

8296
def run(args):
8397
verbose("run", *args)
84-
has_errors = False
85-
has_warnings = False
86-
for sha in git_log("%h", *args):
87-
errors, warnings = verify(sha)
88-
has_errors |= any(errors)
89-
has_warnings |= any(warnings)
90-
for err in errors:
91-
print("error:", err)
92-
for err in warnings:
93-
print("warning:", err)
94-
if has_errors or has_warnings:
98+
99+
err = ErrorCollection()
100+
101+
if "--check-file" in args:
102+
filename = args[-1]
103+
verbose("checking commit message from", filename)
104+
with open(args[-1]) as f:
105+
lines = [line.rstrip("\r\n") for line in f]
106+
verify_message_body(lines, err)
107+
else: # Normal operation, pass arguments to git log
108+
for sha in git_log("%h", *args):
109+
verify(sha, err)
110+
111+
if err.has_errors or err.has_warnings:
95112
if suggestions:
96113
print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md")
97114
else:
98115
print("ok")
99-
if has_errors:
116+
if err.has_errors:
100117
sys.exit(1)
101118

102119

103120
def show_help():
104-
print("usage: verifygitlog.py [-v -n -h] ...")
121+
print("usage: verifygitlog.py [-v -n -h --check-file] ...")
105122
print("-v : increase verbosity, can be speficied multiple times")
106123
print("-n : do not print multi-line suggestions")
107124
print("-h : print this help message and exit")
125+
print(
126+
"--check-file : Pass a single argument which is a file containing a candidate commit message"
127+
)
128+
print(
129+
"--ignore-rebase : Skip checking commits with git rebase autosquash prefixes or WIP as a prefix"
130+
)
108131
print("... : arguments passed to git log to retrieve commits to verify")
109132
print(" see https://www.git-scm.com/docs/git-log")
110133
print(" passing no arguments at all will verify all commits")
@@ -117,6 +140,10 @@ def show_help():
117140
args = sys.argv[1:]
118141
verbosity = args.count("-v")
119142
suggestions = args.count("-n") == 0
143+
if "--ignore-rebase" in args:
144+
args.remove("--ignore-rebase")
145+
ignore_prefixes = ["squash!", "fixup!", "amend!", "WIP"]
146+
120147
if "-h" in args:
121148
show_help()
122149
else:

0 commit comments

Comments
 (0)