Writing Good Commit Messages
Commit messages are documentation for your project's history. Good messages make debugging, code review, and collaboration much easier.
Why Commit Messages Matter
Your future self and teammates will thank you:
# Bad history - useless
abc123 fix
def456 update
789abc changes
fedcba stuff
# Good history - tells a story
abc123 Fix null pointer exception in user lookup
def456 Add email validation to signup form
789abc Refactor database connection pooling
fedcba Update dependencies to fix security vulnerability
The Anatomy of a Good Message
Structure
Subject line (50 chars or less)
Body (72 chars per line, optional but recommended)
Explain the what and why, not the how.
Footer (optional)
Closes #123
Subject Line Rules
- Limit to 50 characters: Git truncates longer lines
- Capitalize first letter: "Add feature" not "add feature"
- No period at the end: "Add feature" not "Add feature."
- Use imperative mood: "Add" not "Added" or "Adds"
Body Rules
- Blank line after subject: Separates subject from body
- Wrap at 72 characters: For readability
- Explain what and why: Code shows how
- Reference issues: If applicable
The Imperative Mood
Write as if giving a command:
✅ Correct (imperative):
- "Add login validation"
- "Fix memory leak"
- "Update dependencies"
- "Remove deprecated function"
❌ Incorrect:
- "Added login validation"
- "Fixes memory leak"
- "Updated dependencies"
- "Removing deprecated function"
Tip: Your message should complete "If applied, this commit will..."
- "If applied, this commit will add login validation"
Examples
Simple Change
Fix typo in error message
With Body
Add rate limiting to API endpoints
Users were experiencing 503 errors during peak traffic.
Rate limiting prevents server overload by capping
requests at 100/minute per IP address.
Closes #234
Bug Fix
Fix null pointer exception in UserService.getUser()
The method crashed when querying for a non-existent user ID.
Now returns Optional.empty() instead of throwing.
This was causing the entire application to crash when
invalid user IDs were passed via the API.
Fixes #567
Breaking Change
Change authentication to use JWT tokens
BREAKING CHANGE: Session-based auth is no longer supported.
All clients must update to use JWT tokens.
Migration guide:
1. Update client libraries to v2.0
2. Replace session endpoints with token endpoints
3. Update token refresh logic
See docs/migration-v2.md for details.
Conventional Commits
A popular specification for commit messages:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types
| Type | Description |
|---|---|
feat | New feature |
fix | Bug fix |
docs | Documentation only |
style | Formatting, missing semicolons |
refactor | Code change that neither fixes nor adds |
perf | Performance improvement |
test | Adding or updating tests |
build | Build system or external dependencies |
ci | CI configuration |
chore | Other changes that don't modify src/test |
revert | Reverts a previous commit |
Examples
feat(auth): add password strength indicator
fix(api): handle null response from external service
docs: update README with new installation steps
refactor(database): extract connection logic to separate module
test(auth): add tests for password reset flow
BREAKING CHANGE: removed deprecated login endpoint
Scopes
Optional, but useful for larger projects:
feat(auth): ...
feat(cart): ...
feat(checkout): ...
fix(api): ...
fix(ui): ...
What NOT to Do
Too Vague
❌ "Fix bug" ✅ "Fix login redirect loop when session expires"
❌ "Update" ✅ "Update moment.js to 2.30.0 to fix CVE-2022-1234"
❌ "Changes" ✅ "Refactor user service to use async/await"
Too Long
❌ "This commit fixes the bug where users cannot log in with special characters in their password because the password was not being properly escaped before being sent to the authentication service"
✅ Subject: "Fix login failure with special characters in password" Body: "Password was not escaped before sending to auth service."
Mixing Concerns
❌ "Fix login bug, add signup validation, refactor database"
✅ Three separate commits:
- "Fix login failure with special characters"
- "Add email validation to signup form"
- "Refactor database connection pooling"
WIP Commits
Avoid pushing WIP commits. Use interactive rebase to clean up:
# Before push, squash WIP commits
git rebase -i main
Commit Message Templates
Setup Template
# Create template file
cat > ~/.gitmessage << 'EOF'
# Subject: Imperative, max 50 chars, no period
# |<---- Using a maximum of 50 characters ---->|
# Body: Explain *what* and *why*, wrap at 72 chars
# |<---- Try to limit each line to 72 characters ---->|
# Footer: Reference issues, breaking changes
# Fixes #123
# BREAKING CHANGE: description
EOF
# Configure Git to use it
git config --global commit.template ~/.gitmessage
Use Template
git commit # Opens editor with template
Amending Commits
Fix Last Commit Message
git commit --amend -m "New, better message"
Add to Last Commit
git add forgotten-file.js
git commit --amend --no-edit
Note: Only amend unpushed commits!
Team Conventions
Agree on team standards:
- Format: Conventional commits or custom
- Scope: Required or optional
- Issue references: Required or optional
- Tools: Commitlint, husky for enforcement
Enforcing with Commitlint
// package.json
{
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
}
Summary
- Subject: 50 chars, imperative mood, capitalized
- Body: Explain what and why (optional but valuable)
- Use blank line between subject and body
- Reference issues in footer
- Consider Conventional Commits format
- One logical change per commit
- Avoid vague messages like "fix" or "update"
- Use templates for consistency
In the next lesson, we'll learn about .gitignore best practices.

