Git Commit Messages

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

Write commit messages that stay useful: the imperative summary, the body that records why, Conventional Commits, and enforcing it with commitlint.

Why Good Commit Messages Matter

A commit message is part of your project's long-term memory. It explains why a change exists and helps later readers understand what happened without opening every file diff first.

Good messages improve:

  • history review
  • git blame investigations
  • changelog generation
  • release notes
  • team communication

A weak message like fix stuff quickly becomes useless. A stronger message like fix(auth): handle expired refresh tokens gives useful context immediately.

Tip: Write commit messages for the person reading them months later, not just for the moment you type them.

The 7 Rules of a Great Commit Message

A widely used set of guidelines includes these rules:

  1. Separate subject from body with a blank line
  2. Keep the subject around 50 characters
  3. Capitalize the subject line
  4. Do not end the subject with a period
  5. Use the imperative mood
  6. Wrap the body near 72 characters
  7. Explain what and why in the body

Example:

Add validation for release tag names

Reject tag names that do not match the release policy. This prevents
invalid CI/CD releases from being published accidentally.

Imperative Mood

Subjects like Add, Fix, and Refactor read naturally after the phrase "If applied, this commit will...".

Good vs Bad Messages

Bad examples:

stuff
misc fixes
updated files

Better examples:

docs: clarify GitHub workflow steps
fix(ci): restore cache key for Node 22 builds
feat(api): add pagination to audit log endpoint

The better versions are specific and searchable.

Conventional Commits

Conventional Commits define a standard format that both people and tooling can understand.

Basic pattern:

type(scope): description

Common types include:

  • feat:
  • fix:
  • docs:
  • chore:
  • refactor:
  • test:
  • build:
  • ci:

Examples:

feat(auth): add SSO login flow
fix(ui): prevent navbar overlap on mobile
docs(git): add submodule troubleshooting section

Breaking Changes

A breaking change can be marked with ! or with a BREAKING CHANGE: note in the body.

feat(api)!: rename userId field to accountId

BREAKING CHANGE: Clients must send accountId instead of userId.

Note: Conventional Commits are most useful when the whole team follows the same format and a hook or CI job validates it.

Conventional Commits and Semantic Versioning

Conventional Commits connect well to semantic versioning:

  • MAJOR for breaking changes
  • MINOR for backward-compatible features
  • PATCH for backward-compatible fixes

A common mapping is:

  • feat: → minor bump
  • fix: → patch bump
  • BREAKING CHANGE: or ! → major bump

That makes commit history useful for release automation.

Helpful Tools

commitlint

commitlint checks whether commit messages follow your rules.

npm install --save-dev @commitlint/cli @commitlint/config-conventional

Example config:

cat > commitlint.config.js <<'EOF'
module.exports = { extends: ['@commitlint/config-conventional'] };
EOF

Test a message:

echo "fix: correct cache key" | npx commitlint

Teams often run this from a commit-msg hook.

semantic-release

semantic-release reads structured commit history and automates versioning and release notes.

npm install --save-dev semantic-release

A common pattern is:

  1. developers write Conventional Commits
  2. commitlint validates them
  3. CI runs semantic-release on main
  4. versioning and changelogs are generated automatically

What You Should Remember

Good commit messages make history easier to search, review, debug, and release. The seven classic rules improve clarity, while Conventional Commits provide a machine-readable format that works well with tools like commitlint and semantic-release. Better commit messages are a small habit with long-term payoff.

Test Your Understanding

Exercise

Exercise 1: Why commit messages matter

Which is the best reason strong commit messages improve long-term project maintenance?

Exercise

Exercise 2: Recognizing a Conventional Commit

Which option follows the Conventional Commits pattern from the lesson?

Exercise

Exercise 3: Linking commits to version bumps

According to the lesson, which type of change most directly signals a MAJOR semantic version bump?


Frequently Asked Questions

What makes a good Git commit message?

A short imperative summary under about 50 characters, "Fix login redirect loop", not "Fixed" or "Fixes", then a blank line, then a body explaining why the change was needed if it is not obvious. The summary appears in git log --oneline, on pull requests and in git blame, so it has to stand alone. The body is where the value is: the diff already shows what changed, and only the message can record the reasoning, the alternative you rejected, or the ticket that prompted it.

Why should commit messages use the imperative mood?

Because Git itself does. Its generated messages read "Merge branch 'main'" and "Revert 'Add caching'", so an imperative summary sits consistently alongside them. The practical test is that the message should complete the sentence "If applied, this commit will…", "Add rate limiting" reads correctly, "Added rate limiting" does not. It is a convention rather than a rule, and its real benefit is consistency: a log where every line is phrased the same way is far quicker to scan.

What are Conventional Commits and do I need them?

A format that prefixes the summary with a type and optional scope: feat(auth): add password reset, fix: handle empty cart, chore(deps): bump axios. The point is machine readability. Tooling can derive a changelog and the next semantic version from the log without anyone maintaining either by hand. You need them if you want automated releases or generated changelogs. If you do not, the discipline still helps, but plain clear messages are perfectly adequate.

How do Conventional Commits map to semantic versioning?

fix: produces a patch bump, feat: a minor bump, and a breaking change a major bump. A breaking change is marked either with ! after the type, feat!: drop Node 18 support, or a BREAKING CHANGE: footer in the body. Tools such as semantic-release read the commits since the last tag, work out the highest bump implied, then tag the release and write the changelog. That is why the format is strict: the version number is derived from it.

How do I change a commit message I already pushed?

git commit --amend rewrites the most recent message, but amending changes the commit hash, so a pushed commit then needs git push --force-with-lease. That is acceptable on your own feature branch and not on a shared one, where everyone who pulled is left on a commit that no longer exists. For older commits, git rebase -i HEAD~n and marking them reword does the same job with the same caveat. On main, the honest answer is to leave it.

How do I enforce commit message rules across a team?

Use commitlint with a commit-msg hook, installed through Husky so the hook lives in the repository and arrives with a clone. Raw .git/hooks files are not version-controlled and so never reach anyone else. That rejects a badly formatted message before the commit is created. Back it up with a server-side check in CI, since local hooks can be skipped with --no-verify; a pull request check that validates the commits is what actually makes the convention stick.

Continue Learning

Explore Related Topics

When it breaks

Related Resources