Komitly
BlogPricing
Home/Blog/Writing Better Commit Messages: A Practical Guide
WorkflowBeginner

Writing Better Commit Messages: A Practical Guide

Komitly·February 27, 2026·9 min read
commit messagesconventional commitsgit commitbest practicesAI commit
Loading article...

On this page

  • Why commit messages matter
  • Anatomy of a good commit message
  • Conventional Commits
  • Good vs. bad messages
  • Commit messages in Komitly
  • Tips and habits
  • Summary

Try it visually with Komitly

Stop memorizing commands. See your branches, commits, and merges in a beautiful visual interface.

Download Komitly — Free

Related articles

WorkflowBeginner

git stash: Save Work Without Committing

Learn how to use git stash to temporarily save changes. Covers stash, pop, apply, list, and managing multiple stashes with practical examples.

git stashgit stash popgit stash apply
6 min
WorkflowBeginner

Your First Pull Request: A Complete Workflow

Walk through the complete pull request workflow from branch to merge. Learn branch naming conventions, how to write a good PR, and manage the review process.

git switch -cgit push -ugit rebase
10 min
WorkflowBeginner

.gitignore: Keeping Secrets and Junk Out of Your Repo

Learn how to use .gitignore to exclude dependencies, build output, secrets, and OS files from your repository. Includes pattern syntax, templates by language, and how to untrack already committed files.

git rm --cachedgit check-ignoregit add
8 min

© 2026 Komitly

support@komitly.com
BlogPricingChangelogTermsPrivacy

Why commit messages matter

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
Terminal
# Set a global commit message template
$ git config --global commit.template ~/.gitmessage
# 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.