How to Write Better Git Commit Messages (With Examples)
This guide has a free tool → Open Git Commit Message Generator
Why Commit Messages Matter
Your git log is the history of your project. Every commit you write is a message to your future self and your teammates. Good commit messages make it possible to:
- Understand why a change was made months later
- Generate changelogs automatically
- Find the commit that introduced a bug using
git bisect - Review pull requests efficiently
- Onboard new team members faster
- Generate accurate release notes without manual effort
- Make rollbacks surgical rather than destructive
Bad commit messages turn your git log into noise:
fix stuff
wip
update
asdf
changes
another fix
tweaks
miscNone of these tell you anything useful. After six months you have no idea what "fix stuff" changed, why "tweaks" were needed, or whether it is safe to revert "another fix". Compare with:
fix: prevent duplicate form submission on slow connections
feat: add CSV export to the reports dashboard
refactor: extract email validation into shared utility
perf: debounce search input to reduce API callsEvery commit tells a story. You can read the log like documentation.
Git Commit Message Generator
Free online git commit message generator - generate conventional commit messages with type, scope, and description
Text Diff Checker
Free online text diff checker - compare two texts and see the differences highlighted line by line
GitHub README Generator
Free online github readme generator - create stunning GitHub profile READMEs with badges and stats
The Real Cost of Bad Commit Messages
The problem with vague commits compounds over time. Consider these scenarios:
Debugging a production issue: You discover that user account balances have been wrong since last Tuesday. You need to find what changed. If your log is full of "fix" and "update" entries, you are reading code diffs for every commit from that week. With descriptive commits, you search for fix(billing) or fix(account) and find the culprit in seconds.
Reviewing a pull request: A PR with 12 commits, all labeled "wip" and "fix", forces the reviewer to read every diff in full to understand what changed and why. A PR with meaningful commit messages communicates intent before the reviewer even opens a file.
Writing release notes: When it is time to ship version 2.5, someone has to translate the commit history into human-readable changelog entries. With good commits, this is automated. With bad commits, it is a multi-hour archaeological excavation.
Onboarding a new engineer: The git log is often the best documentation a codebase has. Commits that explain the reasoning behind architectural decisions are invaluable context for someone learning the codebase.
The Anatomy of a Good Commit Message
A commit message has up to three parts:
<subject line> <- required, max 50-72 characters
<- blank line
<body> <- optional, explains why, wraps at 72 characters
<- blank line
<footer> <- optional, references issues, breaking changesThe Subject Line
This is the most important part. It should:
- Start with a capital letter (unless using Conventional Commits prefix)
- Use imperative mood - "Add feature" not "Added feature" or "Adds feature"
- Be under 72 characters - GitHub truncates beyond this in most views
- Not end with a period
- Describe what the commit does, not what you did
Think of it as completing the sentence: "If applied, this commit will ___."
- "If applied, this commit will add email validation to signup form" - good
- "If applied, this commit will fix bug" - too vague
- "If applied, this commit will updated the readme with new instructions" - wrong tense
- "If applied, this commit will minor performance improvements" - not imperative
The imperative mood matches how Git itself writes messages. When Git auto-generates a merge commit, it says "Merge branch 'feature/search' into main" - not "Merged" or "Merging". Consistent tense makes automated tools and log scans more predictable.
The Body
For complex changes, explain the why in the body. The diff already tells reviewers what changed - your commit message should explain why the change was necessary.
Refactor user authentication to use JWT instead of sessions
Session-based auth required sticky sessions on the load balancer,
which prevented horizontal scaling. JWT tokens are stateless and
can be verified by any server instance without shared state.
The refresh token rotation strategy follows the OAuth 2.0 spec
to prevent token theft through token reuse detection.Good body content includes:
- Why the change was necessary (the problem being solved)
- Any alternative approaches that were considered and rejected
- Side effects or consequences the reviewer should know about
- Context that is not visible from reading the code diff
Wrap body lines at 72 characters. This ensures the message renders correctly in terminal output, git log viewers, and email clients.
The Footer
Reference related issues and note breaking changes:
feat: add two-factor authentication
Implements TOTP-based 2FA using the Web Authentication API.
Users can enable 2FA from their security settings page.
A backup code is generated on enrollment for account recovery.
Closes #142
Refs #138, #141
BREAKING CHANGE: /api/login now returns a 2FA challenge token
when 2FA is enabled, instead of the session token directly.Footer conventions:
Closes #123- automatically closes GitHub issue #123 when the PR mergesRefs #123- references an issue without closing itBREAKING CHANGE:- indicates an API-incompatible change (triggers major version bump with semantic release)Co-authored-by: Name <email>- credits a collaborator
Conventional Commits
The Conventional Commits specification is the most popular format for structured commit messages. It provides a machine-readable structure that tools can parse to automate versioning and changelogs.
Format:
<type>(<optional scope>): <subject>Common Types
| Type | Purpose | Example |
|---|---|---|
feat | New feature | feat: add dark mode toggle |
fix | Bug fix | fix: resolve login timeout on mobile |
docs | Documentation only | docs: update API authentication guide |
style | Formatting, no code change | style: fix indentation in config files |
refactor | Code change that neither fixes nor adds | refactor: simplify date parsing logic |
perf | Performance improvement | perf: cache database queries for user profiles |
test | Adding or updating tests | test: add unit tests for payment processing |
chore | Build process or tooling | chore: update eslint to v9 |
ci | CI/CD changes | ci: add Node 22 to test matrix |
build | Build system changes | build: switch bundler from webpack to vite |
revert | Revert a previous commit | revert: feat(auth): add OAuth login |
Scopes
The optional scope narrows down which part of the codebase the commit affects:
feat(auth): add OAuth 2.0 login with Google
fix(cart): prevent negative quantities when decrementing
perf(images): lazy load below-the-fold product images
refactor(api): replace axios with native fetch
docs(readme): add Docker setup instructions
test(checkout): add integration tests for Stripe webhook handling
chore(deps): upgrade React from 18.2 to 19.0Scopes should match the major modules or features of your codebase. If your project has an auth, cart, api, and dashboard module, use those as scopes consistently.
Why Use Conventional Commits?
- Automatic changelogs - tools like
standard-versionandsemantic-releasegenerate changelogs from commit types automatically - Semantic versioning -
featbumps minor version,fixbumps patch,BREAKING CHANGEbumps major - Searchable history -
git log --grep="^feat"finds all feature commits;git log --grep="^feat(auth)"finds all auth feature commits - Better code reviews - reviewers immediately understand the intent before reading the diff
- CI/CD integration - automated pipelines can react to commit type (skip deployment on
chore, trigger release onfeat)
Setting Up semantic-release
If you want fully automated versioning and changelogs based on Conventional Commits:
npm install --save-dev semantic-release @semantic-release/git @semantic-release/changelog.releaserc.json:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github"
]
}With this setup, pushing to main automatically determines the next version number from your commit messages, generates a changelog, creates a GitHub release, and publishes to npm - all from commit message structure alone.
Real-World Examples
Good Commit Messages by Category
Feature additions:
feat: add CSV export to the reports dashboard
feat(auth): support magic link authentication
feat(api): implement cursor-based pagination
feat(search): add fuzzy search with trigram similarity
feat(notifications): add email digest for weekly summaryBug fixes:
fix: prevent form resubmission on browser back navigation
fix(auth): handle token refresh when clock skew exceeds 5 minutes
fix(upload): reject files larger than 10MB before upload starts
fix(grid): correct column alignment on Safari 17
fix(api): return 409 instead of 500 on duplicate key violationPerformance improvements:
perf: replace full table scan with indexed lookup for user search
perf(images): convert hero images to WebP and serve via srcset
perf(dashboard): memoize expensive chart recalculations
perf(api): add Redis cache for frequently-read configuration dataRefactoring:
refactor: migrate date handling from moment.js to date-fns
refactor(auth): extract token validation into middleware
refactor(components): replace class components with hooks
refactor(db): normalize address data across three tablesDocumentation and chores:
docs: add architecture decision record for database choice
docs(api): document rate limiting behavior per endpoint
chore: remove deprecated lodash usage
chore(deps): update all minor version dependenciesBad Commit Messages (And How to Fix Them)
| Bad | Problem | Better |
|---|---|---|
fix bug | Does not describe the bug | fix: handle null response from payment API |
update styles | No specifics | style: align header navigation on mobile viewports |
WIP | Not a real commit message | feat(search): add autocomplete skeleton (debounce pending) |
review feedback | Content is invisible | fix(auth): validate token expiry before refreshing |
misc changes | Multiple concerns bundled | Split into focused commits |
test | No context | test(checkout): add tests for declined card scenarios |
asdf | Accidental commit | git commit --amend to fix it before pushing |
fix 2, fix 3 | Iterating in public | Squash before merging |
add feature | Too vague | feat(dashboard): add weekly revenue chart |
Tips for Better Commits
Make Atomic Commits
Each commit should represent exactly one logical change. If you fixed a bug and reformatted the file, those should be two separate commits. If you built a feature and added tests, commit the feature logic and the tests together - they are one logical unit.
Use git add -p (patch mode) to interactively stage specific hunks within a file:
git add -p src/components/UserForm.tsxGit will show you each changed section and ask whether to stage it. This lets you split a single modified file into multiple focused commits.
Write the Message Before Committing
If you cannot describe what you did in one clear sentence, your commit might be doing too much. This is a code design signal, not just a writing problem. A commit that is hard to name is often a commit that should be split.
Try writing the subject line before you even start coding. This keeps you focused on one task at a time.
Commit Frequently During Development
Frequent small commits are easier to review, easier to bisect, and easier to cherry-pick than large monolithic commits. You can always squash commits before merging a branch if the intermediate states are not meaningful for the permanent history.
# Squash last 4 commits into one before merging
git rebase -i HEAD~4In the interactive rebase editor, mark all but the first commit as squash or fixup.
Use Interactive Rebase to Clean Up Before Merging
Your working branch history can be messy - that is fine. Before merging to main, clean it up:
git rebase -i origin/mainCommon operations:
pick- keep the commit as-isreword- keep the commit but edit the messagesquash- merge into the previous commit, combining messagesfixup- merge into the previous commit, discarding this messagedrop- remove the commit entirely
Use Present Tense, Imperative Mood
Add feature <- correct (imperative)
Added feature <- incorrect (past tense)
Adding feature <- incorrect (gerund)
Adds feature <- incorrect (third person)This convention feels slightly unnatural at first but quickly becomes automatic. It aligns with how code reviews describe changes ("this commit adds", "this PR fixes") and how changelog tools generate entries.
Reference Issues in the Footer, Not the Subject
# Wrong - clutters the subject line
fix: #142 token expiry not checked before refresh
# Right - issue reference in footer
fix(auth): validate token expiry before refreshing session
Closes #142The subject line should be independently readable. Issue references belong in the footer where they are still linked but do not pollute the summary.
Setting Up Commit Message Linting
For team environments, enforce commit message standards automatically with commitlint:
npm install --save-dev @commitlint/cli @commitlint/config-conventional huskycommitlint.config.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
};Set up the Git hook with Husky:
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msgNow every commit message is validated before it is written. Non-conventional commits are rejected with a helpful error message.
Example output when you try to commit with a bad message:
⧗ input: fix stuff
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 2 problems, 0 warningsConfiguring commitlint for Your Conventions
If you want to allow additional types or scopes:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'chore', 'ci', 'build', 'revert',
'hotfix', 'release', 'security'
]],
'scope-enum': [2, 'always', [
'auth', 'api', 'dashboard', 'billing', 'notifications',
'search', 'ui', 'db', 'deps', 'config', 'docs'
]],
'subject-max-length': [2, 'always', 72],
},
};Reading a Git Log Effectively
Once your commits are well-structured, the log becomes a powerful documentation tool.
View Oneline Log
git log --onelineOutput:
a3f2c1d fix(auth): validate token expiry before refreshing
e9b4a2c feat(dashboard): add weekly revenue chart
7d1c3e8 perf(api): cache user profile queries in RedisFilter by Type
# Find all feature commits
git log --oneline --grep="^feat"
# Find all fixes in the auth scope
git log --oneline --grep="^fix(auth)"
# Find all breaking changes
git log --oneline --grep="BREAKING CHANGE"Find Who Changed a Line
git blame src/auth/tokenService.tsThis shows which commit last changed each line, with the commit hash, author, and date. Combined with good commit messages, you can understand exactly why any line of code looks the way it does.
Bisect to Find a Bug
git bisect start
git bisect bad HEAD # current version has the bug
git bisect good v2.3.0 # this version was fine
# Git checks out the midpoint commit
# Test your application, then:
git bisect good # or
git bisect bad
# Repeat until Git finds the culprit commitWith descriptive commit messages, when git bisect finds the offending commit, you can immediately read what it did:
3f1a2b4 fix(billing): recalculate subtotals before applying taxWithout descriptive commits, you get:
3f1a2b4 misc changesAnd now you need to read the full diff.
Commit Message Templates
You can configure a commit message template that Git shows in your editor when you run git commit:
.gitmessage in your home directory:
# <type>(<scope>): <subject>
# |<---- Using a Maximum Of 72 Characters ---->|
# Explain why this change is being made
# |<---- Try To Limit Each Line to a Maximum Of 72 Characters ---->|
# --- COMMIT END ---
# Type can be:
# feat (new feature)
# fix (bug fix)
# refactor (refactoring production code)
# style (formatting, missing semicolons, etc; no code change)
# docs (changes to documentation)
# test (adding or refactoring tests; no production code change)
# chore (updating grunt tasks, etc.; no production code change)
# perf (performance improvement)
# ci (CI/CD changes)
# --------------------
# Remember to:
# - Use the imperative mood in the subject line
# - Not end the subject line with a period
# - Separate subject from body with a blank line
# - Reference issues in the footer: Closes #123Configure Git to use this template:
git config --global commit.template ~/.gitmessageNow running git commit (without -m) opens your editor with the template, prompting you for the right structure.
Common Commit Message Anti-Patterns
The Junk Drawer Commit
chore: various fixes and updatesThis catches multiple unrelated changes in one commit. The problem is not the message - it is that the commit itself contains unrelated work. Split it with git add -p.
The WIP Chain
WIP
WIP progress
WIP more stuff
WIP done
WIP cleanupWorking in progress is fine for local development, but these should be squashed before your branch is reviewed or merged. Use git rebase -i to clean up the history.
The Annotated Diff
Changed getUserById to accept string instead of number
Removed the parseInt() call from line 47
Added null check before returningThis describes what the diff shows - which the diff already shows. The commit message should explain why these changes were made, not repeat what they are.
The Timestamp Commit
Update 2026-03-01
Monday changes
Weekend workThese are completely opaque. The commit timestamp already records when the change was made - the message should tell you what and why, not when.
Using the Git Commit Message Generator
Drafting a commit message from scratch is sometimes harder than the coding itself - especially when you are tired, rushing, or not a native English speaker. The Git Commit Message Generator helps you write well-structured commit messages in Conventional Commits format.
Describe your changes in plain terms, and the tool formats them into a proper subject line, body, and footer. You can copy the result directly into your terminal.
After generating your commit message, you may also want to:
- Use the Diff Checker to review exactly what changed before committing
- Use the GitHub README Generator to keep your project documentation up to date
Integrating Good Commits Into Your Workflow
Pre-commit Checklist
Before running git commit, ask yourself:
- Does this commit do exactly one thing?
- Can I describe it in one imperative sentence under 72 characters?
- Have I explained the why in the body if the change is non-obvious?
- Have I referenced the relevant issue in the footer?
If you cannot answer yes to all four, consider splitting the commit or spending one more minute on the message.
Team Conventions Document
For teams, document your commit message conventions in your contributing guide. Include:
- The commit type vocabulary you use
- The scope vocabulary for your project
- Whether breaking changes require a specific footer format
- Whether you squash commits before merging or preserve history
- Examples of good and bad commit messages
Consistent conventions reduce cognitive load for everyone on the team and make automated tooling reliable.
Branch Naming That Complements Commits
Good commit messages pair well with consistent branch names:
feature/add-email-verification
fix/duplicate-form-submission
chore/upgrade-dependency-eslint
hotfix/production-payment-crashThis way, the PR title, branch name, and commit messages all tell the same consistent story.
Summary
Good commit messages are a professional habit that pays dividends every time you debug, review, or document your code. The key rules to internalize:
- Use Conventional Commits format:
type(scope): subject - Write in imperative mood: "Add" not "Added"
- Keep the subject under 72 characters
- Explain the why in the body, not the what
- Reference issues in the footer
- Make each commit atomic - one logical change per commit
- Clean up WIP commits with interactive rebase before merging
The discipline of writing good commits forces you to think more carefully about what each change actually does - which leads to better code, better code reviews, and a history that serves as living documentation.
Generate Git Commit Messages With ToolBox
Struggling to write a clear commit message? The Git Commit Message Generator helps you draft well-structured commit messages following Conventional Commits format. Describe your changes, and get a properly formatted message you can copy directly into your terminal.
Need to compare code changes before committing? Use the Diff Checker to review your diff side by side. Building a GitHub README? Try the GitHub README Generator to keep your project documentation professional.
Related Tools
Free, private, no signup required
JSON Formatter
JSON formatter and validator online - format, beautify, and validate JSON data instantly in your browser
Regex Tester
Free online regex tester - test and debug regular expressions with live matching and highlights
Code Formatter
Free online code formatter - beautify and format JavaScript, CSS, HTML, and more
You might also like
Want higher limits, batch processing, and AI tools?