A Git history is only as useful as its commit messages. Six months from now, when a bug is traced to a specific commit, the message is the first thing you read. A good message explains why a change was made and saves hours of detective work. A bad message forces you to re-read every line of the diff to understand the intent.
Compare these two histories. Which one would you rather debug at 2 AM?
Komitly - Unhelpful history
f8a9b0cfixmain
e7b8c9dupdate
d6a7b8cWIP
c5d6e7fstuff
b4c5d6echanges
Komitly - Descriptive history
f8a9b0cfix(auth): prevent session expiry during checkoutmain
e7b8c9dfeat: add keyboard shortcuts to search
d6a7b8crefactor: extract price formatting utility
c5d6e7ffix: off-by-one error in pagination
b4c5d6efeat: add dark mode toggle
The same code changes, the same amount of work — but the second history tells a story. Each commit explains what happened and you can scan the log to find exactly the change you are looking for.
Write commit messages for the person who will read them six months from now. That person is usually you.
Anatomy of a good commit message
A commit message has up to three parts: a subject line, a body, and a footer. Only the subject line is required — the body and footer are optional but recommended for non-trivial changes.
Commit message structure
Subject line (50 chars or less, imperative mood)
← blank line
Body (72 chars per line, explain WHY not WHAT)
The diff already shows what changed. Use the body
to explain the motivation, context, and trade-offs.
Wrap at 72 characters so the message reads well in
terminals and git log.
Footer (references, breaking changes)
Closes #142
Co-authored-by: Alice <alice@example.com>
Subject line examples
# Good subjects — imperative, concise, specific
Add email verification for new signups
Fix off-by-one error in pagination
Remove deprecated v1 API endpoints
# Bad subjects — vague, past tense, or too long
Added some stuff
Fixing the thing that was broken in production when users tried to sign up
update
Subject + blank line + body
Fix race condition in WebSocket reconnection
← this blank line is critical
The client was attempting to send messages before
the connection was fully re-established, causing
silent message drops under high latency.
Body explains the why
Refactor database queries to use connection pooling
The previous approach opened a new connection for every
query, which caused timeouts under load (>50 concurrent
users). A connection pool with max 20 connections reduces
average query time from 120ms to 15ms.
Considered using a query cache instead, but the data
changes too frequently for caching to be effective.
Footer with references and breaking change
feat: add two-factor authentication
Users can now enable 2FA via authenticator apps.
TOTP tokens are validated server-side with a 30-second
window. Recovery codes are generated on enrollment.
BREAKING CHANGE: /api/login now returns a 2fa_required
status when 2FA is enabled. Clients must handle the
additional verification step.
Closes #89
Co-authored-by: Bob <bob@example.com>
Writing multi-line messages
For simple changes, the -m flag works fine. For anything with a body, run git commit without -m to open your editor:
Terminal
$ git commit
# Your editor opens — write the full message:
feat: add keyboard shortcuts to search modal
Users can now press / to focus the search input and Escape
to close the modal. Arrow keys navigate results and Enter
selects the highlighted item.
Closes #67
You can also set up a commit message template that pre-fills the format every time:
~/.gitmessage
# Commit message template (~/.gitmessage or per-repo)
# <type>(<scope>): <subject>
#
# <body>
#
# <footer>
# Types: feat, fix, docs, style, refactor, perf, test, chore
# Scope: optional, describes the area (auth, cart, api, ui)
# Subject: imperative mood, 50 chars max, no period
# Body: explain WHY, wrap at 72 chars
# Footer: Closes #issue, BREAKING CHANGE, Co-authored-by
# Now every git commit opens your editor with the template pre-filled
Conventional Commits
Conventional Commits is a widely adopted convention that adds a structured prefix to the subject line. The format is type(scope): description, where the type describes the kind of change and the scope (optional) describes the area affected.
Conventional Commit types
# Type prefixes for Conventional Commits
feat: A new feature visible to the user
fix: A bug fix
docs: Documentation changes only
style: Code style (formatting, semicolons) — no logic change
refactor: Code restructuring — no feature or fix
perf: Performance improvement
test: Adding or updating tests
chore: Maintenance (dependencies, CI, build scripts)
ci: CI/CD pipeline changes
build: Build system or external dependency changes
revert: Reverting a previous commit
Adding scope
The scope is optional but helpful in larger projects. It narrows down which part of the codebase was affected:
Scope examples
# With optional scope in parentheses
feat(auth): add OAuth2 login with Google
fix(cart): prevent duplicate items on rapid clicks
refactor(api): extract validation into middleware
docs(readme): add local development instructions
chore(deps): upgrade TypeScript to 5.4
test(search): add unit tests for fuzzy matching
Why use Conventional Commits?
Scannable history. You can instantly tell features from fixes from chores just by reading the prefix.
Automated changelogs. Tools can parse the prefixes to generate release notes automatically.
Semantic versioning.feat: bumps the minor version, fix: bumps the patch, BREAKING CHANGE bumps the major.
Team consistency. Everyone follows the same format, so code reviews focus on the change, not the message style.
Here is what a Conventional Commits history looks like in the terminal:
Terminal
Good vs. bad messages
The difference between a useful and a useless commit message is not length — it is specificity. A bad message describes what you did (the diff already shows that). A good message explains why you did it.
Bad vs. good messages
# ✗ BAD — What does this tell you six months from now?
fix
update stuff
WIP
changes
asdfgh
minor tweaks
addressed review comments
# ✓ GOOD — Clear, specific, useful
fix: resolve infinite loop when search query is empty
feat: add CSV export to analytics dashboard
refactor: replace moment.js with date-fns for smaller bundle
fix(auth): prevent session expiry during active checkout
chore: remove unused lodash dependency (saves 72KB)
perf: lazy-load product images below the fold
docs: add webhook payload examples to API guide
Describe the why, not the what
This is the single most important rule. The diff already shows every line that changed. Your message should provide context that the diff cannot:
What vs. why
# ✗ BAD — describes WHAT (the diff already shows that)
Change color from blue to red
Add if statement to check for null
Remove line 42
# ✓ GOOD — describes WHY
Fix low-contrast text failing WCAG AA on dark backgrounds
Prevent crash when user profile has no avatar set
Remove deprecated analytics tracking pixel
Compare these two terminal histories side by side — one is a wall of noise, the other tells a clear story:
Terminal — unhelpful
$ git log --oneline -8
f8a9b0c fix
e7b8c9d update
d6a7b8c WIP
c5d6e7f stuff
b4c5d6e changes
a3b4c5d asdfgh
9a2b3c4 fixed it
8f1a2b3 Initial commit
Terminal — descriptive
$ git log --oneline -8
f8a9b0c fix(auth): prevent session expiry during checkout
e7b8c9d feat: add keyboard shortcuts to search modal
d6a7b8c refactor: extract price formatting into utility
c5d6e7f fix: resolve off-by-one error in pagination
b4c5d6e docs: add API authentication guide
a3b4c5d feat: add dark mode toggle to settings
9a2b3c4 chore: upgrade React to v19
8f1a2b3 Initial commit
Commit messages in Komitly
Komitly makes writing good commit messages easier with several features built directly into the staging panel.
AI-generated commit messages
Click the AI button in the staging panel and Komitly analyzes your staged changes to generate a descriptive commit message. It reads the diff, understands the intent, and produces a message following your preferred format. You can accept it as-is, edit it, or regenerate.
This is especially useful for routine changes (dependency updates, renames, formatting) where writing the message manually feels tedious. For complex changes, use the AI suggestion as a starting point and refine the body to add context only you know.
Komitly - Staging Panel with AI commit message
Staged (3)
Msrc/hooks/useSearch.ts
Msrc/components/SearchModal.tsx
Asrc/utils/keyboard.ts
Changes (0)
Working tree clean
feat: add keyboard shortcuts to search modal
Commit
Commit templates
Komitly supports reusable commit templates that pre-fill the message format. Create templates for your team's conventions (Conventional Commits, ticket numbers, etc.) and select them from a dropdown in the staging panel. Templates can include variables like the current branch name and date.
Commit decomposer
Staged a large set of changes that should really be multiple commits? The Commit Decomposer uses AI to analyze your staged files and suggest how to split them into logical, focused commits — each with its own message. It helps you follow the "one commit = one change" principle even when you forgot to commit along the way.
Co-authors
If you pair-programmed or collaborated on a change, use the co-author selector in the staging panel. It autocompletes names from your Git history and appends the Co-authored-by trailer to the commit message footer automatically.
Writing good commit messages should not feel like a chore. Komitly's AI generates messages from your diff, templates enforce your team's format, and the decomposer helps split large changes into focused commits — so your history stays clean with minimal effort.
Tips and habits
Commit early, commit often. Smaller commits are easier to describe. If you find yourself writing "and" in the subject, split the commit.
Review the diff before writing the message. Run git diff --staged or use Komitly's diff viewer to see exactly what is staged. The message should match the actual changes, not what you think you changed.
Use the imperative mood. "Add", not "Added" or "Adds". The convention is that a commit message completes the sentence: "If applied, this commit will add search feature."
Do not end the subject with a period. It is a title, not a sentence. Every character counts when you have a 50-character budget.
Wrap the body at 72 characters. This ensures the message reads well in terminals, email clients, and git log. Most editors can be configured to wrap automatically.
Reference issues. Adding Closes #42 or Fixes #42 in the footer automatically closes the issue when the commit reaches the default branch.
Avoid "fix typo" commits on shared branches. Squash trivial fixups into the original commit with git commit --amend or interactive rebase before pushing.
Summary
Good commit messages turn your Git history from a wall of noise into a searchable, readable story. Here is a quick recap:
A commit message has a subject (50 chars, imperative mood), an optional body (explains the why, wrapped at 72 chars), and an optional footer (issue references, co-authors).
Conventional Commits add a type prefix (feat:, fix:, chore:, etc.) that makes the history scannable and enables automated changelogs.
Describe the why, not the what. The diff shows what changed — the message should explain the motivation.
In Komitly, the AI commit message generator writes messages from your diff, the commit decomposer splits large changes into focused commits, and commit templates enforce your team's format.
With clear, consistent commit messages, every git log, git blame, and code review becomes more productive. Next, learn how to undo mistakes safely with git reset and git restore.