A logo showing the text blog.marcnuri.com
Español
Home»Quality Engineering»Conventional Commits: A Complete Guide to Better Git Commit Messages

Recent Posts

  • Black Box vs White Box Testing: When to Use Each Approach
  • Fabric8 Kubernetes Client 7.4 is now available!
  • Kubernetes MCP Server Joins the Containers Organization!
  • MCP Tool Annotations: Adding Metadata and Context to Your AI Tools
  • Fabric8 Kubernetes Client 7.2 is now available!

Categories

  • Artificial Intelligence
  • Backend Development
  • Cloud Native
  • Engineering Insights
  • Frontend Development
  • JavaScript
  • Legacy
  • Operations
  • Personal
  • Pet projects
  • Quality Engineering
  • Tools

Archives

  • October 2025
  • September 2025
  • July 2025
  • May 2025
  • April 2025
  • March 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • August 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • July 2022
  • June 2022
  • May 2022
  • March 2022
  • February 2022
  • January 2022
  • December 2021
  • November 2021
  • October 2021
  • September 2021
  • August 2021
  • July 2021
  • January 2021
  • December 2020
  • November 2020
  • October 2020
  • September 2020
  • August 2020
  • July 2020
  • June 2020
  • May 2020
  • March 2020
  • February 2020
  • January 2020
  • December 2019
  • November 2019
  • October 2019
  • September 2019
  • July 2019
  • March 2019
  • November 2018
  • July 2018
  • June 2018
  • May 2018
  • April 2018
  • March 2018
  • February 2018
  • December 2017
  • October 2017
  • August 2017
  • July 2017
  • January 2017
  • December 2015
  • November 2015
  • December 2014
  • November 2014
  • October 2014
  • March 2014
  • February 2011
  • November 2008
  • June 2008
  • May 2008
  • April 2008
  • January 2008
  • November 2007
  • September 2007
  • August 2007
  • July 2007
  • June 2007
  • May 2007
  • April 2007
  • March 2007

Conventional Commits: A Complete Guide to Better Git Commit Messages

2019-11-03 in Quality Engineering tagged Git / Conventional Commits / Best Practices / Semantic Versioning (SemVer) / Changelog / CI by Marc Nuri | Last updated: 2025-12-06
Versión en Español

Introduction

Writing meaningful Git commit messages is one of those skills that separates junior developers from seasoned professionals. Yet, most of us have written commits like "fix bug", "update stuff", or the infamous "WIP" at some point. I know I have.

Conventional Commits is a specification that provides a lightweight convention for creating explicit, standardized commit messages. By following a simple structure, you can communicate the intent of your changes clearly, automate version bumping, and generate changelogs automatically.

In this guide, I'll explain what Conventional Commits is, how it works, why you should adopt it, and how I use it in my day-to-day development workflow.

What are Conventional Commits?

Conventional Commits is a specification for writing standardized commit messages that follow a specific format. The convention is designed to be both human-readable and machine-parseable, enabling automation tools to understand the nature of changes in your codebase.

The specification was created by Benjamin E. Coe in 2017, inspired by the need for clearer documentation around commit formats that were already being used by projects like Angular. The goal was to eliminate the tedium around version bumps and changelog management while encouraging developers to write more thoughtful commit messages.

The Commit Message Structure

A conventional commit message follows this structure:

commit-structure.txt
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Let's break down each component:

  • type: Describes the category of change (required)
  • scope: Provides additional context about what part of the codebase is affected (optional)
  • description: A brief summary of the change (required)
  • body: A more detailed explanation of the change (optional)
  • footer: Additional metadata like breaking changes or issue references (optional)

Note

The description should follow these recommended conventions:

  • Use imperative mood: "add feature" not "added feature" or "adds feature"
  • Think of it as completing the sentence: "This commit will..."
  • Start with lowercase (not "Add feature" but "add feature")
  • No period at the end

Commit Types

The specification defines two mandatory types that correlate directly with Semantic Versioning (SemVer):

  • feat: Introduces a new feature (correlates with MINOR in SemVer)
  • fix: Patches a bug (correlates with PATCH in SemVer)

Beyond these, the Angular convention recommends additional types that many projects adopt:

