Skip to content

chore: add a commit message normalizer, hook, and linter - #229

Open
gregggreg wants to merge 2 commits into
marcus:mainfrom
gregggreg:chore/commit-message-normalizer-20260824
Open

chore: add a commit message normalizer, hook, and linter#229
gregggreg wants to merge 2 commits into
marcus:mainfrom
gregggreg:chore/commit-message-normalizer-20260824

Conversation

@gregggreg

Copy link
Copy Markdown

Summary

Standardizes commit messages on Conventional Commits and ships dependency-free tooling to normalize and validate them.

The convention isn't new — 132 of the last 174 commits (76%) already use type(scope): subject. This documents what the history already trends toward and adds tooling so the rest converges.

What's here

docs/commit-messages.md — the written convention: format, the 11 allowed types (derived from types already in use plus the standard set), the 19 scopes already in use, breaking-change syntax, trailer rules, exemptions, and good/bad examples.

scripts/normalize-commit-msg.sh — rewrites a message file in place. Strips comment/scissors scaffolding, trims whitespace, lowercases the type and scope, drops a trailing period (but not an ellipsis), guarantees exactly one blank line after the subject, and warns — never fails — on a subject over 72 characters. Rejects unknown types and free-form subjects with an actionable error. --check validates without writing.

The body and every trailer are preserved byte-for-byte. Only the subject line is ever rewritten, so Nightshift-Task / Nightshift-Ref / Co-Authored-By survive intact — verified by a dedicated test and by this PR's own commit, which was written by the hook.

scripts/lint-commit-msg.sh — the same validation over files or a whole range (--range base..head), for CI.

scripts/commit-msg.sh — the git hook wrapper.

scripts/test-normalize-commit-msg.sh — 23 tests covering each transformation, each exemption, both rejection paths, trailer preservation, --check non-mutation, and idempotency.

Exemptions

Merge …, Revert "…", and fixup!/squash!/amend! messages pass through untouched.

Installation is opt-in

make install-hooks

Extends the existing target, which already symlinked scripts/pre-commit.sh into .git/hooks/; it now installs commit-msg the same way. Idempotent. Nothing installs automatically, so no contributor's workflow changes without them asking. git commit --no-verify skips it.

CI is non-blocking by default

The new commit-messages job lints every commit in the PR range but carries continue-on-error: true. The repo has ~25 pre-existing commits in recent history that predate the convention, and a blocking gate would fail PRs for history nobody is going to rewrite. Removing that one line makes it enforcing — documented in the doc.

Assumptions

  1. Conventional Commits is the target format — chosen because the existing history already overwhelmingly uses it.
  2. Enforcement ships disabled by default — the CI job reports, it doesn't block.
  3. Hook installation is explicit, not automatic — opt-in via make install-hooks.

Verification

  • ./scripts/test-normalize-commit-msg.sh — 23/23 pass
  • make test — 21 packages, 0 failures
  • make lint — golangci-lint, 0 issues
  • bash -n clean on all scripts
  • End-to-end in a scratch clone: a messy FEAT(CLI): add a thing. with a body and Nightshift trailers normalized to feat(cli): add a thing with trailers intact; wibble: nope blocked; a merge message passed through; make install-hooks idempotent across two runs.

Deviation from the plan

The plan specified .githooks/ + core.hooksPath. This repo already installs hooks by symlinking scripts/*.sh into .git/hooks/, and switching to core.hooksPath would silently disable the pre-commit hook for everyone who already ran make install-hooks. Matched the existing convention instead.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift

🤖 Generated with Claude Code

Adopt Conventional Commits as the repository standard, documented in
docs/commit-messages.md and derived from the format the existing history
already trends toward.

Ships three dependency-free shell scripts: an in-place normalizer, a
--check linter reused over a commit range, and a commit-msg hook wrapper.
The normalizer rewrites only the subject line; the body and every trailer
are preserved byte-for-byte so Nightshift-Task/Nightshift-Ref survive.

Enforcement is opt-in: the hook installs via `make install-hooks`, and the
new CI job is non-blocking so pre-existing history does not fail the gate.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
Two defects from review of the initial normalizer:

--check only proved a message was normalizable, so 'Feat(API): Add thing.'
and a body with no blank separator both passed. It now compares the
normalized result against the original and rejects any difference, which
makes the documented Bad examples actually fail and lets the CI job work as
a real gate once continue-on-error is dropped.

The comment-stripping loop dropped every '#' line, deleting body text.
Git runs its cleanup after the commit-msg hook and is flow-aware: it strips
comments for editor-authored messages but keeps them for 'git commit -m'.
Only the comment block preceding the subject and the scissors section are
dropped now, so git decides the rest.

Also fixes CRLF input, which left a stray CR-only line between the subject
and body. Line endings are detected from the subject and preserved.

Tests: 34 pass, covering both defects and CRLF.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
@gregggreg

Copy link
Copy Markdown
Author

Review round 2 — both defects fixed, plus the minor CRLF issue.

(1) --check now enforces normalization, not just normalizability. It compares the normalized result against the original message and rejects any difference. The exact cases called out now fail:

printf "Feat(API): Add thing.\n" | --check  -> exit 1
printf "feat: x\nbody\n"         | --check  -> exit 1

So the Bad examples in docs/commit-messages.md are genuinely caught, and dropping continue-on-error from the commit-messages job would make it a real gate.

(2) The hook no longer deletes body comments. Verified that git runs its cleanup after commit-msg, and that cleanup is flow-aware — it strips comments for editor-authored messages but keeps them for git commit -m (cleanup mode whitespace). So the hook now drops only the comment block preceding the subject and the scissors section, and lets git decide the rest. End-to-end in a scratch clone with the hook installed:

git commit -m "feat: x" -m "#note kept?" -m "Nightshift-Task: t1"
=> feat: x / (blank) / #note kept? / (blank) / Nightshift-Task: t1

The middle paragraph survives, matching the byte-for-byte guarantee. The editor flow still ends up with template comments stripped — by git, as before.

(3) CRLF. Line endings are detected from the subject and preserved, so printf "feat: x\r\n\r\nbody\r\n" no longer emits a stray CR-only line.

Verification: ./scripts/test-normalize-commit-msg.sh 34/34 pass (was 23; new cases cover both defects, the body-comment preservation, and CRLF). make lint 0 issues, make test 21 packages ok / 0 failures. lint-commit-msg.sh --range main..HEAD passes under the stricter check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant