Skip to content

feat: add commit message normalizer and validator - #224

Open
gregggreg wants to merge 1 commit into
marcus:mainfrom
gregggreg:feat/commit-message-normalizer-20260821
Open

feat: add commit message normalizer and validator#224
gregggreg wants to merge 1 commit into
marcus:mainfrom
gregggreg:feat/commit-message-normalizer-20260821

Conversation

@gregggreg

Copy link
Copy Markdown

What

Standardizes commit messages with a dependency-free normalizer + validator, an opt-in local commit-msg hook, and a PR-range CI lint job.

The standard

Derived from this repo's own history, not imposed: of the last 300 subjects, ~132 are already Conventional Commits (feat 75, fix 29, docs 20, chore 4, test 3). So:

<type>(<optional scope>)<optional !>: <lowercase imperative subject>

<body>

<trailers>

Types: feat fix docs style refactor perf test build ci chore revert. Subject ≤ 72 chars, no trailing period, blank line before body, trailers last.

What it does

scripts/normalize-commit-msg.sh auto-fixes only what is mechanically unambiguous:

Before After
Fix: Thing. fix: thing
Feat(api)!: Drop v1 endpoints. feat(api)!: drop v1 endpoints
feat(api) - add thing feat(api): add thing
[feat] add thing feat: add thing
fix:tighten timeout fix: tighten timeout

Plus structure: strips git comment lines, rstrips, collapses blank runs, inserts the blank line after the subject, and inserts the blank line git needs before a trailer block (otherwise Co-Authored-By: / Nightshift-Task: are silently swallowed into the body).

It deliberately does not guess: Update readme has no known type, so it is left alone and scripts/validate-commit-msg.sh rejects it with an explanation and a suggested fix. Same for empty and over-length subjects.

Merge, revert, fixup!, squash! and amend! messages pass through untouched. Normalization is idempotent (covered by tests).

Opt-in, and non-invasive

  • The hook installs only when you run make install-hooks. Nothing changes your git config as a side effect.
  • Installed via the repo's existing .git/hooks symlink convention rather than core.hooksPath. Deliberate: setting core.hooksPath would have silently disabled the existing pre-commit hook.
  • Bypass: NORMALIZE_COMMIT_MSG=0 git commit ... or --no-verify.

CI

.github/workflows/commit-lint.yml validates every commit in the PR range only and reports all failures at once. No existing history is rewritten and no already-merged commit is retroactively judged.

Tests

tests/run-commit-msg-tests.sh — 32 assertions covering every transformation, both skip paths, each rejection reason, trailer preservation, and idempotence. Written before the implementation and run to red first. Wired into make check and CI. Verified under dash as well as bash.

The commit on this branch was itself created by typing a deliberately malformed message (Feat: Add commit message normalizer and validator.) and letting the installed hook fix it.

🤖 Generated with Claude Code

@gregggreg
gregggreg force-pushed the feat/commit-message-normalizer-20260821 branch from 494a8f8 to 6d341f3 Compare August 21, 2026 09:27
@gregggreg

Copy link
Copy Markdown
Author

Addressed the blocking review finding.

Defect: the normalizer deleted every body line starting with #, including in the git commit -m path where git itself preserves it — silent loss of user-authored content. It also hardcoded # and ignored core.commentChar.

Fix: the hook no longer deletes comment lines at all. I verified empirically that git runs its own cleanup after the commit-msg hook, and only strips comments when it should — an editor-authored message loses its template, while git commit -m (cleanup=whitespace) keeps a # body line. So the scripts now carve off only the trailing run of blank/comment lines (the git template, when there is one), leave it completely untouched, and hand it back verbatim for git to deal with. core.commentChar and core.commentString are both respected, defaulting to #.

Verified against a no-hook baseline in a scratch repo:

# baseline (no hook)          # with hook
Fix: Thing.                   fix: thing

#123 explains why.            #123 explains why.

The body now survives exactly as it would without the hook, while the subject is still normalized. With core.commentChar=; the ; template line is stripped by git and #123 is kept.

Tests: wrote the cases red first, including two end-to-end tests that install the hook into a scratch repo and drive real git commit -m and editor-authored commits — the -m one reproduced the exact reported data loss before the fix. Suite is 41 assertions, all passing via make test-commit-msg.

One documented tradeoff: a trailing # line is indistinguishable from a git template, so validation ignores it. That is a missed warning rather than a rejected commit; erring the other way would reject editor-authored commits. Noted in CONTRIBUTING.md and covered by a test.

Standardize commit messages on Conventional Commits with a dependency-free
normalizer, a validator, an opt-in commit-msg hook, and a CI job that lints
every commit in the PR range. No history is rewritten.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
@gregggreg
gregggreg force-pushed the feat/commit-message-normalizer-20260821 branch from 6d341f3 to 6f52f4b Compare August 21, 2026 09:38
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