Commit Message
When to use
Use this when you have a change - a diff, a summary, or a spoken description - and need the commit message a maintainer will thank you for two years later: what changed, why, and what to watch for.
Not for: PR descriptions or changelogs (those summarize many commits and address users, not future maintainers); release notes; or squashing unrelated changes into one message. If the change does several unrelated things, that is a signal to split the commit, not to write one vague message.
Method
- Find the single dominant intent. One change, one reason. If you cannot name it in a phrase, the commit is probably two commits.
- Pick the type honestly.
fix = behavior correction; feat = new capability; refactor = no behavior change; docs, test, chore as they say. Decision point: if the change mixes types, choose the dominant one and mention the rest in the body - never label a refactor fix to look productive.
- Set the scope from the actual area touched (
parser, auth). Omit it rather than invent one when the change is broad.
- Write the subject:
type(scope): imperative summary, 72 chars max, no trailing period.
- Write the body (wrap ~72 cols). Explain WHY: the bug's cause, the constraint, the reasoning. The diff already shows what. Decision point: if the description is too thin to know why, say so plainly rather than inventing a rationale.
- Add footers.
BREAKING CHANGE: for incompatible changes; ticket refs only if given.
Example
Input: "we stopped caching user avatars because stale images showed after profile updates, now they get a cache-busting hash in the url"
Output:
fix(avatars): cache-bust avatar URLs with a content hash
Profile updates left stale avatars on screen because the CDN kept
serving the cached image at the unchanged URL. Appending a content
hash makes every new upload a new URL, so caches stay hot for
unchanged avatars while updates appear immediately.
Old cached URLs remain valid until CDN TTL expiry; no purge needed.
Pitfalls
- Restating the diff. "Changed line 42 in parser.js" is noise; the diff shows that. Record the reason instead.
- Dishonest type. Tagging a behavior change
refactor, or a cleanup fix, so history lies about what happened.
- Inventing metadata. Ticket numbers, benchmarks, or reviewer names that were never provided.
- Silent breaking changes. Shipping an incompatible change with no
BREAKING CHANGE: footer, so downstream discovers it in production.
Output format
type(scope): imperative subject, <=72 chars, no period
Why this change exists: the problem, its cause or the constraint
that forced this design. Wrapped at ~72 columns. Omit if the
subject is genuinely self-explanatory.
BREAKING CHANGE: <what breaks and the migration> (only if true)
Refs: <ticket> (only if provided)
1---2name: commit-message3description: Turn a diff or plain description into a conventional-commit message - honest type and scope, a why-focused body, breaking changes flagged, nothing invented.4---56# Commit Message78## When to use9Use this when you have a change - a diff, a summary, or a spoken description - and need the commit message a maintainer will thank you for two years later: what changed, why, and what to watch for.1011**Not for:** PR descriptions or changelogs (those summarize many commits and address users, not future maintainers); release notes; or squashing unrelated changes into one message. If the change does several unrelated things, that is a signal to split the commit, not to write one vague message.1213## Method141. **Find the single dominant intent.** One change, one reason. If you cannot name it in a phrase, the commit is probably two commits.152. **Pick the type honestly.** `fix` = behavior correction; `feat` = new capability; `refactor` = no behavior change; `docs`, `test`, `chore` as they say. *Decision point:* if the change mixes types, choose the dominant one and mention the rest in the body - never label a refactor `fix` to look productive.163. **Set the scope from the actual area touched** (`parser`, `auth`). Omit it rather than invent one when the change is broad.174. **Write the subject:** `type(scope): imperative summary`, 72 chars max, no trailing period.185. **Write the body (wrap ~72 cols).** Explain WHY: the bug's cause, the constraint, the reasoning. The diff already shows what. *Decision point:* if the description is too thin to know why, say so plainly rather than inventing a rationale.196. **Add footers.** `BREAKING CHANGE:` for incompatible changes; ticket refs only if given.2021## Example22**Input:** "we stopped caching user avatars because stale images showed after profile updates, now they get a cache-busting hash in the url"2324**Output:**25```26fix(avatars): cache-bust avatar URLs with a content hash2728Profile updates left stale avatars on screen because the CDN kept29serving the cached image at the unchanged URL. Appending a content30hash makes every new upload a new URL, so caches stay hot for31unchanged avatars while updates appear immediately.3233Old cached URLs remain valid until CDN TTL expiry; no purge needed.34```3536## Pitfalls37- **Restating the diff.** "Changed line 42 in parser.js" is noise; the diff shows that. Record the reason instead.38- **Dishonest type.** Tagging a behavior change `refactor`, or a cleanup `fix`, so history lies about what happened.39- **Inventing metadata.** Ticket numbers, benchmarks, or reviewer names that were never provided.40- **Silent breaking changes.** Shipping an incompatible change with no `BREAKING CHANGE:` footer, so downstream discovers it in production.4142## Output format43```44type(scope): imperative subject, <=72 chars, no period4546Why this change exists: the problem, its cause or the constraint47that forced this design. Wrapped at ~72 columns. Omit if the48subject is genuinely self-explanatory.4950BREAKING CHANGE: <what breaks and the migration> (only if true)51Refs: <ticket> (only if provided)52```