This is a complete, beginner-to-advanced guide on writing great Git commit messages. You will learn why commit messages matter, the anatomy of a perfect commit, conventional commit standards, real-world examples, team workflows, and tools that automate and enforce quality — all from top to bottom.
📖 Table of Contents
- Why Commit Messages Matter
- The Anatomy of a Commit Message
- The 7 Golden Rules of Commit Messages
- Commit Types — What Changed?
- The Conventional Commits Specification
- Scopes — Where Did It Change?
- Breaking Changes
- Writing the Subject Line
- Writing the Body
- Writing the Footer
- Good vs Bad Examples
- Commit Message Templates
- Atomic Commits — What to Include in One Commit
- Branch Naming and Its Relation to Commits
- Commit Messages for Different Scenarios
- Tools — Commitizen, Commitlint, Husky
- Automating Changelogs and Versioning
- Emoji in Commit Messages — Gitmoji
- Team Conventions and Contributing Guides
- Common Mistakes to Avoid
1️⃣ Why Commit Messages Matter
Most developers treat commit messages as an afterthought — a quick note dashed off before pushing. But commit messages are one of the most valuable forms of documentation in a software project. They are the only place where you explain why a change was made, not just what changed.
What Commit Messages Are Used For
- 🔍 Code reviews — Reviewers read commit messages to understand intent before looking at the diff
- 🐛 Debugging — git blame and git log tell you who changed what and why
- 📋 Changelogs — Commit messages can be auto-generated into human-readable release notes
- 🔄 Reverting — A clear message makes it obvious whether a commit is safe to revert
- 🧭 Onboarding — New team members read git history to understand how the codebase evolved
- 📦 Semantic versioning — Tools like semantic-release use commit types to automatically determine version bumps
- ⚖️ Audit trails — In regulated industries, commit messages are part of the compliance record
The Cost of Bad Commit Messages
bad git log
# This is what a bad git history looks like
$ git log --oneline
a1b2c3d fix
9e8f7g6 update
5h4i3j2 WIP
1k0l9m8 asdf
7n6o5p4 changes
3q2r1s0 more stuff
e9f8g7h final
d6e5f4g final final
c3d2e1f FINAL (for real this time)
b0c1d2e refactor
a9b8c7d miscWhen you see a history like this, you have zero information. You cannot tell what changed, why it changed, when a bug was introduced, or what a release contains. Every question requires reading the actual code diff — which defeats the purpose of having a history at all.
Note
2️⃣ The Anatomy of a Commit Message
A well-structured commit message has up to three parts, separated by blank lines:
commit message structure
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>The Three Parts
| Part | Required? | Purpose | Max Length |
|---|---|---|---|
| Subject | ✅ Always | One-line summary of what changed | 50–72 characters |
| Body | ⚪ Optional | Explains WHY and HOW in detail | 72 chars per line |
| Footer | ⚪ Optional | Issue references, breaking changes, co-authors | No limit |
A Real Full Commit Message
full commit example
feat(auth): add OAuth2 login with Google
Previously, users could only sign in with email and password.
This adds Google OAuth2 as an alternative login option using
the Passport.js GoogleStrategy.
The callback URL is configurable via the GOOGLE_CALLBACK_URL
environment variable, defaulting to /auth/google/callback.
Session handling was updated to store the Google profile ID
alongside the existing user record.
Closes #142
BREAKING CHANGE: The /auth/login route now redirects to /auth instead of /dashboard
Co-authored-by: Alice Smith <alice@example.com>Let's break down each part in detail over the next several sections.
3️⃣ The 7 Golden Rules of Commit Messages
These rules come from Chris Beams' widely cited essay "How to Write a Git Commit Message"and have become the de facto standard across the industry.
- Separate subject from body with a blank line
Git uses the first line as the subject in many commands (git log --oneline, email subjects, GitHub UI). A blank line tells Git where the subject ends and the body begins. - Limit the subject line to 50 characters
GitHub truncates at 72 characters. 50 is the ideal target. It forces clarity and conciseness. If you can't summarize in 50 chars, your commit is probably doing too much. - Capitalize the subject line
Start with an uppercase letter: Fix login redirect bug not fix login redirect bug. - Do not end the subject line with a period
It's a title, not a sentence. Add user avatar upload not Add user avatar upload. - Use the imperative mood in the subject line
Write as if giving a command: Fix bug, Add feature, Update docs. Not Fixed bug, Adding feature, or Updated docs. Git itself uses this convention — merge commits say "Merge branch X", not "Merging branch X". - Wrap the body at 72 characters
Git doesn't wrap text automatically. Without a character limit, long lines look bad in terminals, email clients, and side-by-side diffs. Most editors can be configured to wrap at 72. - Use the body to explain WHAT and WHY, not HOW
The diff shows how you made the change. The commit message should explain why it was necessary and what problem it solves. Context that isn't in the code belongs in the message.
Note
4️⃣ Commit Types — What Changed?
The type is the first word of your commit subject under the Conventional Commits standard. It tells readers at a glance what category of change this commit represents.
| Type | When to Use | Version Bump |
|---|---|---|
| feat | A new feature visible to users or consumers of an API | MINOR (1.x.0) |
| fix | A bug fix that corrects incorrect behavior | PATCH (1.0.x) |
| docs | Documentation only changes — README, JSDoc, comments | None |
| style | Code formatting, whitespace, semicolons — no logic change | None |
| refactor | Code restructuring that neither fixes a bug nor adds a feature | None |
| perf | Performance improvement — faster queries, reduced memory usage | PATCH |
| test | Adding or updating tests — no production code change | None |
| build | Build system, bundler, dependency changes (webpack, npm, gradle) | None |
| ci | CI/CD configuration changes (GitHub Actions, CircleCI, Jenkins) | None |
| chore | Maintenance tasks that don't modify src or test files | None |
| revert | Reverting a previous commit | Depends |
| wip | Work in progress — should be squashed before merging | None |
Choosing the Right Type
type decision guide
Did you add something new that users/consumers will notice?
→ feat
Did you fix something that was broken?
→ fix
Did you only change .md, comments, or docs files?
→ docs
Did you only change whitespace, formatting, or semicolons?
→ style
Did you restructure code without changing what it does?
→ refactor
Did you make something faster or use less memory?
→ perf
Did you add, update, or fix tests?
→ test
Did you change package.json, webpack, Dockerfile?
→ build
Did you change .github/workflows, .travis.yml?
→ ci
Did you do something that doesn't fit above (rename files, update .gitignore)?
→ chore5️⃣ The Conventional Commits Specification
Conventional Commits is a formal specification for commit message structure that has become the industry standard. It is designed to be machine-readable (for tooling) and human-readable at the same time.
conventionalcommits.orgFull Format
conventional commits format
<type>[optional scope][optional !]: <description>
[optional body]
[optional footer(s)]Format Rules
- The type must be a noun: feat, fix, docs, etc.
- The scope is optional, wrapped in parentheses: feat(auth):
- A ! before the colon indicates a breaking change: feat!: or feat(api)!:
- The description is a short summary in the present tense, lowercase after the colon
- The body is free-form text explaining the change in detail
- The footer contains tokens like BREAKING CHANGE:, Closes #123, Reviewed-by:
Valid Examples
valid conventional commits
# Minimal — just a subject with type
feat: add dark mode toggle
# With scope
fix(auth): prevent session timeout on remember-me login
# With scope and breaking change indicator
feat(api)!: remove deprecated v1 endpoints
# With body
fix(cart): correct item count after quantity update
The cart total was not recalculating when users changed
item quantity using the +/- buttons. This was because the
quantity change event was not dispatching to the store.
# With footer
feat(payments): add Stripe Connect for marketplace sellers
Closes #87
Reviewed-by: Bob Johnson <bob@example.com>
# With breaking change in footer
refactor(config): rename DATABASE_URL to DB_CONNECTION_STRING
BREAKING CHANGE: The environment variable DATABASE_URL has been
renamed to DB_CONNECTION_STRING. Update your .env files and
deployment configs before upgrading.
# Revert
revert: feat(auth): add OAuth2 login with Google
This reverts commit a1b2c3d4e5f6.
Reason: Google OAuth credentials were committed accidentally.6️⃣ Scopes — Where Did It Change?
A scope is an optional noun describing the part of the codebase affected by the commit. It goes in parentheses after the type: fix(auth):, feat(ui):, docs(api):.
When to Use a Scope
- ✅ Use scopes when your project has clearly defined modules, packages, or areas
- ✅ Use scopes in monorepos to identify which package was changed
- ✅ Use scopes when multiple teams work on different parts of the codebase
- ⚠️ Skip scopes in very small projects where every commit already implies a single context
Common Scope Examples by Project Type
| Project Type | Common Scopes |
|---|---|
| Web App | auth, ui, api, db, router, store, forms |
| React App | components, hooks, pages, context, utils, styles |
| Node.js API | users, orders, payments, middleware, models, routes |
| Monorepo | web, mobile, api, shared, admin, cli |
| Library/SDK | core, plugins, types, cli, docs |
scope examples
feat(auth): add biometric login support
fix(checkout): apply correct tax rate for EU customers
docs(api): update rate limiting documentation
refactor(db): extract connection pooling into separate module
test(payments): add integration tests for Stripe webhook handler
style(components): format Button component with Prettier
perf(images): lazy load product thumbnails on category page
build(deps): upgrade React from 18.2 to 18.3
ci(github): add automated accessibility testing to PR checks7️⃣ Breaking Changes
A breaking change is any change that requires consumers of your code — other developers, API clients, or downstream packages — to update their own code to remain compatible.
Two Ways to Signal Breaking Changes
Method 1 — Exclamation Mark in Subject
breaking change with !
# Add ! before the colon — quick and visible
feat!: remove support for Node.js 14
feat(api)!: require authentication on all endpoints
refactor(config)!: flatten nested configuration structureMethod 2 — BREAKING CHANGE in Footer
breaking change in footer
feat(api): require authentication on all public endpoints
Previously, the /products and /categories endpoints were publicly
accessible without an API key. This change requires all clients
to send a valid Bearer token in the Authorization header.
BREAKING CHANGE: All API endpoints now require authentication.
Clients must include "Authorization: Bearer <token>" in every
request. The /auth/token endpoint is exempt and remains public.
See the migration guide at docs/migration/v2.md.What Counts as a Breaking Change?
- 🔴 Removing a public function, method, class, or endpoint
- 🔴 Renaming a public API without backward-compatible alias
- 🔴 Changing the signature of a function (parameters, return type)
- 🔴 Changing the shape of a response payload or database schema
- 🔴 Removing or renaming environment variables
- 🔴 Dropping support for a runtime version (Node.js, Python, etc.)
- 🟡 Changing default behavior that users may depend on
- 🟢 Adding new optional parameters (usually NOT breaking)
- 🟢 Adding new endpoints or fields to a response (usually NOT breaking)
Note
8️⃣ Writing the Subject Line
The subject line is the most important part of your commit message. It's what people see ingit log --oneline, GitHub pull requests, email notifications, and changelogs. It must be clear, concise, and informative on its own.
Subject Line Formula
subject line formula
<type>(<scope>): <imperative verb> <what was changed>
Examples:
feat(auth): add Google OAuth2 login
fix(cart): correct total when coupon is applied
docs(readme): add deployment instructions for Heroku
refactor(api): extract pagination logic into utility function
test(users): add unit tests for password reset flow
perf(search): cache product query results with Redis
build(deps): upgrade Express from 4.18 to 5.0Imperative Verbs to Use
| ✅ Use These (Imperative) | ❌ Not These (Past / Progressive) |
|---|---|
| Add | Added / Adding |
| Fix | Fixed / Fixing |
| Update | Updated / Updating |
| Remove | Removed / Removing |
| Refactor | Refactored / Refactoring |
| Improve | Improved / Improving |
| Implement | Implemented / Implementing |
| Rename | Renamed / Renaming |
| Move | Moved / Moving |
| Replace | Replaced / Replacing |
| Extract | Extracted / Extracting |
| Merge | Merged / Merging |
Subject Line Checklist
- ☑️ Starts with a type (feat, fix, etc.)
- ☑️ Has an optional scope in parentheses
- ☑️ Uses imperative mood ("add" not "added")
- ☑️ Capitalized after the colon
- ☑️ No period at the end
- ☑️ 72 characters or fewer
- ☑️ Specific enough to understand without reading the diff
9️⃣ Writing the Body
The body is optional but powerful. Not every commit needs one — simple, obvious changes don't. But for anything non-trivial, the body is where you give future readers the context they need to understand your decision.
What Belongs in the Body
- 🧠 Why this change was made — the reasoning behind the decision
- 🔍 What problem it solves or what requirement it fulfills
- ⚖️ Alternatives considered and why they were rejected
- ⚠️ Side effects or caveats that reviewers should know about
- 📊 Before/after comparison for performance changes
- 🔗 Links to discussions, RFCs, Stack Overflow answers, or documentation
What Does NOT Belong in the Body
- ❌ A restatement of what the diff already shows
- ❌ A list of files changed (git already tracks this)
- ❌ The name of the person who made the change (git already tracks this)
- ❌ The date of the change (git already tracks this)
Body Examples
good body — explains why
fix(auth): prevent race condition in concurrent login requests
The session creation handler was not atomic — when two login
requests arrived within milliseconds of each other for the same
user, both would pass the "session exists?" check, and both would
create new session records. This resulted in duplicate sessions
and unpredictable behavior on subsequent requests.
The fix wraps the check-and-create operation in a database
transaction with a unique index on (user_id, session_token) to
enforce atomicity at the DB level.
Fixes #301good body — explains trade-offs
perf(search): replace linear scan with inverted index
The product search was performing a full table scan (O(n)) on
every query, which caused unacceptable response times as the
catalog grew past 50k products (avg 2.3s per search).
We now maintain an inverted index in Redis that maps search tokens
to product IDs. Search queries are O(1) lookups in the index.
Benchmark results:
Before: 2,300ms avg (50k products)
After: 18ms avg (50k products)
The trade-off is that product updates now require invalidating and
rebuilding the index entries for that product. This adds ~5ms to
write operations, which is acceptable for our read-heavy workload.
See: https://redis.io/docs/data-types/sorted-sets/good body — explains context
docs(contributing): add section on commit message conventions
Several recent PRs had commit messages that didn't follow the
project's Conventional Commits format, which broke the automated
changelog generation.
This adds a dedicated section to CONTRIBUTING.md explaining:
- The required type/scope/description format
- How to install and use Commitizen interactively
- What the CI Commitlint check validates
- Common mistake examples with correct alternatives🔟 Writing the Footer
The footer comes after the body, separated by a blank line. It contains structured metadataabout the commit — references to issues, breaking change descriptions, and attribution.
Footer Tokens
| Token | Purpose | Example |
|---|---|---|
| Closes #N | Close a GitHub/GitLab issue when merged | Closes #142 |
| Fixes #N | Same as Closes — fix a bug issue | Fixes #87 |
| Resolves #N | Same as Closes — more general | Resolves #204 |
| Refs #N | Reference without closing | Refs #55, #56 |
| BREAKING CHANGE: | Describe a breaking change in detail | BREAKING CHANGE: renamed X to Y |
| Co-authored-by: | Credit a co-author (pair programming) | Co-authored-by: Alice <alice@example.com> |
| Reviewed-by: | Credit a reviewer | Reviewed-by: Bob Smith |
| See-also: | Link to related commits or PRs | See-also: #189 |
Footer Examples
footer examples
# Close one issue
Closes #142
# Close multiple issues
Closes #142, Closes #167, Fixes #201
# Reference without closing
Refs #55
# Breaking change with detailed description
BREAKING CHANGE: The config file format has changed from JSON to
YAML. Run "myapp migrate-config" to automatically convert your
existing config.json to config.yaml.
# Multiple footers
Closes #142
Co-authored-by: Jane Doe <jane@example.com>
Reviewed-by: John Smith <john@example.com>
# Combining everything
BREAKING CHANGE: removed the --verbose flag; use --log-level=debug instead
Closes #88
Co-authored-by: Alice <alice@example.com>1️⃣1️⃣ Good vs Bad Examples
❌ Bad — Too Vague
bad commits
fix stuff
update
changes
WIP
asdf
misc updates
temp
final
final2
FINAL (actually final)
refactor things
improvements
bug fix
tweaks❌ Bad — Wrong Tense / Too Long / No Type
more bad commits
# Past tense (should be imperative)
Fixed the authentication bug that caused users to be logged out
# Too long and vague
Made some changes to the way the application handles user input
in various places throughout the codebase to improve stability
# No type prefix
Add new feature for uploading profile pictures to user accounts
# Explains HOW, not WHY (the diff already shows how)
Changed the for loop to a while loop and moved the counter
variable declaration outside the loop body✅ Good — Clear, Typed, Scoped, Imperative
good commits
feat(auth): add two-factor authentication via SMS
fix(cart): prevent negative quantity when decrementing items
docs(api): add rate limiting examples to quickstart guide
refactor(db): extract query builder into standalone module
test(orders): add edge case tests for same-day delivery cutoff
perf(images): serve WebP format to browsers that support it
build(deps): upgrade Mongoose from 7.x to 8.x
ci(deploy): add production smoke tests after deployment
chore(cleanup): remove unused utility functions from helpers.js
style(lint): apply ESLint auto-fix across entire codebase✅ Good — Full Commit with Body and Footer
complete good commit
fix(payments): retry failed Stripe webhooks with exponential backoff
Stripe webhook delivery can fail transiently due to network issues
or our server being temporarily unavailable. Previously, a failed
webhook was simply logged and dropped, causing orders to get stuck
in "payment pending" state even after successful payment.
This adds a webhook retry queue using Bull (backed by Redis) that:
- Retries up to 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s)
- Sends an alert to Slack if all retries are exhausted
- Stores retry attempts in the webhook_log table for debugging
The Bull dashboard is accessible at /admin/queues for monitoring.
Closes #312
Refs #298
Co-authored-by: Priya Patel <priya@example.com>1️⃣2️⃣ Commit Message Templates
Git lets you set a commit message template that pre-fills the editor whenever you rungit commit. This is great for reminding yourself and your team of the required format.
Creating a Template File
.gitmessage
# <type>(<scope>): <subject> [max 72 chars]
# |<---- Using a Maximum Of 72 Characters --->|
# --- WHY is this change needed? ---
# (explain the motivation, not the implementation)
# --- HOW does it address the issue? ---
# (optional — if non-obvious)
# --- What side effects does this change have? ---
# (optional — caveats, risks, dependencies)
# --- Footer ---
# Closes #ISSUE_NUMBER
# BREAKING CHANGE: describe what breaks and how to migrate
# Co-authored-by: Name <email>
# ---- TYPE OPTIONS ----
# feat: New feature
# fix: Bug fix
# docs: Documentation only
# style: Formatting, no logic change
# refactor: Code change that's neither fix nor feature
# perf: Performance improvement
# test: Adding or updating tests
# build: Build system or dependency changes
# ci: CI/CD configuration changes
# chore: Maintenance, no src or test file changes
# revert: Reverting a previous commitConfiguring Git to Use the Template
terminal
# Set template for just this repository
git config commit.template .gitmessage
# Set template globally for all repositories
git config --global commit.template ~/.gitmessage
# Verify the config
git config --global --list | grep templatePer-Project Template with .editorconfig
terminal
# You can also set it in your project's .git/config
# (done automatically by the above command without --global)
# Now every 'git commit' in this repo opens the template
git commit
# Opens your editor pre-filled with the template1️⃣3️⃣ Atomic Commits — What to Include in One Commit
An atomic commit is a commit that makes exactly one logical change. It is self-contained — it could be applied, reverted, or cherry-picked independently without breaking anything else.
The Atomic Commit Principle
- ✅ One commit = one logical change = one clear message
- ✅ The codebase should be in a working state after every commit
- ✅ Tests should pass after every commit (especially before pushing)
- ❌ Don't mix a feature and a bug fix in one commit
- ❌ Don't mix a refactor and a behavior change in one commit
- ❌ Don't batch unrelated changes together
Staging Parts of a File
If you've changed multiple unrelated things in one file, use git add -p (patch mode) to selectively stage only the changes that belong to the current commit.
terminal
# Interactively stage chunks of changes
git add -p filename.js
# Git will show each "hunk" and ask:
# y = stage this hunk
# n = skip this hunk
# s = split hunk into smaller pieces
# e = manually edit the hunk
# q = quit
# Stage parts of all files
git add -p
# Then commit only the staged part
git commit -m "fix(cart): prevent negative item quantity"
# Stage the remaining changes separately
git add -p
git commit -m "refactor(cart): extract quantity validation into helper"When Atomic Commits Break Down — WIP Commits
terminal
# During development it's ok to make messy WIP commits locally
git commit -m "wip: experimenting with new pagination approach"
git commit -m "wip: still broken, investigating"
git commit -m "wip: found the issue, fixing"
git commit -m "fix(pagination): correct page offset calculation"
# Before pushing, squash WIP commits into a clean history
git rebase -i HEAD~4
# In the interactive editor, mark WIP commits as 'squash' or 'fixup'
# to merge them into the final clean commitNote
1️⃣4️⃣ Branch Naming and Its Relation to Commits
Branch names and commit messages work together to tell the story of a change. A consistent branch naming convention makes it easy to link branches to issues and commits to features.
Branch Naming Convention
branch naming format
<type>/<issue-number>-<short-description>
Examples:
feat/142-google-oauth-login
fix/301-session-race-condition
docs/55-update-api-reference
refactor/189-extract-payment-module
chore/cleanup-unused-importsBranch → Commits → PR Relationship
terminal
# Branch created for a feature
git checkout -b feat/142-google-oauth-login
# Commits on the branch tell the story of how the feature was built
git commit -m "feat(auth): scaffold GoogleStrategy with Passport.js"
git commit -m "feat(auth): add /auth/google and /auth/google/callback routes"
git commit -m "feat(auth): store Google profile ID in users table"
git commit -m "test(auth): add integration tests for OAuth flow"
git commit -m "docs(auth): update README with Google OAuth setup steps"
# PR title mirrors the branch / first commit
# PR description can reference: Closes #142
# After merge, the PR and commits are linked to issue #142 automaticallyConnecting Commits to Issues
| Platform | Auto-Close Keywords in Footer |
|---|---|
| GitHub | Closes, Fixes, Resolves |
| GitLab | Closes, Fixes, Implements |
| Jira | Include the Jira key in branch/commit: feat/PROJ-123-add-login |
| Linear | Include the Linear ID: feat/ENG-456-dark-mode |
1️⃣5️⃣ Commit Messages for Different Scenarios
Initial Commit
initial commit
# The very first commit in a project
git commit -m "chore: initial project setup"
# Or more descriptive
git commit -m "chore: scaffold Next.js app with TypeScript and Tailwind"Dependency Updates
dependency updates
build(deps): upgrade React from 18.2.0 to 18.3.1
build(deps): add zod for runtime schema validation
build(deps): remove unused lodash dependency
build(deps-dev): upgrade ESLint from 8.x to 9.x
build(deps): pin axios to 1.6.8 due to security advisory CVE-2024-XXXXMerge and Rebase Commits
merge commits
# Default merge commit (auto-generated by Git)
Merge branch 'feat/142-google-oauth' into main
# Squash merge — combine all branch commits into one
feat(auth): add Google OAuth2 login (#142)
# After squash merge, the subject is the PR title
# Body can summarize the key commits from the branchHotfix Commits
hotfix commit
fix(payments): prevent double charge on network timeout
A race condition was causing payments to be submitted twice when
the network timed out during the POST /charge request. The client
retried the request while the original was still processing.
Added idempotency keys to all Stripe API calls using the order ID
as the key. Stripe deduplicates requests with the same key within
a 24-hour window.
Closes #HOTFIX-2024-03-15
Severity: P0 — ProductionReverting a Commit
revert commits
# Git auto-generates this when you run: git revert <hash>
revert: feat(auth): add Google OAuth2 login
This reverts commit a1b2c3d4e5f6g7h8.
Reason: The Google OAuth credentials were accidentally committed
to the repository in this change. The commit has been removed from
history via force push and the credentials have been rotated.
See security incident report #INC-2024-007.Documentation-only Changes
docs commits
docs(readme): add Docker deployment instructions
docs(api): document all query parameters for /search endpoint
docs(changelog): add release notes for v2.1.0
docs(contributing): update commit message guide with new examples
docs(jsdoc): add type annotations to utility functionsRefactoring
refactor commits
refactor(auth): extract token validation into middleware
refactor(db): replace raw SQL queries with Knex query builder
refactor(api): rename 'users' endpoints to 'accounts' for clarity
refactor(components): decompose 500-line Dashboard into smaller components
refactor: migrate from CommonJS require() to ES module importsConfiguration and Infrastructure
config commits
chore(config): add .nvmrc specifying Node.js 20 LTS
ci(github): add PR lint check for conventional commit format
build(docker): create multi-stage production Dockerfile
ci(deploy): configure zero-downtime blue-green deployment
chore(gitignore): add .env.local and dist/ to .gitignore1️⃣6️⃣ Tools — Commitizen, Commitlint, Husky
The best way to enforce commit message quality across a team is with automated tooling. These three tools work together to make conventional commits easy and enforceable.
🧙 Commitizen — Interactive Commit Builder
Commitizen replaces git commit with an interactive CLI wizard that asks you to choose a type, scope, subject, body, and footer step by step. It's impossible to produce a malformed commit using it.
terminal
# Install globally
npm install -g commitizen
# Initialize in your project (adds cz-conventional-changelog adapter)
npx commitizen init cz-conventional-changelog --save-dev --save-exact
# Use 'git cz' instead of 'git commit'
git add .
git cz
# Commitizen then prompts you:
# ? Select the type of change: (Use arrow keys)
# feat: A new feature
# fix: A bug fix
# docs: Documentation only changes
# ...
# ? What is the scope of this change? (e.g. auth, payments): auth
# ? Write a short, imperative description: add Google OAuth login
# ? Provide a longer description (optional):
# ? Are there any breaking changes? No
# ? Does this change affect any open issues? Yes
# ? Add issue references: Closes #142package.json
{
"scripts": {
"commit": "cz"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}🚔 Commitlint — Validate Commit Messages
Commitlint checks your commit messages against a set of rules. It integrates with Husky to run automatically on every git commit, rejecting non-conforming messages before they enter the history.
terminal
# Install commitlint
npm install --save-dev @commitlint/cli @commitlint/config-conventionalcommitlint.config.js
module.exports = {
// Extend the conventional commits ruleset
extends: ['@commitlint/config-conventional'],
// Override or add custom rules
rules: {
// Enforce specific types only
'type-enum': [
2, 'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']
],
// Subject max length
'subject-max-length': [2, 'always', 100],
// Scope must be lowercase
'scope-case': [2, 'always', 'lower-case'],
// Subject must not end with a period
'subject-full-stop': [2, 'never', '.'],
// Subject must be in sentence case (or lower-case)
'subject-case': [0], // disabled — use your preference
},
}🐶 Husky — Git Hooks Made Easy
Husky lets you run scripts on Git events. Use it to run Commitlint on the commit-msg hook and tests/linting on the pre-commit hook.
terminal
# Install Husky
npm install --save-dev husky
# Initialize Husky (creates .husky/ directory)
npx husky init
# Add commit-msg hook to run commitlint
echo "npx --no -- commitlint --edit $1" > .husky/commit-msg
# Add pre-commit hook to run linting
echo "npm run lint" > .husky/pre-commit
# Add pre-push hook to run tests
echo "npm test" > .husky/pre-pushpackage.json
{
"scripts": {
"prepare": "husky"
},
"devDependencies": {
"husky": "^9.0.0",
"@commitlint/cli": "^19.0.0",
"@commitlint/config-conventional": "^19.0.0",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0"
}
}Complete Setup in One Go
terminal
# Install all tools
npm install --save-dev husky @commitlint/cli @commitlint/config-conventional commitizen cz-conventional-changelog
# Initialize
npx husky init
npx commitizen init cz-conventional-changelog --save-dev --save-exact
# Create commitlint config
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
# Create Husky hooks
echo "npx --no -- commitlint --edit $1" > .husky/commit-msg
chmod +x .husky/commit-msg
# Now any non-conventional commit is automatically rejected!
git commit -m "stuff"
# ✖ subject may not be empty [subject-empty]
# ✖ type may not be empty [type-empty]1️⃣7️⃣ Automating Changelogs and Versioning
The real payoff of conventional commits is automated changelogs and semantic versioning. Tools can read your commit history and generate a release with the correct version number and a human-readable changelog automatically.
standard-version
terminal
# Install
npm install --save-dev standard-version
# Add to package.json scripts
# "release": "standard-version"
# Run a release
npm run release
# → Analyzes commits since last tag
# → Bumps version in package.json (patch/minor/major based on commit types)
# → Generates/updates CHANGELOG.md
# → Creates a version commit and git tag
# Dry run to preview
npm run release -- --dry-run
# Force a specific version bump
npm run release -- --release-as minor
npm run release -- --release-as 2.0.0semantic-release (Fully Automated)
terminal
# Install
npm install --save-dev semantic-release
# Configure in .releaserc.json
# semantic-release runs in CI and:
# 1. Analyzes commits since last release
# 2. Determines version bump (feat→minor, fix→patch, BREAKING CHANGE→major)
# 3. Generates release notes
# 4. Creates a GitHub/GitLab release
# 5. Publishes to npm
# 6. Updates CHANGELOG.md
# All fully automatic — no human step needed!.releaserc.json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}]
]
}How Commit Types Map to Version Bumps
| Commit Type | Version Bump | Example |
|---|---|---|
| BREAKING CHANGE (any type) | MAJOR (x.0.0) | 1.4.2 → 2.0.0 |
| feat | MINOR (0.x.0) | 1.4.2 → 1.5.0 |
| fix, perf, revert | PATCH (0.0.x) | 1.4.2 → 1.4.3 |
| docs, style, refactor, test, build, ci, chore | No bump | 1.4.2 → 1.4.2 |
1️⃣8️⃣ Emoji in Commit Messages — Gitmoji
Gitmoji is a convention that uses emoji at the start of commit messages to add visual context. It makes git logs more scannable and fun. It can be used alongside or instead of conventional commit type prefixes.
gitmoji.dev — full emoji referenceMost Common Gitmoji
| Emoji | Code | Meaning |
|---|---|---|
| ✨ | :sparkles: | New feature |
| 🐛 | :bug: | Bug fix |
| 🔥 | :fire: | Remove code or files |
| 📝 | :memo: | Documentation |
| 🎨 | :art: | Code structure / formatting |
| ♻️ | :recycle: | Refactor code |
| ⚡ | :zap: | Performance improvement |
| 🔒 | :lock: | Fix security issue |
| ✅ | :white_check_mark: | Add or update tests |
| 🚀 | :rocket: | Deploy to production |
| 💄 | :lipstick: | UI / styles update |
| 🔧 | :wrench: | Configuration files |
| 📦 | :package: | Dependency update |
| 🚨 | :rotating_light: | Fix linter warnings |
| ⏪ | :rewind: | Revert changes |
| 💡 | :bulb: | Add source comments |
| 🌐 | :globe_with_meridians: | Internationalization |
| 🔀 | :twisted_rightwards_arrows: | Merge branches |
| 🎉 | :tada: | Initial commit |
| 💥 | :boom: | Breaking change |
Gitmoji Examples
gitmoji commits
✨ feat(auth): add Google OAuth2 login
🐛 fix(cart): correct total when coupon applied
📝 docs(api): update authentication guide
♻️ refactor(db): extract query builder module
⚡ perf(search): add Redis caching for queries
✅ test(orders): add integration tests for checkout
📦 build(deps): upgrade React to 18.3
🔒 fix(auth): patch JWT token expiry vulnerability
💥 feat(api)!: remove deprecated v1 endpoints
🎉 chore: initial commitUsing the Gitmoji CLI
terminal
# Install
npm install -g gitmoji-cli
# Use instead of git commit
gitmoji -c
# Prompts you to:
# 1. Choose an emoji from the list
# 2. Enter a title
# 3. Enter a message (optional)
# 4. Add issue/PR references (optional)Note
1️⃣9️⃣ Team Conventions and Contributing Guides
Good commit messages only happen consistently when the whole team agrees on the rulesand those rules are documented and enforced. This is what a CONTRIBUTING.md file is for.
What to Include in CONTRIBUTING.md
- Commit message format — type/scope/subject structure, examples, rules
- Branch naming convention — format, examples, how to link to issues
- How to run linting and tests before committing
- Pull request process — description template, required reviewers, CI requirements
- Tooling setup — how to install and use Commitizen, where to find the template
CONTRIBUTING.md Commit Section Template
CONTRIBUTING.md
## Commit Message Guidelines
We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification.
### Format
<type>(<scope>): <subject>
[optional body]
[optional footer]
### Types
| Type | Description |
|----------|----------------------------------------|
| feat | A new feature |
| fix | A bug fix |
| docs | Documentation only changes |
| style | Formatting, no logic change |
| refactor | Code change, neither fix nor feature |
| perf | Performance improvement |
| test | Adding or updating tests |
| build | Build system or dependency changes |
| ci | CI/CD configuration changes |
| chore | Maintenance, no src or test changes |
### Rules
- Use the imperative mood: "add feature" not "added feature"
- Limit the subject to 72 characters
- Do not end the subject with a period
- Separate subject from body with a blank line
### Tooling
Install Commitizen for an interactive commit builder:
npm install -g commitizen
git cz (instead of git commit)
Commit messages are validated automatically by Commitlint
via the pre-commit Husky hook.
### Examples
feat(auth): add two-factor authentication
fix(cart): prevent negative quantity on decrement
docs(api): add pagination examples to referencePull Request Template
.github/pull_request_template.md
## Summary
<!-- What does this PR do? Why is it needed? -->
## Changes
<!-- List the key changes made -->
-
-
-
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
## Testing
<!-- How was this tested? -->
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] Tested manually in local environment
## Related Issues
Closes #2️⃣0️⃣ Common Mistakes to Avoid
❌ Mistake 1 — The "and" commit
If your commit message contains the word "and", you're probably combining two separate changes that should be two separate commits.
mistake vs fix
# BAD — two separate concerns in one commit
fix(auth): fix login redirect bug and update password validation
# GOOD — split into two atomic commits
fix(auth): correct redirect URL after successful login
fix(auth): enforce minimum 8-character password requirement❌ Mistake 2 — Vague "update" commits
mistake vs fix
# BAD — tells you nothing
update user model
changes to profile page
refactoring
# GOOD — specific and informative
refactor(users): extract address fields into separate UserAddress model
feat(profile): add avatar upload with image cropping
refactor(checkout): replace switch statement with strategy pattern❌ Mistake 3 — Commits that describe the diff, not the intent
mistake vs fix
# BAD — just describes the code change
change the for loop to map()
move UserService to a new file
add null check before calling .length
# GOOD — explains why
refactor(notifications): simplify delivery loop with Array.map
refactor(services): separate UserService into its own module for testability
fix(products): prevent crash when product has no variants❌ Mistake 4 — Giant commits
mistake vs fix
# BAD — one massive commit with 47 files changed
feat: implement full e-commerce checkout flow
# GOOD — incremental atomic commits
feat(cart): add shopping cart store with Zustand
feat(cart): implement add/remove/update item actions
feat(checkout): add shipping address form with validation
feat(checkout): integrate Stripe Elements for payment
feat(checkout): create order confirmation page
test(checkout): add end-to-end tests for full purchase flow❌ Mistake 5 — Committing broken code
mistake vs fix
# BAD — committing code that doesn't build or fails tests
git commit -m "feat(auth): WIP adding OAuth, tests broken"
# GOOD — only commit working code
# Use git stash or feature flags to keep broken work off commits
git stash # save incomplete work
git commit -m "feat(auth): scaffold OAuth route handlers"
git stash pop # restore incomplete work❌ Mistake 6 — Ignoring breaking changes
mistake vs fix
# BAD — breaking change with no warning
feat(api): update user endpoint response format
# GOOD — explicitly mark and describe the breaking change
feat(api)!: flatten user response to remove nested 'data' wrapper
BREAKING CHANGE: The /api/users/:id endpoint previously returned
{ data: { id, name, email } }. It now returns { id, name, email }
directly. Update all client-side destructuring accordingly.❌ Mistake 7 — Using past tense
mistake vs fix
# BAD — past tense
Fixed the login bug
Added user avatar support
Updated the documentation
# GOOD — imperative (present) tense
fix(auth): prevent session expiry on active users
feat(profile): add avatar upload with cropping
docs(api): update authentication examplesQuick Reference — Mistakes Cheatsheet
| ❌ Don't | ✅ Do |
|---|---|
| Use past tense (Fixed, Added) | Use imperative (Fix, Add) |
| Write vague messages (stuff, WIP) | Be specific about what changed and why |
| Mix unrelated changes in one commit | One logical change per commit |
| Describe the code change | Explain the reason for the change |
| Skip the type prefix | Always use feat:, fix:, etc. |
| Commit broken code | Only commit code that builds and passes tests |
| Ignore breaking changes | Always flag with ! or BREAKING CHANGE: |
| Exceed 72 characters in subject | Keep subject concise and under 72 chars |
| End subject with a period | No punctuation at end of subject line |