Skip to content

Commit e68b0d6

Browse files
authored
Merge branch 'master' into feat/gpu-resident-nonts
2 parents 359a0d0 + 1ca524d commit e68b0d6

146 files changed

Lines changed: 10482 additions & 1326 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
"""Auto-fix a single commit message to satisfy commitlint (config-conventional +
3+
this repo's commitlint.config.js).
4+
5+
Designed to be driven by `git filter-branch --msg-filter` so it rewrites EVERY
6+
commit in a PR range in place, preserving each commit's diff and all merge
7+
topology — no destructive squash. Reads the raw message on stdin, writes the
8+
fixed message on stdout. It is deliberately conservative: a message that already
9+
passes is emitted byte-for-byte unchanged, so valid history is never churned.
10+
11+
Rules handled (all commitlint ERROR-level, i.e. the ones that actually fail CI):
12+
* type-enum / type-case — unknown or mis-cased type is remapped to an allowed,
13+
lower-case type (best-effort inference from wording).
14+
* subject-case — a Sentence/Start/Pascal/UPPER subject is lower-cased
15+
at its first character (the standard commitlint fix).
16+
* subject-full-stop — a single trailing '.' is stripped.
17+
* header-max-length (100)— an over-long header is trimmed at a word boundary and
18+
the trimmed remainder is preserved as a body line, so
19+
no information is lost.
20+
* non-conventional header— a header with no valid type gets one inferred and
21+
prefixed.
22+
23+
Merge/revert commits and commits already co-authored by github-actions[bot] are
24+
left untouched (commitlint ignores them via commitlint.config.js).
25+
26+
Kept in sync with commitlint.config.js: update ALLOWED_TYPES and HEADER_MAX_LEN
27+
here whenever that config changes.
28+
"""
29+
import re
30+
import sys
31+
32+
# Must match commitlint.config.js type-enum (config-conventional + 'deps').
33+
ALLOWED_TYPES = {
34+
"feat", "fix", "docs", "refactor", "perf", "test",
35+
"chore", "ci", "style", "revert", "deps",
36+
}
37+
# config-conventional header-max-length (not overridden in commitlint.config.js).
38+
HEADER_MAX_LEN = 100
39+
40+
_HEADER_RE = re.compile(r"^(?P<type>\w+)(?P<scope>\([^)]*\))?(?P<bang>!)?:\s*(?P<subject>.*)$")
41+
42+
43+
def _infer_type(text: str) -> str:
44+
"""Best-effort conventional type from free-form wording."""
45+
t = text.strip().lower()
46+
if re.match(r"^(add|implement|create|introduce|support)\b", t):
47+
return "feat"
48+
if re.match(r"^(fix|correct|resolve|patch|prevent|stop|guard)\b", t):
49+
return "fix"
50+
if re.match(r"^(document|docs?\b|update docs)", t):
51+
return "docs"
52+
if re.match(r"^refactor\b", t):
53+
return "refactor"
54+
if re.match(r"^(test|cover)\b", t):
55+
return "test"
56+
if re.match(r"^(bump|upgrade|update .*version|dependency|deps)\b", t):
57+
return "deps"
58+
if re.match(r"^(perf|optimi[sz]e|speed up)\b", t):
59+
return "perf"
60+
return "chore"
61+
62+
63+
def _lower_first(s: str) -> str:
64+
"""Lower-case the first character so the subject cannot be classified as
65+
sentence-/start-/pascal-/upper-case (the cases config-conventional's
66+
subject-case rejects).
67+
68+
We deliberately lower-case even a leading acronym/PascalCase class name
69+
('DeepFilterNet' -> 'deepFilterNet', 'DFA' -> 'dFA'): commitlint keys the
70+
check off the leading character, so this is the reliable automated fix. It
71+
is slightly less pretty than a human rephrase, but an auto-fixer's job is to
72+
make CI pass deterministically — a maintainer can always reword afterward.
73+
Interior words (e.g. a mid-subject 'BN'/'GPU') are untouched.
74+
"""
75+
if not s:
76+
return s
77+
if s[0].isupper():
78+
return s[0].lower() + s[1:]
79+
return s
80+
81+
82+
def _shorten_header(prefix: str, subject: str):
83+
"""Trim `prefix+subject` to <= HEADER_MAX_LEN at a word boundary.
84+
85+
Returns (header, overflow_or_None). Any trimmed words are returned as overflow
86+
so the caller can preserve them in the body.
87+
"""
88+
budget = HEADER_MAX_LEN - len(prefix)
89+
if budget <= 0:
90+
# Pathological: prefix alone already too long. Hard-truncate.
91+
return (prefix + subject)[:HEADER_MAX_LEN], None
92+
if len(subject) <= budget:
93+
return prefix + subject, None
94+
words = subject.split(" ")
95+
kept, length = [], 0
96+
for w in words:
97+
add = (1 if kept else 0) + len(w)
98+
if length + add <= budget:
99+
kept.append(w)
100+
length += add
101+
else:
102+
break
103+
if not kept:
104+
# A single word longer than the budget — hard split it.
105+
return prefix + subject[:budget], subject[budget:]
106+
rest = " ".join(words[len(kept):]).strip()
107+
header = (prefix + " ".join(kept)).rstrip()
108+
overflow = ("…" + rest) if rest else None
109+
return header, overflow
110+
111+
112+
def fix_message(msg: str) -> str:
113+
lines = msg.split("\n")
114+
if not lines:
115+
return msg
116+
header = lines[0]
117+
118+
# Leave commitlint-ignored commits untouched.
119+
if header.startswith("Merge ") or header.startswith("Revert "):
120+
return msg
121+
if "Co-Authored-By: github-actions[bot]" in msg or "Co-authored-by: github-actions[bot]" in msg:
122+
return msg
123+
124+
body_lines = lines[1:]
125+
overflow = None
126+
127+
m = _HEADER_RE.match(header)
128+
if m:
129+
type_ = m.group("type")
130+
scope = m.group("scope") or ""
131+
bang = m.group("bang") or ""
132+
subject = m.group("subject")
133+
134+
type_l = type_.lower()
135+
if type_l not in ALLOWED_TYPES:
136+
type_l = _infer_type(subject)
137+
scope = scope.lower()
138+
# Drop a scope made redundant by a type remap, e.g. build(deps) -> deps.
139+
if scope.strip("()") == type_l:
140+
scope = ""
141+
subject = _lower_first(subject.rstrip())
142+
if subject.endswith(".") and not subject.endswith("..."):
143+
subject = subject[:-1]
144+
145+
prefix = f"{type_l}{scope}{bang}: "
146+
header, overflow = _shorten_header(prefix, subject)
147+
else:
148+
# Non-conventional header: infer a type and prefix it.
149+
subject = _lower_first(header.strip())
150+
prefix = f"{_infer_type(header)}: "
151+
header, overflow = _shorten_header(prefix, subject)
152+
153+
if overflow:
154+
# Preserve trimmed words as a body line with a leading blank (also
155+
# satisfies body-leading-blank when there was no body before).
156+
if body_lines and body_lines[0] != "":
157+
body_lines = ["", overflow] + body_lines
158+
elif body_lines:
159+
body_lines = [body_lines[0], overflow] + body_lines[1:]
160+
else:
161+
body_lines = ["", overflow]
162+
163+
return "\n".join([header] + body_lines)
164+
165+
166+
def main() -> int:
167+
raw = sys.stdin.buffer.read().decode("utf-8", errors="replace")
168+
sys.stdout.buffer.write(fix_message(raw).encode("utf-8"))
169+
return 0
170+
171+
172+
if __name__ == "__main__":
173+
raise SystemExit(main())

0 commit comments

Comments
 (0)