Conventional Commits: A Complete Guide to Better Git Commit Messages
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:
<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:
| Type | Description |
|---|---|
build | Changes to build system or external dependencies |
chore | Maintenance tasks that don't modify src or test files |
ci | Changes to CI configuration files and scripts |
docs | Documentation-only changes |
style | Code style changes (formatting, whitespace, etc.) |
refactor | Code changes that neither fix bugs nor add features |
perf | Performance improvements (a special type of refactor) |
test | Adding or correcting tests |
ops | Infrastructure, deployment, backup, recovery operations |
revert | Reverts 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: prevent racing condition in user authenticationA new feature:
feat: add dark mode toggle to settings pageA documentation update:
docs: correct spelling in READMECommits 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:
feat(api): add endpoint for user preferences
fix(auth): handle expired tokens gracefully
docs(readme): update installation instructionsCaution
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:
feat!: remove deprecated authentication endpointsfeat(api)!: change response format for user endpointsUsing a BREAKING CHANGE footer:
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:
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
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 DoeSpecial Commits
Some commits follow Git's default patterns rather than the conventional format:
Initial commit:
chore: initMerge commits follow Git's default format:
Merge branch 'feature/user-auth' into mainRevert commits follow Git's default format:
Revert "feat(auth): add OAuth support"
This reverts commit abc1234.More Examples
Here are additional examples covering different commit types:
perf: reduce memory footprint by using streaming parserrefactor: extract validation logic into separate modulestyle: apply consistent formatting to configuration filestest(api): add integration tests for user endpointsops: configure automatic database backupsWhy 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 CHANGEor!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:
# 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:
npm install --save-dev @commitlint/cli @commitlint/config-conventionalCreate a 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:
npm install --save-dev husky
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msgNote
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:
- Make focused, atomic changes
- Stage the changes with
git add -pfor precise control - Write a conventional commit message
- 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 changesui- Frontend component changesauth- Authentication-related changesdeps- Dependency updatesconfig- 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:
chore(build): update maven-compiler-plugin to 3.12.1
chore(ci): add code coverage step to PR workflowFor Kubernetes projects like Eclipse JKube, I use scopes like:
kubernetes- Kubernetes-specific functionalityopenshift- OpenShift-specific functionalitymaven- Maven plugin changesgradle- 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:
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:
refactor(auth): extract token validation to separate function
feat(auth): add support for refresh tokensTip
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:
- Automated changelog generation
- Automatic version bumping
- 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