TypeDescription
buildChanges to build system or external dependencies
choreMaintenance tasks that don't modify src or test files
ciChanges to CI configuration files and scripts
docsDocumentation-only changes
styleCode style changes (formatting, whitespace, etc.)
refactorCode changes that neither fix bugs nor add features
perfPerformance improvements (a special type of refactor)
testAdding or correcting tests
opsInfrastructure, deployment, backup, recovery operations
revertReverts a previous commit

Tip

You don't have to use all these types. Start with feat, fix, and docs, then add more as your team needs them. The key is consistency, not completeness.

Practical Examples

Let's look at real-world examples of conventional commit messages, from simple to complex. I find that seeing actual examples is the fastest way to internalize the format.

Simple Commits

A basic bug fix:

fix-commit.txt
fix: prevent racing condition in user authentication

A new feature:

feat-commit.txt
feat: add dark mode toggle to settings page

A documentation update:

docs-commit.txt
docs: correct spelling in README

Commits with Scope

Scopes provide context about which part of the codebase is affected. I find scopes particularly useful in monorepos or projects with distinct modules:

scoped-commits.txt
feat(api): add endpoint for user preferences

fix(auth): handle expired tokens gracefully

docs(readme): update installation instructions

Caution

Don't use issue identifiers as scopes. Use fix(auth): resolve token expiration with Fixes #123 in the footer, not fix(#123): resolve token expiration.

Breaking Changes

Breaking changes are significant because they indicate that consumers of your code need to make changes. They correlate with MAJOR version bumps in SemVer.

You can indicate breaking changes in two ways:

Using the ! notation:

breaking-change-exclamation.txt
feat!: remove deprecated authentication endpoints
breaking-change-scoped.txt
feat(api)!: change response format for user endpoints

Using a BREAKING CHANGE footer:

breaking-change-footer.txt
feat: allow provided config object to extend other configs

BREAKING CHANGE: `extends` key in config file is now used for
extending other config files instead of extending presets.

You can also combine both approaches for maximum clarity:

breaking-change-combined.txt
refactor(runtime)!: drop support for Node 14

BREAKING CHANGE: Node 14 has reached end-of-life status.
Minimum supported version is now Node 18.

Caution

Breaking changes should be rare and well-documented. When introducing one, make sure the commit body explains why the change was necessary and how users should migrate.

Multi-line Commits with Body and Footer

For complex changes, use the body to explain the "what" and "why". I've found that taking the time to write a good commit body pays dividends when debugging issues months later.

The body should:

  • Explain the motivation for the change
  • Contrast the new behavior with the previous behavior
  • Use imperative mood, just like the description

The footer is used for:

  • Issue references: Fixes #123, Closes #456, Refs JIRA-789
  • Breaking change descriptions (when more detail is needed)
  • Co-authors and reviewers
multiline-commit.txt
fix(parser): handle edge case in markdown table parsing

The previous implementation failed when table cells contained
pipe characters. This fix properly escapes pipes within cell
content while maintaining backwards compatibility.

Fixes #1234
Reviewed-by: Jane Doe

Special Commits

Some commits follow Git's default patterns rather than the conventional format:

Initial commit:

initial-commit.txt
chore: init

Merge commits follow Git's default format:

merge-commit.txt
Merge branch 'feature/user-auth' into main

Revert commits follow Git's default format:

revert-commit.txt
Revert "feat(auth): add OAuth support"

This reverts commit abc1234.

More Examples

Here are additional examples covering different commit types:

perf-example.txt
perf: reduce memory footprint by using streaming parser
refactor-example.txt
refactor: extract validation logic into separate module
style-example.txt
style: apply consistent formatting to configuration files
test-example.txt
test(api): add integration tests for user endpoints
ops-example.txt
ops: configure automatic database backups

Why Use Conventional Commits?

Adopting Conventional Commits brings several tangible benefits to your development workflow. Here's why I've made it a standard practice in all my projects.

Automated Changelog Generation

Tools like conventional-changelog and semantic-release can parse your commit history and automatically generate a changelog. This eliminates the manual effort of maintaining a CHANGELOG.md file and ensures accuracy.

I don't use automated tooling for every project, but even without it, the format pays off. With a quick glance at the commit history, I can easily determine the next version: fix means patch, feat means minor, and a breaking change (with ! or BREAKING CHANGE) means a new major version. No guesswork, no debates, the commits tell you exactly what kind of release you're preparing.

