Skip to content

feat: standardize commit message format - #234

Open
gregggreg wants to merge 6 commits into
marcus:mainfrom
gregggreg:nightshift/commit-normalize-spec
Open

feat: standardize commit message format#234
gregggreg wants to merge 6 commits into
marcus:mainfrom
gregggreg:nightshift/commit-normalize-spec

Conversation

@gregggreg

Copy link
Copy Markdown

Summary

Standardizes the commit message format going forward, without rewriting history.

The convention is derived, not invented. Of the 171 commits on main, 129 (75%)
already use a Conventional Commits prefix. This PR writes that de facto convention
down and enforces it on new commits.

What lands

Piece File
Validator (dependency-free bash, no npm) scripts/commit-msg.sh
Fixture test harness — 43 cases scripts/commit-msg-test.sh
Commit template .gitmessage
One-command setup make install-hooks
Full spec CONTRIBUTING.md
PR-range-only enforcement .github/workflows/ci.yml

Format: <type>[(<scope>)][!]: <description>, subject ≤ 72 chars, blank line before
any body. Types: feat fix docs chore test refactor perf build ci style revert.

Design decisions

History is grandfathered. 61 subjects on main do not pass — merge commits,
Bump version to ..., and otherwise-valid subjects pushed past 72 chars by an
appended (#42) ref. Rewriting them is destructive and out of proportion, so CI
validates only origin/<base>..HEAD. A non-conforming main can never fail CI.

Local hooks are opt-in. Nothing installs automatically; --no-verify bypasses.
CI is the actual gate.

Git-generated messages are exempt — merge, revert, fixup!/squash!/amend!.

Review fixes (iteration 2)

  • Trailing whitespace defeated the rules. feat: add thing. was accepted, then
    git's cleanup — which runs after the hook — stripped the space and landed
    feat: add thing. on the branch, exactly the forbidden form. Lines are now
    right-trimmed as they are read, mirroring git's cleanup.
  • ^[a-z] over-rejected. It refused fix: 2x faster lookups,
    feat: HTTP retry support and feat: OAuth token refresh. The intent was
    "not sentence-cased", so that is now what is checked: a capital is only wrong when
    a lowercase letter immediately follows.
  • Statistics were measured on the branch, not main, so they counted this PR's
    own commits (174/132/113). Corrected to 171/129/110, which reproduce.
  • Also: honor core.commentChar/core.commentString rather than hardcoding #, and
    resolve the hooks dir via git rev-parse so make install-hooks works inside
    linked worktrees, where .git is a file.

Verification

  • 43/43 fixture tests pass under bash 3.2 (macOS stock) and bash 5
  • make build, go vet ./..., go test ./... pass; gofmt -l . empty (no Go changes)
  • Live git commit in a scratch repo: sentence-case and trailing-period-past-space
    rejected with clear diagnostics; acronym and leading-digit subjects accepted;
    commit -v diff correctly stripped at the scissors marker
  • Replaying all 171 main subjects yields exactly the 61 documented grandfathered
    rejections — no surprises

⚠️ Deconfliction needed

Draft #186 (chore: normalize commit messages) covers overlapping scope with a
different design — a .githooks/ directory plus a normalizer that rewrites subjects
in place, rather than a validator that rejects them. It also targets docs-backfill
rather than main and has not been updated since 2026-07-15.

The two should not both land. Key difference worth a maintainer decision: #186
rewrites your message silently; this PR rejects it and tells you why. I favor
rejection — silent rewriting of a commit subject is surprising — but that is a call
for the maintainer, not this PR.

🤖 Generated with Claude Code

Dependency-free bash hook enforcing <type>[(<scope>)][!]: <description>,
derived from existing history rather than invented: 132 of the 174 commits
on main already used a Conventional Commits prefix.

Ships with a 35-case fixture harness, a .gitmessage commit template, and an
extended `make install-hooks` that wires both hooks and the template.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
Adds CONTRIBUTING.md with the format spec, type and scope tables, breaking
change rules, agent trailers, and the grandfathering decision. Cross-links
from README.md and AGENTS.md.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
Validates only origin/<base>..HEAD so grandfathered history never fails CI,
and runs the validator's own fixture suite as a regression check.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
Three defects found in review of the initial validator:

Trailing whitespace defeated the no-trailing-period and length rules.
"feat: add thing. " was accepted, then git's own cleanup — which runs
after the hook — stripped the space and landed "feat: add thing." on the
branch, exactly the form the rule forbids. Each line is now right-trimmed
as it is read, mirroring git's cleanup.

The lowercase rule was implemented as ^[a-z], which rejected "2x faster
lookups", "HTTP retry support" and "OAuth token refresh". The intent was
"not sentence-cased", so that is now what is checked: a capital is only
an error when a lowercase letter follows it immediately. Docs, template
and the reject message were reworded to match.

Also honor core.commentChar / core.commentString instead of hardcoding
"#", and resolve the hooks directory via git rev-parse in install-hooks
so it works inside linked worktrees, where .git is a file.

Verified: 43/43 fixture tests pass under bash 3.2 and bash 5.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
The figures were measured on the feature branch rather than on main, so
they counted this PR's own commits: 174/132/113 instead of 171/129/110.
The 61-rejection figure was correct either way.

These numbers are the stated evidence for deriving the convention from
existing history rather than imposing one, so they need to reproduce.
Verified against `git rev-list --count main` and by replaying every
subject on main through scripts/commit-msg.sh.

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
install-hooks resolved link targets with `git rev-parse --show-toplevel`,
which is the *current* worktree. The hooks directory is shared across all
worktrees, so running `make install-hooks` from a linked worktree pointed
.git/hooks/{pre-commit,commit-msg} into that worktree. Removing the
worktree left both symlinks dangling, and git skips broken hooks silently
-- disabling commit-msg validation and the pre-existing gofmt/vet/build
pre-commit checks with no error.

Resolve the main worktree via --git-common-dir/.. instead, which stays
valid after any linked worktree is removed.

Also document how to reproduce the history statistics, and note that the
129 figure counts any parenthesised scope (a stricter count is 128; the
sole difference is `fix(marcus#19)`, whose scope the validator rejects).

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
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