Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ardupilot
GitHub Repository: Ardupilot/ardupilot
Path: blob/master/Tools/scripts/check_branch_conventions.py
13808 views
1
#!/usr/bin/env python3
2
3
'''
4
Check PR branch commit conventions and markdown linting.
5
6
Validates:
7
- no merge commits
8
- no fixup! commits
9
- commit messages have a well-formed subsystem prefix before ':'
10
- commit subject lines are <= 160 characters
11
- changed markdown files pass markdownlint-cli2
12
13
AP_FLAKE8_CLEAN
14
'''
15
16
from __future__ import annotations
17
18
import argparse
19
import os
20
import re
21
import subprocess
22
import sys
23
24
import build_script_base
25
26
DOCS_URL = "https://ardupilot.org/dev/docs/submitting-patches-back-to-master.html"
27
MAX_SUBJECT_LEN = 160
28
BLACKLISTED_PREFIXES = {
29
"DEBUG",
30
"DRAFT",
31
"TEMP",
32
"TMP",
33
"WIP",
34
}
35
# spaces and quotes allowed to support Revert commits e.g. 'Revert "AP_Periph: ...'
36
PREFIX_RE = re.compile(r'^[-A-Za-z0-9._/" ]+$')
37
38
# Enable colour when attached to a terminal or running under GitHub Actions
39
_colour = sys.stdout.isatty() or os.environ.get('GITHUB_ACTIONS') == 'true'
40
_GREEN = '\033[32m' if _colour else ''
41
_RED = '\033[31m' if _colour else ''
42
_YELLOW = '\033[33m' if _colour else ''
43
_RESET = '\033[0m' if _colour else ''
44
45
PASS = f"{_GREEN}{_RESET}"
46
FAIL = f"{_RED}{_RESET}"
47
SKIP = f"{_YELLOW}~{_RESET}"
48
49
50
class CheckBranchConventions(build_script_base.BuildScriptBase):
51
52
DEFAULT_UPSTREAM = "origin/master"
53
54
def __init__(self, base_branch: str | None = None) -> None:
55
super().__init__()
56
self.base_branch = base_branch
57
58
def progress_prefix(self) -> str:
59
return "CBC"
60
61
def run_git(self, args, show_output=True, source_dir=None):
62
cmd_list = ["git"] + list(args)
63
return self.run_program(
64
"SCB-GIT", cmd_list,
65
show_output=show_output, show_command=False, cwd=source_dir,
66
)
67
68
def check_merge_commits(self) -> bool:
69
merge_commits = self.run_git(
70
["log", f"{self.base_branch}..HEAD", "--merges", "--oneline"],
71
show_output=False,
72
).strip()
73
if merge_commits:
74
print(f"{FAIL} Merge commits are not allowed:")
75
for line in merge_commits.splitlines():
76
print(f" {line}")
77
print(f" See: {DOCS_URL}")
78
return False
79
print(f"{PASS} No merge commits.")
80
return True
81
82
def check_fixup_commits(self, commits: str) -> bool:
83
bad = [line for line in commits.splitlines() if "fixup!" in line]
84
if bad:
85
print(f"{FAIL} fixup! commits are not allowed:")
86
for line in bad:
87
print(f" {line}")
88
print(f" See: {DOCS_URL}")
89
return False
90
print(f"{PASS} No fixup! commits.")
91
return True
92
93
def check_commit_messages(self, commits: str) -> bool:
94
ok = True
95
for line in commits.splitlines():
96
if not line.strip():
97
continue
98
# strip leading hash from --oneline format
99
subject = line.split(" ", 1)[1] if " " in line else line
100
if ":" not in subject:
101
print(f"{FAIL} Missing subsystem prefix: {line}")
102
print(f" Reword to e.g. 'AP_Compass: {subject}'")
103
print(f" See: {DOCS_URL}")
104
ok = False
105
continue
106
prefix = subject.split(":")[0]
107
if prefix.strip().upper() in BLACKLISTED_PREFIXES:
108
print(f"{FAIL} Bad subsystem prefix '{prefix}': {line}")
109
print(f" See: {DOCS_URL}")
110
ok = False
111
if not PREFIX_RE.match(prefix):
112
print(f"{FAIL} Malformed subsystem prefix '{prefix}': {line}")
113
print(" Prefix must contain only letters, digits, '.', '_', '/', '-', spaces, quotes.")
114
print(f" See: {DOCS_URL}")
115
ok = False
116
if ok:
117
print(f"{PASS} All commit messages have well-formed subsystem tags.")
118
return ok
119
120
def check_commit_lengths(self, commits: str) -> bool:
121
ok = True
122
for line in commits.splitlines():
123
if not line.strip():
124
continue
125
if len(line) > MAX_SUBJECT_LEN:
126
print(f"{FAIL} Subject too long ({len(line)} chars, limit {MAX_SUBJECT_LEN}): {line}")
127
ok = False
128
if ok:
129
print(f"{PASS} All commit subject lines within {MAX_SUBJECT_LEN} characters.")
130
return ok
131
132
def check_author_emails(self) -> bool:
133
emails = self.run_git(
134
["log", f"{self.base_branch}..HEAD", "--format=%ae"],
135
show_output=False,
136
).strip()
137
bad = []
138
for email in emails.splitlines():
139
if "example.com" in email:
140
bad.append(email)
141
if bad:
142
print(f"{FAIL} Author email(s) with example.com are not allowed:")
143
for email in bad:
144
print(f" {email}")
145
return False
146
print(f"{PASS} No unacceptable author emails.")
147
return True
148
149
def check_markdown(self) -> bool:
150
changed_md = self.run_git(
151
["diff", "--name-only", "--diff-filter=AM",
152
f"{self.base_branch}...HEAD", "--", "*.md"],
153
show_output=False,
154
).strip()
155
if not changed_md:
156
print(f"{PASS} No markdown files changed.")
157
return True
158
159
try:
160
result = subprocess.run(["markdownlint-cli2"] + changed_md.splitlines())
161
except FileNotFoundError:
162
print(f"{SKIP} markdownlint-cli2 not installed.")
163
return True
164
if result.returncode != 0:
165
print(f"{FAIL} Markdown linting errors found (see above).")
166
return False
167
168
print(f"{PASS} Markdown files pass linting.")
169
return True
170
171
def run(self) -> None:
172
if self.base_branch is None:
173
current = self.find_current_git_branch_or_sha1()
174
self.base_branch = self.find_git_branch_merge_base(current, self.DEFAULT_UPSTREAM)
175
self.progress(f"Using merge base with {self.DEFAULT_UPSTREAM}: {self.base_branch}")
176
177
commits = self.run_git(
178
["log", f"{self.base_branch}..HEAD", "--oneline"],
179
show_output=False,
180
).strip()
181
182
n = len(commits.splitlines()) if commits else 0
183
print(f"\nChecking {n} commit(s) since {self.base_branch}...\n")
184
185
results = [
186
self.check_merge_commits(),
187
self.check_fixup_commits(commits),
188
self.check_commit_messages(commits),
189
self.check_commit_lengths(commits),
190
self.check_author_emails(),
191
self.check_markdown(),
192
]
193
194
failures = results.count(False)
195
print(f"\n{'All checks passed.' if not failures else f'{failures} check(s) failed.'}")
196
sys.exit(0 if all(results) else 1)
197
198
199
if __name__ == "__main__":
200
parser = argparse.ArgumentParser(
201
description="Check PR branch commit conventions and markdown linting",
202
)
203
parser.add_argument(
204
"--base-branch",
205
default=None,
206
help="Upstream base branch or commit to compare against "
207
"(default: merge base of HEAD with origin/master)",
208
)
209
args = parser.parse_args()
210
CheckBranchConventions(args.base_branch).run()
211
212