Skip to content

Commit 21cb27c

Browse files
committed
tools: Add pre-commit check rule for code check and commit check.
Signed-off-by: imliubo <[email protected]>
1 parent ddc5316 commit 21cb27c

File tree

4 files changed

+270
-30
lines changed

4 files changed

+270
-30
lines changed

.pre-commit-config.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
repos:
2+
- repo: local
3+
hooks:
4+
- id: codeformat
5+
name: MicroPython codeformat.py for changed C files
6+
entry: tools/codeformat.py -v -c -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]
14+
- repo: https://github.com/charliermarsh/ruff-pre-commit
15+
rev: v0.1.3
16+
hooks:
17+
- id: ruff
18+
- id: ruff-format

pyproject.toml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
[tool.codespell]
2+
count = ""
3+
ignore-regex = '\b[A-Z]{3}\b'
4+
ignore-words-list = "ans,asend,deques,dout,extint,hsi,iput,mis,numer,shft,technic,ure"
5+
quiet-level = 3
6+
skip = """
7+
*/build*,\
8+
./.git,\
9+
./micropython,\
10+
"""
11+
12+
[tool.ruff]
13+
# Exclude third-party code from linting and formatting
14+
extend-exclude = ["docs", "micropython", "tools", "m5stack/components/lv_bindings"]
15+
line-length = 99
16+
target-version = "py37"
17+
18+
[tool.ruff.lint]
19+
extend-select = ["C9", "PLC"]
20+
ignore = [
21+
"E401",
22+
"E402",
23+
"E722",
24+
"E731",
25+
"E741",
26+
"F401",
27+
"F403",
28+
"F405",
29+
"PLC1901",
30+
]
31+
32+
[tool.ruff.lint.mccabe]
33+
max-complexity = 40
34+
35+
[tool.ruff.lint.per-file-ignores]
36+
# manifest.py files are evaluated with some global names pre-defined
37+
"m5stack/**/manifest*.py" = ["F821"]
38+
"m5stack/boards/**/manifes.py" = ["F821"]
39+
# pyi files are evaluated with some global names pre-defined
40+
"m5stack/**/*.pyi" = ["F821"]
41+
# specific files are evaluated with some global names pre-defined
42+
"m5stack/**/status_bar.py" = ["F821"]
43+
"m5stack/modules/tiny_gui/**.py" = ["F821"]
44+
45+
[tool.ruff.format]
46+
exclude = ["docs/**/*.py", "micropython/**/*.py", "tools/**/*.py"]

tools/codeformat.py

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -42,29 +42,19 @@
4242
"m5stack/components/M5Unified/*.[ch]",
4343
"m5stack/components/M5Unified/*.cpp",
4444
"tools/littlefs/*.[ch]",
45-
# Python
46-
"m5stack/boards/**/*.py",
47-
"m5stack/fs/**/*.py",
48-
"m5stack/libs/**/*.py",
49-
"m5stack/modules/**/*.py",
50-
"tools/*.py",
51-
"tests/**/*.py",
52-
"examples/**/*.py",
45+
]
46+
47+
EXCLUSIONS = [
48+
# micropython upstream files that we don't want to format
49+
"micropython/*",
50+
"tools/*",
5351
]
5452

5553
# Path to repo top-level dir.
5654
TOP = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
5755

5856
UNCRUSTIFY_CFG = os.path.join(TOP, "tools/uncrustify.cfg")
5957

60-
C_EXTS = (
61-
".c",
62-
".cpp",
63-
".h",
64-
".hpp",
65-
)
66-
PY_EXTS = (".py",)
67-
6858

6959
def list_files(paths, exclusions=None, prefix=""):
7060
files = set()
@@ -122,6 +112,11 @@ def main():
122112
cmd_parser.add_argument("-c", action="store_true", help="Format C code only")
123113
cmd_parser.add_argument("-p", action="store_true", help="Format Python code only")
124114
cmd_parser.add_argument("-v", action="store_true", help="Enable verbose output")
115+
cmd_parser.add_argument(
116+
"-f",
117+
action="store_true",
118+
help="Filter files provided on the command line against the default list of files to check.",
119+
)
125120
cmd_parser.add_argument("files", nargs="*", help="Run on specific globs")
126121
args = cmd_parser.parse_args()
127122

@@ -133,19 +128,24 @@ def main():
133128
files = []
134129
if args.files:
135130
files = list_files(args.files)
131+
if args.f:
132+
# Filter against the default list of files. This is a little fiddly
133+
# because we need to apply both the inclusion globs given in PATHS
134+
# as well as the EXCLUSIONS, and use absolute paths
135+
files = set(os.path.abspath(f) for f in files)
136+
all_files = set(list_files(PATHS, EXCLUSIONS, TOP))
137+
if args.v: # In verbose mode, log any files we're skipping
138+
for f in files - all_files:
139+
print("Not checking: {}".format(f))
140+
files = list(files & all_files)
136141
else:
137-
files = list_files(PATHS, None, TOP)
138-
139-
# Extract files matching a specific language.
140-
def lang_files(exts):
141-
for file in files:
142-
if os.path.splitext(file)[1].lower() in exts:
143-
yield file
142+
files = list_files(PATHS, EXCLUSIONS, TOP)
144143

145144
# Run tool on N files at a time (to avoid making the command line too long).
146-
def batch(cmd, files, N=200):
145+
def batch(cmd, N=200):
146+
files_iter = iter(files)
147147
while True:
148-
file_args = list(itertools.islice(files, N))
148+
file_args = list(itertools.islice(files_iter, N))
149149
if not file_args:
150150
break
151151
subprocess.check_call(cmd + file_args)
@@ -155,18 +155,19 @@ def batch(cmd, files, N=200):
155155
command = ["uncrustify", "-c", UNCRUSTIFY_CFG, "-lC", "--no-backup"]
156156
if not args.v:
157157
command.append("-q")
158-
batch(command, lang_files(C_EXTS))
159-
for file in lang_files(C_EXTS):
158+
batch(command)
159+
for file in files:
160160
fixup_c(file)
161161

162-
# Format Python files with black.
162+
# Format Python files with "ruff format" (using config in pyproject.toml).
163163
if format_py:
164-
command = ["black", "--fast", "--line-length=99"]
164+
command = ["ruff", "format"]
165165
if args.v:
166166
command.append("-v")
167167
else:
168168
command.append("-q")
169-
batch(command, lang_files(PY_EXTS))
169+
command.append(".")
170+
subprocess.check_call(command, cwd=TOP)
170171

171172

172173
if __name__ == "__main__":

tools/verifygitlog.py

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

0 commit comments

Comments
 (0)