💬 Commit Messages — Complete Tutorial

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.

>>A commit message is a letter to your future self and your teammates. Write it with the same care you write your code.

📖 Table of Contents

  1. Why Commit Messages Matter
  2. The Anatomy of a Commit Message
  3. The 7 Golden Rules of Commit Messages
  4. Commit Types — What Changed?
  5. The Conventional Commits Specification
  6. Scopes — Where Did It Change?
  7. Breaking Changes
  8. Writing the Subject Line
  9. Writing the Body
  10. Writing the Footer
  11. Good vs Bad Examples
  12. Commit Message Templates
  13. Atomic Commits — What to Include in One Commit
  14. Branch Naming and Its Relation to Commits
  15. Commit Messages for Different Scenarios
  16. Tools — Commitizen, Commitlint, Husky
  17. Automating Changelogs and Versioning
  18. Emoji in Commit Messages — Gitmoji
  19. Team Conventions and Contributing Guides
  20. 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
  • 🐛 Debugginggit 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 misc

When 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

📌 A project's git history is its autobiography. Future you — six months from now at 2am debugging a production incident — will either thank you or curse you based on the commit messages you write today.

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

PartRequired?PurposeMax Length
Subject✅ AlwaysOne-line summary of what changed50–72 characters
Body⚪ OptionalExplains WHY and HOW in detail72 chars per line
Footer⚪ OptionalIssue references, breaking changes, co-authorsNo 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.

  1. 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.
  2. 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.
  3. Capitalize the subject line
    Start with an uppercase letter: Fix login redirect bug not fix login redirect bug.
  4. 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.
  5. 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".
  6. 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.
  7. 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

💡 A useful mental test: your subject line should complete this sentence — "If applied, this commit will..."For example: "If applied, this commit will fix the login redirect bug". If the sentence sounds awkward, rewrite it.

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.

TypeWhen to UseVersion Bump
featA new feature visible to users or consumers of an APIMINOR (1.x.0)
fixA bug fix that corrects incorrect behaviorPATCH (1.0.x)
docsDocumentation only changes — README, JSDoc, commentsNone
styleCode formatting, whitespace, semicolons — no logic changeNone
refactorCode restructuring that neither fixes a bug nor adds a featureNone
perfPerformance improvement — faster queries, reduced memory usagePATCH
testAdding or updating tests — no production code changeNone
buildBuild system, bundler, dependency changes (webpack, npm, gradle)None
ciCI/CD configuration changes (GitHub Actions, CircleCI, Jenkins)None
choreMaintenance tasks that don't modify src or test filesNone
revertReverting a previous commitDepends
wipWork in progress — should be squashed before mergingNone

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)?
  → chore

5️⃣ 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.org

Full 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 TypeCommon Scopes
Web Appauth, ui, api, db, router, store, forms
React Appcomponents, hooks, pages, context, utils, styles
Node.js APIusers, orders, payments, middleware, models, routes
Monorepoweb, mobile, api, shared, admin, cli
Library/SDKcore, 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 checks

7️⃣ 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 structure

Method 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

⚠️ A breaking change triggers a MAJOR version bump (x.0.0) in semantic versioning. Make sure your commit message is explicit about what breaks and how to migrate.

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.0

Imperative Verbs to Use

✅ Use These (Imperative)❌ Not These (Past / Progressive)
AddAdded / Adding
FixFixed / Fixing
UpdateUpdated / Updating
RemoveRemoved / Removing
RefactorRefactored / Refactoring
ImproveImproved / Improving
ImplementImplemented / Implementing
RenameRenamed / Renaming
MoveMoved / Moving
ReplaceReplaced / Replacing
ExtractExtracted / Extracting
MergeMerged / 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 #301

good 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

TokenPurposeExample
Closes #NClose a GitHub/GitLab issue when mergedCloses #142
Fixes #NSame as Closes — fix a bug issueFixes #87
Resolves #NSame as Closes — more generalResolves #204
Refs #NReference without closingRefs #55, #56
BREAKING CHANGE:Describe a breaking change in detailBREAKING 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 reviewerReviewed-by: Bob Smith
See-also:Link to related commits or PRsSee-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 commit

Configuring 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 template

Per-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 template

1️⃣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 commit

Note

💡 Think of your local commit history as a draft. Clean it up with git rebase -i before pushing. Your pushed history should read like a clean story, not a stream of consciousness.

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-imports

Branch → 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 automatically

Connecting Commits to Issues

PlatformAuto-Close Keywords in Footer
GitHubCloses, Fixes, Resolves
GitLabCloses, Fixes, Implements
JiraInclude the Jira key in branch/commit: feat/PROJ-123-add-login
LinearInclude 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-XXXX

Merge 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 branch

Hotfix 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 — Production

Reverting 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 functions

Refactoring

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 imports

Configuration 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 .gitignore

1️⃣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 #142

package.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-conventional

commitlint.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-push

package.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.0

semantic-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 TypeVersion BumpExample
BREAKING CHANGE (any type)MAJOR (x.0.0)1.4.2 → 2.0.0
featMINOR (0.x.0)1.4.2 → 1.5.0
fix, perf, revertPATCH (0.0.x)1.4.2 → 1.4.3
docs, style, refactor, test, build, ci, choreNo bump1.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 reference

Most Common Gitmoji

EmojiCodeMeaning
: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 commit

Using 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

💡 Whether or not to use emoji is a team preference. They add visual richness but can look cluttered in some tools. If you use Conventional Commits tooling (commitlint, semantic-release), stick with the type prefix format — most tools don't parse emoji types.

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

  1. Commit message format — type/scope/subject structure, examples, rules
  2. Branch naming convention — format, examples, how to link to issues
  3. How to run linting and tests before committing
  4. Pull request process — description template, required reviewers, CI requirements
  5. 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 reference

Pull 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 examples

Quick 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 commitOne logical change per commit
Describe the code changeExplain the reason for the change
Skip the type prefixAlways use feat:, fix:, etc.
Commit broken codeOnly commit code that builds and passes tests
Ignore breaking changesAlways flag with ! or BREAKING CHANGE:
Exceed 72 characters in subjectKeep subject concise and under 72 chars
End subject with a periodNo punctuation at end of subject line
>>Every commit message is a gift to the next developer who reads it — and that developer is very often you, six months from now at 2am. Write generously. 🎁

Note

📌 What to Learn Next: Explore git log --oneline --graph --decorate for visualizing your commit history, git bisect for using your commit history to track down bugs, and git cherry-pick for applying individual commits across branches. A clean commit history makes all of these dramatically more powerful.