Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 

Repository files navigation

Contributing

This document defines the commit message conventions used across my repositories. It exists so that my history stays consistent, readable, and useful to me months or years from now.

The single principle underneath everything here:

Write what the diff cannot show.

The diff already records what changed, line by line. A commit message exists to capture what the diff can't: the motivation, the constraints, and the decisions behind the change. Every rule below is downstream of that idea.


Commit message structure

Every non-trivial commit has three conceptual parts:

<header>            ← the intent/outcome (Conventional Commits)
<blank line>
<why>               ← the problem: what's wrong and why it matters
<blank line>
<how>               ← the solution: the approach, and why it's sufficient

The body splits into two conceptual parts — Why and How — but this is a scaffold, not a quota. Real commits range from a bare header to a header plus a full two-part body. How much you write tracks how much the diff can't explain on its own. See When to omit parts of the body.

Wrap the header at ~50 characters and the body at ~72. This keeps git log, git blame, and terminal tooling readable without horizontal scrolling.


Header

The header is the one line that shows up in git log --oneline, changelogs, and release notes. It must earn its place.

The header summarizes the intent or outcome of the commit — not the implementation details. It answers one question:

"What does this commit accomplish?"

Prefer the effect over the mechanism. The mechanism is visible in the diff; the effect usually isn't. A header that describes the outcome also frees the body's Why section to describe the cause instead of restating the goal.

Conventional Commits

Headers must follow the Conventional Commits specification — see their docs for the full grammar and rules. This isn't decoration: these repositories use automated release tooling that parses the type (and the ! / BREAKING CHANGE marker) to choose the version bump and generate changelogs, so a malformed header makes the automation do the wrong thing.

Rules

  • Imperative mood. The header completes the sentence "If applied, this commit will ___." → "add", not "added" or "adds".
  • No trailing period. It's a title, not a sentence.
  • ~50 characters. If you can't summarize it in one line, the commit may be doing too much.

Good and bad headers

✅ feat: allow users to reset their password by email

Describes the outcome a user gets. Imperative, concise, and it doesn't leak implementation (nothing about tokens, tables, or mailers).

❌ feat: add reset_token column and sendmail call

Describes the mechanism — which the diff already shows — and buries the point. A reader scanning the log learns nothing about why this matters.

✅ fix: prevent crash when submitting an empty form

States the effect (no more crash) and the condition. Self-contained.

❌ fix: bug fix

Says nothing. Useless in a changelog, useless in git blame.

✅ chore: enforce consistent indentation across editors

Adds a single .editorconfig file, but the header names what the file achieves (consistent indentation) rather than the file itself. Even a plain file addition can be framed by its outcome — the reason it exists — instead of the artifact.

❌ chore: add .editorconfig

Names the artifact, not what it achieves — the weaker counterpart to the version above. When the file's purpose is the point, reach for the outcome.

❌ chore: update files
❌ chore: changes

Non-descriptive. These are the commits I'll curse when bisecting a regression.

Naming a file is fine when the file is the change. chore: add .gitignore or chore: add tsconfig.json are good — the filename carries the meaning and is greppable. But when the file's purpose is the interesting part (like a linter config whose whole point is enforcing a rule), prefer the outcome.


Body — Part 1: Why

The Why is the most valuable part of the message, because it's the part the diff can never reconstruct. It describes:

  • the problem being solved;
  • why the problem exists;
  • the mechanism or limitation that causes it;
  • why the current behavior is insufficient.

Tense and mood

Write the Why in present tense, indicative mood. Describe the situation as it exists before this commit is applied — because at the moment the commit applies, the problem is still true. The commit is what makes it false.

  • ✅ "The extension stays inactive until a config file exists."
  • ❌ "The extension was inactive." — implies it's already fixed; it isn't, yet.

This mirrors the header's "if applied, this commit will…" framing: the header is the future the commit creates; the Why is the present it starts from.

Reframe "problem" for additive changes

Not every commit fixes a bug. For new features there is no defect — there's a missing capability or unmet need. State the absence as a present-tense fact:

  • ✅ "Users cannot collect multiple items before checkout."
  • ❌ "The app had no cart." — past tense, and frames a non-bug as a bug.

The Why always describes a true, current state of the world, whether that's a defect, a gap, or a structural cost.

Good and bad "Why" sections

✅ The session token is stored in localStorage, so it survives a reload but
   is readable by any script on the page, exposing it to theft via XSS.

Present tense, states the real limitation, and gives concrete stakes (token theft) that explain why it matters. A future reader instantly understands the motivation.

❌ Users complained that login was insecure, so I decided to improve it
   because the old approach felt outdated.

Vague ("insecure", "felt outdated"), past tense, and narrates the author's process ("I decided") instead of describing the problem. No concrete mechanism.

✅ The retry logic uses a fixed delay, so under load every client retries in
   lockstep and amplifies the spike instead of letting it drain.

Names the exact mechanism (fixed delay → synchronized retries) and the concrete consequence (amplified spike). This is information the diff cannot show.

❌ The retries weren't working well and needed to be fixed.

Restates the goal without explaining the cause. "Weren't working well" is past tense and tells me nothing about why.


Body — Part 2: How

The How describes the solution:

  • the chosen approach;
  • how it solves the problem;
  • why this approach is sufficient or correct — the decision the diff can't justify on its own.

Mood

Write the How in imperative mood, exactly like the header — describe the actions the commit introduces into the codebase, not what you did while coding.

  • ✅ "Replace the static lines with interactive text areas."
  • ❌ "Replaced the static lines…" / "I changed the lines…"

The imperative describes what the patch does to the tree, which is timeless. Past tense narrates your afternoon, which no future reader cares about.

Don't narrate the diff — justify it

The weakest How sections re-describe the diff line by line. The strongest ones explain the why behind the how: why this approach, why it's enough, what alternative was rejected. That's the second, subtler kind of "why" — the why of the decision — and it belongs right next to the decision it explains.

Good and bad "How" sections

✅ Move the session token into an httpOnly cookie so client-side scripts can
   no longer read it, closing the XSS theft vector without changing the
   server's session lookup.

Imperative, describes the approach, and the trailing clause justifies why it's sufficient (closes the vector, no server change needed). That justification is the payoff.

❌ Changed a bunch of stuff in the auth module and moved the token around
   until the tests passed.

Past tense, narrates the coding process ("moved… until the tests passed"), and explains nothing a reader couldn't get — less clearly — from the diff.

✅ Add exponential backoff with jitter: spreading retries over a random
   window is enough to break the lockstep, no coordination between clients
   required.

The colon links the action to its justification. "enough to break the lockstep" explains why this approach is sufficient — the question a reader would have about the choice. Nothing here restates the diff.

❌ Change the delay from a constant to Math.random() * base * 2 ** attempt.

Pure diff narration. The exact formula is already in the diff; this sentence adds zero information about why.


When to omit parts of the body

The Why/How structure is a scaffold, not a mandate. Match structure to substance:

Situation Body
Motivation and change are both self-evident No body (header only)
Motivation isn't obvious; approach is Why only
Approach or its rationale is non-obvious Why + How
Large or subtle diff that's hard to read Why + How (How aids navigation)

Guidance:

  • Never write a How that just restates the diff. If the diff makes the approach obvious, drop the How. Write it only when the diff is large or the approach is non-obvious enough that a summary genuinely helps.
  • Almost always write the Why — unless a reasonable reader would never ask "why?". chore: fix typo in README needs no body.
  • A one-sentence rationale stays one sentence. Don't split a single thought into two ceremonial paragraphs. Two paragraphs are for when each part is substantial.

Examples where no body is correct:

chore: add .gitignore
docs: fix broken link in README
chore: bump eslint to 9.2.0

Example where a one-line why earns its place despite a trivial diff:

build: make the release script executable

CI invokes ./scripts/release.sh directly; without the executable bit the
pipeline fails with "permission denied".

The chmod is a one-line diff, but why it's needed isn't visible in that diff — so the Why is worth writing, especially since this commit may live standalone in history where nothing else explains it.


Relationship to Linux kernel conventions

This structure is a formalization of the Linux kernel commit guidelines, which advise:

"Describe your changes in imperative mood… Describe your problem. Whether your patch is a one-line bug fix or 5000 lines of a new driver, there must be an underlying problem that motivated you to do this work."

The kernel philosophy is: first explain why the change is necessary, then explain how the patch addresses it. This document keeps that exact rationale and makes it explicit as two named parts — Why and How — so I don't have to re-derive the shape every time.

Two kernel conventions I deliberately keep:

  • Motivation first. The problem precedes the solution, always.
  • Imperative mood for anything describing what the change does (header + How).

One thing I add on top: Conventional Commits, which the kernel does not use. The kernel encodes intent in prose; my release tooling needs it encoded in the type: prefix. The prose discipline is the kernel's; the machine-readable header is mine.


Complete examples

Bug fix

fix: prevent crash on checkout with an empty cart

Clicking checkout with no items calls the totals reducer with an undefined
accumulator, which throws and unmounts the entire page instead of showing a
validation message.

Guard the reducer against an empty cart and short-circuit to an "empty cart"
state so the user sees a clear message instead of a blank screen.

Why it's good: the Why names the exact mechanism (undefined accumulator → throw) and its blast radius (whole page unmounts). The How states the fix and justifies the fallback (a clear message) — a decision the diff wouldn't explain.

Refactor

refactor: extract duplicated list row into a component

The list template inlines identical markup for every row, so any change to a
row's structure has to be repeated in each copy and drifts out of sync
easily.

Extract the shared markup into a single row component and render the list
from it, so the structure is defined in exactly one place.

Why it's good: a refactor has no bug, so the Why states the structural cost (duplication, drift risk) in present tense. The How explains the outcome (one source of truth), not a play-by-play of the edits.

New feature

feat: let users reorder list items by dragging

The list renders in fixed insertion order, so users who want a different
priority have to delete and re-add items just to rearrange them.

Add drag-and-drop reordering backed by a persisted order field, and keep
up/down buttons as a fallback for keyboard and screen-reader users.

Why it's good: the Why frames the absence of a capability as a present-tense limitation with a concrete consequence (delete-and-re-add churn). The How captures a non-obvious decision — the accessibility fallback — that a reader couldn't infer from the header.

Documentation

docs: add local setup instructions to the README

Why it's good: self-explanatory header, no body needed. The change is the documentation; narrating it would be redundant. Restraint is correct here.

Performance improvement

perf: cache parsed config to avoid re-reading per request

The config file is read and parsed from disk on every incoming request,
adding synchronous I/O to the hot path and dominating latency under load.

Parse the config once at startup and serve the cached object, since the file
cannot change while the process is running.

Why it's good: the code isn't "broken", so the Why states the cost (disk I/O on every request) and the felt symptom (latency under load). The How explains the mechanism and justifies its safety (the file can't change at runtime).


Poor examples, and why they fail

❌ update main.js

No type prefix (breaks release tooling), no intent, not imperative in any meaningful sense. Names a file without saying what changed or why.

❌ feat: changes to the login page

made some improvements to make it better and fixed a few things while I
was in there

Vague header, and a body that narrates the author's session ("while I was in there") instead of a problem and a solution. "A few things" is a red flag that the commit isn't atomic — it should be split.

❌ fix: fixed the bug where it didn't work

Changed the code so now it works.

Past tense throughout, circular ("fixed the bug… now it works" says nothing), and zero mechanism. A future reader learns neither what broke nor why.

❌ feat: add shopping cart

Added a shopping cart. It has items and a total. Created Cart.tsx and
CartItem.tsx and wired them into the checkout page.

The body is pure diff narration — every sentence restates something visible in the file list. It never says why a cart is needed or why the state lives where it does. Compare to a good version, whose Why would state the unmet need ("users can't collect multiple items before checkout") and whose How would justify the design (where cart state persists), not list the filenames.


Quick checklist

Before finalizing a commit, I ask:

  1. Scope — is this exactly one logical change? If not, split it.
  2. Header — does it state the outcome, in imperative mood, with a correct Conventional Commits type?
  3. Why — if the motivation isn't obvious, is it stated in present tense as a real current problem or gap?
  4. How — if the approach is non-obvious, does it justify the decision rather than narrate the diff?
  5. Restraint — have I deleted anything the diff already shows?

The order I write in: scope → why → how → header → trim. The header comes near the end because it's a distillation of the body, and the final trim pass removes everything the diff can already tell the reader.

About

No description, website, or topics provided.

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors