Commit Message Conventions
A well-written commit message is one of the most underrated tools in software development. It tells the story of your code—why changes were made, not just what changed. Learn the Conventional Commits standard and best practices for writing clear, meaningful commit messages.
Commit messages are the historical record of your project. Months or years from now, you'll look at the git log to understand why something was changed. A good commit message answers the question "Why?" not just "What?" The "what" is already visible in the code diff. The "why" is what matters for future developers.
Good commit messages help with code review, debugging (finding when a bug was introduced), understanding rationale, generating changelogs automatically, and onboarding new team members. They also improve your team's ability to communicate about the codebase.
- 1. Separate subject from body with a blank line - The subject line is a summary; the body provides details.
- 2. Limit the subject line to 50 characters - Short enough to be readable in git log --oneline.
- 3. Capitalize the subject line - Starts with a capital letter, like a sentence.
- 4. Do not end the subject line with a period - Save the space for content.
- 5. Use the imperative mood in the subject line - Write as if giving a command: "Fix bug" not "Fixed bug" or "Fixes bug".
- 6. Wrap the body at 72 characters - Git doesn't wrap text; do it manually for readability.
- 7. Use the body to explain what and why vs. how - The how is in the diff.
Fix calculation error in tax calculator
The tax calculation was incorrectly using the subtotal after discount
but before shipping. This caused incorrect tax amounts for orders
with free shipping.
Fixed by moving the tax calculation after all discounts are applied.
Added tests for the free shipping edge case.
Conventional Commits is a lightweight convention for commit messages that enables automatic changelog generation and semantic versioning. The format is simple: <type>[optional scope]: <description>, followed by an optional body and footer.
Core commit types: feat - a new feature (triggers a minor version bump), fix - a bug fix (triggers a patch version bump), docs - documentation changes, style - formatting, whitespace, etc. (no code change), refactor - code change that neither fixes a bug nor adds a feature, perf - performance improvement, test - adding or updating tests, chore - maintenance tasks (build process, dependencies).
Use ! after the type/scope to indicate a breaking change: feat(api)!: change response format (triggers a major version bump).
Implement OAuth2 integration for Google authentication.
Users can now sign in using their Google accounts.
Added new environment variables for client ID and secret.
| Type | Description | Version Impact |
|---|---|---|
feat | New feature for the user | Minor (x.Y.z) |
fix | Bug fix for the user | Patch (x.y.Z) |
docs | Documentation changes | None |
style | Formatting, missing semicolons, etc. | None |
refactor | Code change that neither fixes bug nor adds feature | None |
perf | Performance improvement | Patch |
test | Adding missing tests or correcting tests | None |
chore | Build process or auxiliary tool changes | None |
ci | CI/CD configuration changes | None |
BREAKING CHANGE | Breaking API change | Major (X.y.z) |
The subject line is the first line of the commit message, limited to 50 characters. It should complete the sentence "If applied, this commit will..." Use the imperative mood: "Fix" not "Fixed" or "Fixes". Be specific but concise.
fix: resolve null pointer in login flow
docs: update API documentation for v2
perf(api): reduce response time by caching
refactor: extract validation logic to helper
git commit -m "subject" -m "body" to easily create multi-line commit messages with a blank line between subject and body.
The body should explain what changed and why, not the mechanics of the change. Focus on the problem being solved and the approach taken. Ask yourself: "If I read this commit message in six months, will I understand the reasoning?"
Wrap lines at 72 characters. Use bullet points for multiple changes or reasons. Mention ticket numbers, related issues, or previous commits that are relevant.
The tax calculation was applied before the free shipping discount,
causing incorrect tax amounts for orders with free shipping.
This affected all orders over $50 where free shipping applies.
Changes made:
- Move tax calculation after all discounts
- Add test for free shipping tax calculation
- Update documentation for calculation order
When a change breaks backward compatibility, mark it clearly. In Conventional Commits, add a ! after the type/scope, or add a BREAKING CHANGE: footer. This triggers a major version bump in automated semantic versioning tools.
The API now returns consistent error objects and uses camelCase
for all fields. Previous snake_case fields are still available
temporarily but will be removed in v3.0.
| ❌ Bad | ✅ Good |
|---|---|
| "fix bug" | "fix: correct null pointer when user session expires" |
| "update" | "feat(profile): add avatar upload functionality" |
| "changed stuff" | "refactor: extract payment validation to dedicated service" |
| "fixed tests" | "test: add integration tests for checkout flow" |
| "more changes" | "perf: lazy-load images on homepage" |
Enforce commit conventions with automated tools. Commitlint checks commit messages against rules. Husky runs lint-staged and commitlint on git hooks. Semantic-release automatically bumps versions and generates changelogs from Conventional Commits.
# Install commitlint and husky
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
# commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'subject-case': [2, 'never', ['start-case', 'pascal-case']],
'subject-max-length': [2, 'always', 50]
}
};
One of the biggest benefits of Conventional Commits is automatic changelog generation. Tools like standard-version or semantic-release scan commit history and produce formatted changelogs grouped by type (Features, Bug Fixes, Performance Improvements, Breaking Changes, etc.).
## 1.2.0 (2024-03-15)
### Features
- add user profile editing (abc1234)
- implement social login with Google (def5678)
### Bug Fixes
- correct tax calculation for free shipping (ghi9012)
- resolve null pointer in login flow (jkl3456)
- Agree on commit types as a team. Add custom types if needed (e.g., "ui", "i18n", "security").
- Enforce conventions with commitlint in CI. Reject commits that don't follow the format.
- Use scopes to indicate the area of the codebase (e.g., "auth", "api", "database").
- Keep commits focused. One logical change per commit. If you need "and" in the description, split into multiple commits.
- Write commit messages before writing code. It helps clarify what you're trying to accomplish.
- Squash WIP commits before merging. A feature branch doesn't need 15 "WIP" commits in the final history.
git commit --amend to edit the most recent commit message. For older commits, use git rebase -i HEAD~N and use "reword" on the commit. Never rewrite history on shared branches.Closes #123 or Fixes #456. GitHub automatically links the commit to the issue and closes it when the commit is merged to main.git add -p to stage parts of files. Each commit should represent one logical change. If you're describing the change with "and," it's probably two commits.git revert which automatically creates a commit message like "Revert 'previous commit message'". You can add additional explanation in the body about why the revert is needed.<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
Example:
feat(auth): add biometric login support
Implement fingerprint and face recognition login
using the device's biometric hardware.
Closes #342
Great commit messages tell the story of your project. They make git log a valuable resource rather than a mystery. Invest in your commit messages—they pay dividends for years.