Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion check-commit-messages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Checks every non-merge commit in a pull request.

## Policy

- Commit titles should be at most 50 characters and must not exceed 60.
- Commit titles must not start with a lowercase letter or end with a period.
They should be at most 50 characters and must not exceed 60.
- Commit titles should use imperative mood. Common past-tense leading words are
rejected with the corresponding imperative form.
- Commit descriptions should wrap at 72 characters. Ordinary text must not
exceed 79 characters.
- `Assisted-by` and `Co-authored-by` trailers must not credit common AI models
Expand Down
2 changes: 1 addition & 1 deletion check-commit-messages/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Check commit messages
description: Check commit title length, description wrapping, and attribution trailers
description: Check commit title style and length, description wrapping, and attribution trailers

inputs:
base_ref:
Expand Down
87 changes: 75 additions & 12 deletions check-commit-messages/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@
r"^(?:Assisted-by|Co-authored-by):\s*\S.*$",
re.IGNORECASE,
)
PAST_TENSE_SUBJECT_STARTS = {
"added": "Add",
"bumped": "Bump",
"changed": "Change",
"converted": "Convert",
"created": "Create",
"disabled": "Disable",
"documented": "Document",
"enabled": "Enable",
"fixed": "Fix",
"implemented": "Implement",
"improved": "Improve",
"migrated": "Migrate",
"moved": "Move",
"refactored": "Refactor",
"removed": "Remove",
"renamed": "Rename",
"replaced": "Replace",
"reverted": "Revert",
"updated": "Update",
"upgraded": "Upgrade",
}
PROHIBITED_ATTRIBUTION_MARKERS = (
"aider",
"claude",
Expand All @@ -46,6 +68,9 @@ class CommitSubjectViolation:
commit: str
subject: str
length: int
starts_with_lowercase: bool
ends_with_period: bool
suggested_imperative: str | None


@dataclass(frozen=True)
Expand Down Expand Up @@ -103,14 +128,31 @@ def get_subject_violations(
commit: str, message: str
) -> list[CommitSubjectViolation]:
lines = message.splitlines()
if not lines or len(lines[0]) <= MAX_SUBJECT_LENGTH:
if not lines:
return []

subject = lines[0]
starts_with_lowercase = subject[:1].islower()
ends_with_period = subject.endswith(".")
words = subject.split(maxsplit=1)
first_word = words[0].rstrip(".,:;").casefold() if words else ""
suggested_imperative = PAST_TENSE_SUBJECT_STARTS.get(first_word)
if (
len(subject) <= MAX_SUBJECT_LENGTH
and not starts_with_lowercase
and not ends_with_period
and suggested_imperative is None
):
return []

return [
CommitSubjectViolation(
commit=commit,
subject=lines[0],
length=len(lines[0]),
subject=subject,
length=len(subject),
starts_with_lowercase=starts_with_lowercase,
ends_with_period=ends_with_period,
suggested_imperative=suggested_imperative,
)
]

Expand Down Expand Up @@ -330,6 +372,15 @@ def strip_commit_comments(message: str) -> str:
def print_subject_violations(
violations: list[CommitSubjectViolation],
) -> None:
print(
"Commit subjects must not start with a lowercase letter or end with "
"a period.",
file=sys.stderr,
)
print(
"Common past-tense leading verbs must use their imperative form.",
file=sys.stderr,
)
print(
f"Commit subjects should be at most {RECOMMENDED_SUBJECT_LENGTH} "
f"characters; this check fails subjects over {MAX_SUBJECT_LENGTH} "
Expand All @@ -340,10 +391,20 @@ def print_subject_violations(

for violation in violations:
print_commit_header(violation.commit, violation.subject)
print(
f" subject: {violation.length} characters",
file=sys.stderr,
)
if violation.starts_with_lowercase:
print(" subject: starts with a lowercase letter", file=sys.stderr)
if violation.ends_with_period:
print(" subject: ends with a period", file=sys.stderr)
if violation.suggested_imperative is not None:
print(
f" subject: use imperative '{violation.suggested_imperative}'",
file=sys.stderr,
)
if violation.length > MAX_SUBJECT_LENGTH:
print(
f" subject: {violation.length} characters",
file=sys.stderr,
)
print(file=sys.stderr)


Expand Down Expand Up @@ -407,7 +468,7 @@ def format_commit_reference(commit: str) -> str:
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Check commit subject length, description wrapping, "
"Check commit subject style and length, description wrapping, "
"and attribution trailers."
)
)
Expand Down Expand Up @@ -453,14 +514,16 @@ def main() -> int:

if args.message_file is not None:
print(
"Checked commit message; subject and description meet length limits "
"and no prohibited attributions were found."
"Checked commit message; subject meets style and length "
"requirements, description meets length limits, and no prohibited "
"attributions were found."
)
else:
noun = "message" if commit_count == 1 else "messages"
print(
f"Checked {commit_count} commit {noun}; subjects and descriptions "
"meet length limits and no prohibited attributions were found."
f"Checked {commit_count} commit {noun}; subjects meet style and "
"length requirements, descriptions meet length limits, and no "
"prohibited attributions were found."
)
return 0

Expand Down
Loading