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 <angus@redyak.com.au>
pull/9498/head
Angus Gratton 2022-08-29 17:30:14 +10:00 zatwierdzone przez Damien George
rodzic bdac8272d8
commit 0e35c4de9b
4 zmienionych plików z 122 dodań i 35 usunięć

Wyświetl plik

@ -0,0 +1,13 @@
repos:
- repo: local
hooks:
- id: codeformat
name: MicroPython codeformat.py for changed files
entry: tools/codeformat.py -v -f
language: python
- id: verifygitlog
name: MicroPython git commit message format checker
entry: tools/verifygitlog.py --check-file --ignore-rebase
language: python
verbose: true
stages: [commit-msg]

Wyświetl plik

@ -69,6 +69,38 @@ the tool the files that changed and it will only reformat those.
v0.71 or v0.72 for MicroPython. Different uncrustify versions produce slightly v0.71 or v0.72 for MicroPython. Different uncrustify versions produce slightly
different formatting, and the configuration file formats are often incompatible. different formatting, and the configuration file formats are often incompatible.
Automatic Pre-Commit Hooks
==========================
To have code formatting and commit message conventions automatically checked
using [pre-commit](https://pre-commit.com/), run the following commands in your
local MicroPython directory:
```
$ pip install pre-commit
$ pre-commit install
$ pre-commit install --hook-type commit-msg
```
pre-commit will now automatically run during `git commit` for both code and
commit message formatting.
The same formatting checks will be run by CI for any Pull Request submitted to
MicroPython. Pre-commit allows you to see any failure more quickly, and in many
cases will automatically correct it in your local working copy.
Tips:
* To skip pre-commit checks on a single commit, use `git commit -n` (for
`--no-verify`).
* To ignore the pre-commit message format check temporarily, start the commit
message subject line with "WIP" (for "Work In Progress").
(It is also possible to install pre-commit using Brew or other sources, see
[the docs](https://pre-commit.com/index.html#install) for details.)
Python code conventions Python code conventions
======================= =======================

Wyświetl plik

@ -151,6 +151,11 @@ def main():
cmd_parser.add_argument("-c", action="store_true", help="Format C code only") cmd_parser.add_argument("-c", action="store_true", help="Format C code only")
cmd_parser.add_argument("-p", action="store_true", help="Format Python code only") cmd_parser.add_argument("-p", action="store_true", help="Format Python code only")
cmd_parser.add_argument("-v", action="store_true", help="Enable verbose output") cmd_parser.add_argument("-v", action="store_true", help="Enable verbose output")
cmd_parser.add_argument(
"-f",
action="store_true",
help="Filter files provided on the command line against the default list of files to check.",
)
cmd_parser.add_argument("files", nargs="*", help="Run on specific globs") cmd_parser.add_argument("files", nargs="*", help="Run on specific globs")
args = cmd_parser.parse_args() args = cmd_parser.parse_args()
@ -162,6 +167,16 @@ def main():
files = [] files = []
if args.files: if args.files:
files = list_files(args.files) files = list_files(args.files)
if args.f:
# Filter against the default list of files. This is a little fiddly
# because we need to apply both the inclusion globs given in PATHS
# as well as the EXCLUSIONS, and use absolute paths
files = set(os.path.abspath(f) for f in files)
all_files = set(list_files(PATHS, EXCLUSIONS, TOP))
if args.v: # In verbose mode, log any files we're skipping
for f in files - all_files:
print("Not checking: {}".format(f))
files = list(files & all_files)
else: else:
files = list_files(PATHS, EXCLUSIONS, TOP) files = list_files(PATHS, EXCLUSIONS, TOP)

Wyświetl plik

@ -7,6 +7,8 @@ import sys
verbosity = 0 # Show what's going on, 0 1 or 2. verbosity = 0 # Show what's going on, 0 1 or 2.
suggestions = 1 # Set to 0 to not include lengthy suggestions in error messages. suggestions = 1 # Set to 0 to not include lengthy suggestions in error messages.
ignore_prefixes = []
def verbose(*args): def verbose(*args):
if verbosity: if verbosity:
@ -18,6 +20,22 @@ def very_verbose(*args):
print(*args) print(*args)
class ErrorCollection:
# Track errors and warnings as the program runs
def __init__(self):
self.has_errors = False
self.has_warnings = False
self.prefix = ""
def error(self, text):
print("error: {}{}".format(self.prefix, text))
self.has_errors = True
def warning(self, text):
print("warning: {}{}".format(self.prefix, text))
self.has_warnings = True
def git_log(pretty_format, *args): def git_log(pretty_format, *args):
# Delete pretty argument from user args so it doesn't interfere with what we do. # Delete pretty argument from user args so it doesn't interfere with what we do.
args = ["git", "log"] + [arg for arg in args if "--pretty" not in args] args = ["git", "log"] + [arg for arg in args if "--pretty" not in args]
@ -28,83 +46,88 @@ def git_log(pretty_format, *args):
yield line.decode().rstrip("\r\n") yield line.decode().rstrip("\r\n")
def verify(sha): def verify(sha, err):
verbose("verify", sha) verbose("verify", sha)
errors = [] err.prefix = "commit " + sha + ": "
warnings = []
def error_text(err):
return "commit " + sha + ": " + err
def error(err):
errors.append(error_text(err))
def warning(err):
warnings.append(error_text(err))
# Author and committer email. # Author and committer email.
for line in git_log("%ae%n%ce", sha, "-n1"): for line in git_log("%ae%n%ce", sha, "-n1"):
very_verbose("email", line) very_verbose("email", line)
if "noreply" in line: if "noreply" in line:
error("Unwanted email address: " + line) err.error("Unwanted email address: " + line)
# Message body. # Message body.
raw_body = list(git_log("%B", sha, "-n1")) raw_body = list(git_log("%B", sha, "-n1"))
verify_message_body(raw_body, err)
def verify_message_body(raw_body, err):
if not raw_body: if not raw_body:
error("Message is empty") err.error("Message is empty")
return errors, warnings return
# Subject line. # Subject line.
subject_line = raw_body[0] subject_line = raw_body[0]
for prefix in ignore_prefixes:
if subject_line.startswith(prefix):
verbose("Skipping ignored commit message")
return
very_verbose("subject_line", subject_line) very_verbose("subject_line", subject_line)
subject_line_format = r"^[^!]+: [A-Z]+.+ .+\.$" subject_line_format = r"^[^!]+: [A-Z]+.+ .+\.$"
if not re.match(subject_line_format, subject_line): if not re.match(subject_line_format, subject_line):
error("Subject line should match " + repr(subject_line_format) + ": " + subject_line) err.error("Subject line should match " + repr(subject_line_format) + ": " + subject_line)
if len(subject_line) >= 73: if len(subject_line) >= 73:
error("Subject line should be 72 or less characters: " + subject_line) err.error("Subject line should be 72 or less characters: " + subject_line)
# Second one divides subject and body. # Second one divides subject and body.
if len(raw_body) > 1 and raw_body[1]: if len(raw_body) > 1 and raw_body[1]:
error("Second message line should be empty: " + raw_body[1]) err.error("Second message line should be empty: " + raw_body[1])
# Message body lines. # Message body lines.
for line in raw_body[2:]: for line in raw_body[2:]:
# Long lines with URLs are exempt from the line length rule. # Long lines with URLs are exempt from the line length rule.
if len(line) >= 76 and "://" not in line: if len(line) >= 76 and "://" not in line:
error("Message lines should be 75 or less characters: " + line) err.error("Message lines should be 75 or less characters: " + line)
if not raw_body[-1].startswith("Signed-off-by: ") or "@" not in raw_body[-1]: if not raw_body[-1].startswith("Signed-off-by: ") or "@" not in raw_body[-1]:
warning("Message should be signed-off") err.warning("Message should be signed-off")
return errors, warnings
def run(args): def run(args):
verbose("run", *args) verbose("run", *args)
has_errors = False
has_warnings = False err = ErrorCollection()
for sha in git_log("%h", *args):
errors, warnings = verify(sha) if "--check-file" in args:
has_errors |= any(errors) filename = args[-1]
has_warnings |= any(warnings) verbose("checking commit message from", filename)
for err in errors: with open(args[-1]) as f:
print("error:", err) lines = [line.rstrip("\r\n") for line in f]
for err in warnings: verify_message_body(lines, err)
print("warning:", err) else: # Normal operation, pass arguments to git log
if has_errors or has_warnings: for sha in git_log("%h", *args):
verify(sha, err)
if err.has_errors or err.has_warnings:
if suggestions: if suggestions:
print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md") print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md")
else: else:
print("ok") print("ok")
if has_errors: if err.has_errors:
sys.exit(1) sys.exit(1)
def show_help(): def show_help():
print("usage: verifygitlog.py [-v -n -h] ...") print("usage: verifygitlog.py [-v -n -h --check-file] ...")
print("-v : increase verbosity, can be speficied multiple times") print("-v : increase verbosity, can be speficied multiple times")
print("-n : do not print multi-line suggestions") print("-n : do not print multi-line suggestions")
print("-h : print this help message and exit") print("-h : print this help message and exit")
print(
"--check-file : Pass a single argument which is a file containing a candidate commit message"
)
print(
"--ignore-rebase : Skip checking commits with git rebase autosquash prefixes or WIP as a prefix"
)
print("... : arguments passed to git log to retrieve commits to verify") print("... : arguments passed to git log to retrieve commits to verify")
print(" see https://www.git-scm.com/docs/git-log") print(" see https://www.git-scm.com/docs/git-log")
print(" passing no arguments at all will verify all commits") print(" passing no arguments at all will verify all commits")
@ -117,6 +140,10 @@ if __name__ == "__main__":
args = sys.argv[1:] args = sys.argv[1:]
verbosity = args.count("-v") verbosity = args.count("-v")
suggestions = args.count("-n") == 0 suggestions = args.count("-n") == 0
if "--ignore-rebase" in args:
args.remove("--ignore-rebase")
ignore_prefixes = ["squash!", "fixup!", "amend!", "WIP"]
if "-h" in args: if "-h" in args:
show_help() show_help()
else: else: