{"post":{"slug":"how-to-write-good-commit-messages-2026","title":"How to write good commit messages in 2026: the standard that sticks","excerpt":"A good commit message is a letter to the next engineer who has to understand why this code exists. This 2026 guide gives the rules and the template.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span> git history is a log of decisions, not just a log of changes. When a bug appears six months from now and someone runs `git log --oneline`, they are not looking for a list of files changed — they are looking for the reasoning behind each change. A good commit message takes 60 seconds to write and saves hours of archaeological work. A bad one is permanently useless.\n\n## What changed in 2026\n\n- **Conventional Commits is the industry default.** Libraries, CLI tools, and AI commit message generators all default to `feat:`, `fix:`, `docs:`, `chore:` prefixes. It is not an opinion anymore — it is the standard.\n- **Automated changelogs from commits are mainstream.** Tools like `release-please`, `semantic-release`, and `changesets` generate changelogs and bump semver versions directly from commit messages. Your commit messages are now release notes.\n- **AI commit message generation is good enough to use as a draft.** VS Code, JetBrains, and CLI tools can suggest a message from the diff — but they produce \"what,\" not \"why.\" Always add the why yourself.\n- **Co-author and signed commits are common.** AI pair programming tools add `Co-Authored-By:` trailers; regulated industries require GPG-signed commits.\n\n## The anatomy of a commit message\n\n```\n<type>(<scope>): <subject>       ← subject line, max 72 chars\n                                  ← blank line (required)\n<body>                            ← why, context, tradeoffs (optional)\n                                  ← blank line\n<footer>                          ← issue refs, breaking changes (optional)\n```\n\n### The subject line rules\n\n1. **Use a type prefix** — `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `ci`.\n2. **Imperative mood** — \"Add\", \"Fix\", \"Remove\", not \"Added\", \"Fixes\", \"Removing\".\n3. **No full stop at the end.**\n4. **Maximum 72 characters** — many tools truncate at 72; keep it under that.\n5. **Lowercase after the colon** (Conventional Commits convention).\n\n```\nfeat(auth): add rate limiting to login endpoint\nfix(payments): prevent duplicate charge on network timeout\nrefactor(users): extract email validation into shared util\ndocs(api): add pagination examples to REST guide\nchore(deps): bump express from 4.18.3 to 4.19.2\n```\n\n### The body\n\nThe body is optional but valuable for anything non-obvious. Answer:\n- **Why** was this change made?\n- **What alternative was considered** and why was it rejected?\n- **What is the tradeoff** or known limitation?\n\n```\nfeat(billing): switch to idempotency keys for Stripe charges\n\nPreviously we retried failed charges by re-submitting the request.\nThis caused duplicate charges when the network failed after Stripe\nhad processed the payment but before we received the 200 response.\n\nIdempotency keys (scoped to order ID + attempt number) let Stripe\nreturn the same result for duplicate requests within 24 hours,\neliminating the duplicate-charge race condition entirely.\n```\n\n### The footer\n\n```\nfeat(auth): add TOTP two-factor authentication\n\nImplements RFC 6238 TOTP using the otpauth library.\nBackup codes are hashed with bcrypt before storage.\n\nBREAKING CHANGE: /auth/login now returns 202 Accepted with a\n  session_token when 2FA is enabled, instead of 200 with a full JWT.\n  Clients must handle the 2FA challenge flow.\n\nCloses #847\nRefs JIRA-1234\nCo-Authored-By: Alice <alice@example.com>\n```\n\n## Conventional Commits type reference\n\n| Type | When to use |\n| --- | --- |\n| `feat` | New feature for the user |\n| `fix` | Bug fix for the user |\n| `docs` | Documentation only |\n| `style` | Formatting, whitespace — no logic change |\n| `refactor` | Code restructure — no feature or fix |\n| `test` | Adding or correcting tests |\n| `chore` | Maintenance — build system, deps, tooling |\n| `perf` | Performance improvement |\n| `ci` | CI/CD configuration changes |\n| `revert` | Reverting a previous commit |\n\n## Bad vs good examples\n\n| Bad | Good |\n| --- | --- |\n| `fix bug` | `fix(orders): correct off-by-one in pagination limit` |\n| `WIP` | `feat(search): add partial match for product names (draft)` |\n| `changes` | `refactor(db): replace raw SQL with typed query builder` |\n| `update deps` | `chore(deps): upgrade to Node 22 LTS, drop Node 20` |\n| `added tests` | `test(auth): add coverage for expired token refresh path` |\n\n## Setting up a commit template\n\nStore a template so your team uses it consistently:\n\n```bash\n# Create a template\ncat > ~/.gitmessage << 'EOF'\n# <type>(<scope>): <subject> — max 72 chars, imperative, no period\n# Types: feat fix docs style refactor test chore perf ci revert\n\n# Body: explain WHY, not WHAT. What alternatives were considered?\n\n# Footer: Closes #<issue>  |  BREAKING CHANGE: <desc>\nEOF\n\n# Tell git to use it\ngit config --global commit.template ~/.gitmessage\n```\n\nFor a team-wide template, commit it to the repo and set up a git hook:\n\n```bash\n# .git/hooks/commit-msg (or via Husky)\n#!/bin/sh\n# Enforce Conventional Commits subject line\npattern='^(feat|fix|docs|style|refactor|test|chore|perf|ci|revert)(\\(.+\\))?: .{1,72}$'\nsubject=$(head -1 \"$1\")\nif ! echo \"$subject\" | grep -qE \"$pattern\"; then\n  echo \"ERROR: Commit message does not follow Conventional Commits.\"\n  echo \"Expected: <type>(<scope>): <subject>\"\n  exit 1\nfi\n```\n\nHusky + `commitlint` is the standard automated enforcement in 2026:\n\n```json\n// package.json\n{\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^19\",\n    \"@commitlint/config-conventional\": \"^19\",\n    \"husky\": \"^9\"\n  }\n}\n```\n\n```js\n// commitlint.config.js\nexport default { extends: ['@commitlint/config-conventional'] };\n```\n\n## How to pick what to put in the body\n\nAsk yourself: \"Would a developer 12 months from now, seeing this commit in `git blame`, understand why this code exists the way it does?\"\n\nIf the subject line alone answers that — no body needed. If the change involved a non-obvious decision, a rejected alternative, a security tradeoff, or a workaround for a third-party bug — write a body.\n\n## Common mistakes\n\n**Past tense subject lines.** `Added validation` — make it `add validation`. The imperative reads as \"if you apply this commit, it will add validation.\"\n\n**Padding the subject.** `feat: various improvements and bug fixes` — useless. Split into separate commits.\n\n**No reference to the issue.** A commit that closes a bug without `Closes #123` breaks the traceability chain.\n\n**Committing unrelated changes together.** A commit that refactors AND adds a feature creates noise in the changelog and makes rollback painful. One logical change per commit.\n\n## What to skip\n\n- **Emoji-first commit messages** (`✨ feat: ...`, `🐛 fix: ...`) unless your team has explicitly agreed. They are unsearchable and cluttered in many log views.\n- **Vague scopes** — `(misc)`, `(various)`, `(other)` are not useful. Name the specific module or service.\n- **Closing multiple unrelated issues in one commit.** It signals the commit is doing too much.\n\n## FAQ\n\n**How granular should commits be?**\nOne logical unit of work. You should be able to describe the commit in one subject line without \"and.\" If you need \"and,\" split it.\n\n**What about merge commits vs squash-merge?**\nSquash-merge is popular for keeping main history clean; but you lose the commit-level context of the PR. Use squash for small PRs, preserve commits for large, multi-commit PRs where individual commits add context.\n\n**Should I rewrite commit messages before merging?**\nYes — rebase or amend to clean up \"WIP\" and fixup commits before they land on main. On main, never rewrite history.\n\n**Does this matter for solo projects?**\nYes, because your future self is the audience. Six-month-old-you will be very grateful for a descriptive message.\n\n## Where to go next\n\n- [How to review a pull request in 2026](/blog/how-to-review-a-pull-request-2026)\n- [How to review code effectively in 2026](/blog/how-to-review-code-effectively-2026)\n- [How to set up CI/CD in 2026](/blog/how-to-set-up-ci-cd-2026)","hue":312,"metaTitle":"How to Write Good Commit Messages in 2026: The Standard","metaDescription":"2026 guide to writing git commit messages. Conventional Commits format, subject line rules, body vs footer, and the anti-patterns that make git history useless.","keywords":["how to write good commit messages 2026","git commit message best practices 2026","conventional commits 2026","git commit format","commit message subject line","git history best practices","semantic versioning commits","commit message template","git log readability","good commit message examples"],"tldr":["**The subject line is a command, not a description** — \"Add rate limiting to auth endpoint\" not \"Added rate limiting\" or \"Rate limit.\"","**The body answers why, not what** — the diff shows what; the message explains the reasoning the diff cannot.","**Conventional Commits is the 2026 standard** — `feat:`, `fix:`, `chore:` prefixes enable automated changelogs and semantic versioning.","**Reference the ticket in the footer** — `Closes #123` or `Refs JIRA-456` keeps commits traceable to requirements.","**Skip:** \"WIP\", \"fix stuff\", \"changes\", or any message that requires reading the diff to understand the purpose."],"readTime":"6 min read","wordCount":1214},"related":[{"slug":"how-to-write-a-good-commit-message-2026","title":"How to Write a Good Commit Message in 2026: a practical guide","excerpt":"A good commit message explains why a change was made, not just what changed. Here is a simple, durable format and the habits that make a git history actually useful months later.","category":"code","date":"June 16, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":205},{"slug":"conventional-commits-explained-2026","title":"Conventional Commits Explained for 2026","excerpt":"Conventional Commits is a lightweight format for commit messages that both humans and machines can parse. This guide covers the spec, how it drives changelogs and versioning, and when to enforce it.","category":"code","date":"July 31, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":312},{"slug":"semantic-versioning-2026","title":"Semantic versioning in 2026: the version number system that actually works","excerpt":"Version numbers communicate intent to every consumer of your software. SemVer done right prevents dependency hell and makes releases predictable.","category":"code","date":"March 3, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":308}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-write-faster-2026","title":"How to write faster in 2026: practical techniques for every writer","excerpt":"Slow writing is usually a thinking problem, not a typing problem. These concrete techniques cut drafting time in half without sacrificing quality.","category":"guides","date":"April 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":95},"older":{"slug":"how-to-write-meeting-minutes-2026","title":"How to write meeting minutes in 2026: useful, fast, and actually read","excerpt":"Meeting minutes nobody reads are just bureaucracy. Here is how to write minutes in 2026 that are short, scannable, and actually drive follow-through.","category":"guides","date":"April 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":295}}