Automatic Version Bumping

By parsing commit messages, tools can determine the appropriate version bump:

  • fix: commits trigger a PATCH version bump (1.0.0 -> 1.0.1)
  • feat: commits trigger a MINOR version bump (1.0.0 -> 1.1.0)
  • BREAKING CHANGE or ! triggers a MAJOR version bump (1.0.0 -> 2.0.0)

This removes the human error from versioning decisions and ensures your releases follow SemVer correctly.

Tip

Even if you don't automate version bumping, the mental exercise of choosing the right commit type helps you think about the impact of your changes.

Better Team Communication

Standardized commit messages make it easier for team members to understand what changed and why. When reviewing pull requests or investigating issues, meaningful commit messages serve as documentation that explains the evolution of the codebase.

I've seen teams waste hours trying to understand why a particular change was made, only to find a commit message that says "fix stuff". Conventional Commits prevents this.

Improved Git History Navigation

A well-structured commit history becomes a valuable resource. You can easily filter commits by type, find all breaking changes, or trace when a feature was introduced:

git-log-examples.sh
# Find all breaking changes
git log --oneline --grep="BREAKING CHANGE"

# Find all new features
git log --oneline --grep="^feat"

# Find all bug fixes in the auth module
git log --oneline --grep="^fix(auth)"

CI/CD Integration

Conventional Commits integrates seamlessly with CI pipelines. You can configure your pipeline to:

  • Validate commit messages before merging
  • Automatically publish releases based on commit types
  • Generate release notes for GitHub/GitLab releases
  • Trigger different workflows based on commit types

Tooling and Automation

Several tools help enforce and leverage Conventional Commits in your projects. I'll walk you through the ones I've found most useful.

Commitlint

commitlint validates that your commit messages follow the conventional format. It's typically used with Git hooks to prevent non-conforming commits.

Install and configure commitlint:

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

Create a commitlint.config.js:

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

Husky

Husky makes it easy to set up Git hooks. Combined with commitlint, it validates commits before they're created:

setup-husky.sh
npm install --save-dev husky
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

Note

Husky 9+ uses a simpler setup than previous versions. If you're upgrading from an older version, check the migration guide.

Semantic Release

semantic-release automates the entire release workflow:

  • Determines the next version number
  • Generates release notes
  • Publishes the package
  • Creates Git tags and GitHub releases

This tool is particularly powerful for library authors who want fully automated releases. It reads your conventional commits and, by default, applies Angular-style semantics to decide whether a change is patch, minor, or major. I use it in several of my open source projects, and it has eliminated the manual steps in my release process entirely.

How I Use Conventional Commits

Over the years, I've refined my approach to using Conventional Commits in my projects. Here's what works for me.

My Daily Workflow

I don't use interactive tools for every commit. Instead, I've internalized the format and write commits directly. For complex commits, I use my editor to compose multi-line messages with proper formatting.

My typical workflow:

  1. Make focused, atomic changes
  2. Stage the changes with git add -p for precise control
  3. Write a conventional commit message
  4. Push when the feature or fix is complete

I tend to focus on implementing features or fixes with narrow scopes and baby steps. By writing conventional commits, I naturally stay focused on small, incremental changes. This makes it really easy for people to review my work since each commit has a clear, single purpose.

Tip

Using git add -p (patch mode) helps you create atomic commits by staging only the hunks related to a single change. This naturally leads to better commit messages because each commit has a clear purpose.

Scopes I Commonly Use

I define scopes based on the project structure. For a typical web application, my scopes might include:

  • api - Backend API changes
  • ui - Frontend component changes
  • auth - Authentication-related changes
  • deps - Dependency updates
  • config - Configuration changes

I also use the commit type as a scope when it makes sense. For example, I scope build file modifications (like pom.xml or package.json changes) with build, and GitHub workflow changes with ci:

type-as-scope.txt
chore(build): update maven-compiler-plugin to 3.12.1

chore(ci): add code coverage step to PR workflow

For Kubernetes projects like Eclipse JKube, I use scopes like:

  • kubernetes - Kubernetes-specific functionality
  • openshift - OpenShift-specific functionality
  • maven - Maven plugin changes
  • gradle - Gradle plugin changes

Enforcing in Open Source Projects

In open source projects, you can use GitHub Actions to validate PR titles follow the conventional format. This is often more practical than validating every commit since contributors may squash and rebase their work.

Here's a simple approach using a basic regex that works for any project without external dependencies:

.github/workflows/pr-title.yml
name: PR Title Check
on:
  pull_request:
    types: [opened, edited, synchronize]

jobs:
  check-title:
    runs-on: ubuntu-latest
    steps:
      - name: Validate PR title follows Conventional Commits
        run: |
          TITLE="${{ github.event.pull_request.title }}"
          PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?: .+$'
          if ! echo "$TITLE" | grep -qE "$PATTERN"; then
            echo "::error::PR title does not follow Conventional Commits format"
            echo "Expected: <type>[optional scope]: <description>"
            echo "Got: $TITLE"
            exit 1
          fi
          echo "PR title is valid: $TITLE"

This workflow works for any project without requiring Node.js or external actions. If you're using commitlint locally, you can add a commitlint.config.js to your project and use npx -y commitlint instead for more comprehensive validation.

Handling Squash Merges

When using squash merges, the PR title becomes the commit message. This is why validating PR titles is crucial: it ensures your main branch maintains a clean, conventional history even when contributors don't follow the convention in their individual commits.

Common Mistakes and How to Avoid Them

Here are pitfalls I've encountered (and sometimes made myself) and how to avoid them.

Using the Wrong Type

Mistake: Using fix for anything that changes code.

Solution: Reserve fix for actual bug fixes. Use refactor for code restructuring, perf for optimizations, and chore for maintenance tasks.

Overly Generic Descriptions

Mistake: Writing descriptions like "update code" or "fix issue".

Solution: Be specific about what changed. "Fix null pointer in user validation" is far more useful than "fix bug".

Inconsistent Scopes

Mistake: Using different scopes for the same module (e.g., auth, authentication, login).

Solution: Document your scopes in your contributing guide and stick to them. Use a commitlint plugin to enforce allowed scopes.

Mixing Concerns in One Commit

Mistake: Combining a feature with a refactor in the same commit.

Solution: Keep commits atomic. If you need to refactor to implement a feature, make separate commits:

atomic-commits.txt
refactor(auth): extract token validation to separate function

feat(auth): add support for refresh tokens

Tip

If you find yourself writing "and" in your commit description, you're probably trying to do too much in one commit.

Getting Started

Ready to adopt Conventional Commits? Here's a pragmatic approach that I recommend to teams new to the convention.

Start Simple

Don't try to set up all the tooling at once. Begin by just following the format manually. Once you're comfortable with the convention, gradually add tooling.

Document Your Conventions

Create a section in your CONTRIBUTING.md that explains:

  • Which commit types your project uses
  • What scopes are available
  • Examples of good commit messages

Set Up Basic Validation

Once your team is comfortable with the format, add commitlint with Husky to catch mistakes early. This prevents non-conforming commits from entering your history.

Automate Incrementally

After validation is working, consider adding:

  1. Automated changelog generation
  2. Automatic version bumping
  3. Automated releases

Each step builds on the previous one, so take your time.

Conclusion

Conventional Commits transforms how teams communicate through their Git history. By following a simple, standardized format, you gain clearer communication, automated versioning, and better tooling integration.

The specification is intentionally lightweight: you can start using it today without any tooling. As your team grows comfortable with the convention, layer on automation to unlock its full potential.

Whether you're working on a personal project or maintaining enterprise software, meaningful commit messages make your codebase more maintainable and your releases more predictable. Give Conventional Commits a try. Your future self (and your teammates) will thank you.

References

  • Conventional Commits Specification v1.0.0
  • Semantic Versioning (SemVer)
  • Angular Commit Message Guidelines
  • commitlint - Lint commit messages
  • semantic-release - Automated version management

You Might Also Like

  • Test-Driven Development (TDD): An Introduction
  • Introduction to the Model Context Protocol (MCP): The Future of AI Integration
Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Post navigation
Triggering GitHub Actions across different repositoriesQuarkus + Fabric8 Maven Plugin + GraalVM integration
© 2007 - 2025 Marc Nuri