Commit conventions
Shared rules for what goes into one commit and how its message is written, so the history explains itself and tools can read it.
How it works
A commit message has a subject line, a blank line, and an optional body. The subject is a short summary, about 50 characters as a soft limit, written in the imperative mood (“Fix bug”, not “Fixed bug”), the same form as the messages Git itself writes for merges and reverts. The body, wrapped at about 72 characters so it reads well in git log and in emailed patches, explains why the change was needed and why this approach was chosen; the diff already shows what changed. Each commit should hold one logical change, so it can be reviewed, reverted or bisected on its own. Trailers at the end add structured lines such as Fixes: #482 or Signed-off-by:, the sign-off by which a contributor certifies the Developer Certificate of Origin. Conventional Commits 1.0.0 adds a machine-readable prefix: fix: for a bug fix, feat: for a new feature, and a ! or a BREAKING CHANGE: footer for an incompatible change, which map to patch, minor and major in semantic versioning. Tools build on that: commitlint rejects messages that break the format, and semantic-release reads them to choose the next version number, write release notes and publish. Changesets takes the opposite route: rather than parsing messages, each pull request adds a small changeset that states the version bump and the changelog line. Teams that squash-merge pull requests apply the rules to the pull request title, which usually becomes the commit subject.
Labels on moving boxes. “Kitchen: plates, fragile” tells whoever unpacks what’s inside and how to treat it without opening it; “stuff” forces them to open every box.
Reach for it when
- Any shared repository: someone will read the history months later to find out why a line exists
- You want changelogs and version numbers generated from the history (Conventional Commits with semantic-release)
- A project that needs contributors to certify their right to submit code (DCO sign-off)
Think twice when
- Enforcing a format so strictly that people stop committing small steps; tidy up with an interactive rebase or a squash merge instead
- Treating a passing linter as a good message: it checks the format, not whether the body explains why
A real example
A library team adopts Conventional Commits and squash merges, with commitlint checking each pull request title in CI. When three pull requests titled fix(parser): …, feat(cli): add --json output and docs: … merge in one week, semantic-release bumps the version from 2.3.1 to 2.4.0, because a feat means a minor release, writes release notes listing the feature and the fix, and publishes the package. Nobody edits a version number by hand.
Conventional Commits and version numbers#
Only fix, feat and breaking changes carry meaning in the specification; other types are a team convention.
| Commit | Means | Semantic version bump |
|---|---|---|
| `fix: …` | Patches a bug | Patch (2.3.1 to 2.3.2) |
| `feat: …` | Adds a feature | Minor (2.3.1 to 2.4.0) |
| `feat!: …` or a `BREAKING CHANGE:` footer | Breaks the API, whatever the type | Major (2.3.1 to 3.0.0) |
| `build`, `chore`, `ci`, `docs`, `style`, `refactor`, `perf`, `test` | Types recommended by commitlint’s conventional config, from Angular | None implied by the specification |
- A scope in parentheses names the area changed, e.g.
feat(parser): add ability to parse arrays.
In code · text
Fix crash when checking out an empty cart
Checkout read items[0].price without checking the list, so an empty
cart threw a TypeError and the API returned 500. Return a 422 with
"cart is empty" instead, and add a test for it.
Fixes: #482
Signed-off-by: Ana Ruiz <ana@example.com>
----- the same habits in Conventional Commits form -----
fix(cart): reject checkout of an empty cart
feat(search): add filter by price range
docs: explain how to run the tests locally
feat(api)!: remove the v1 orders endpoint
BREAKING CHANGE: /v1/orders is gone; use /v2/orders instead.Often confused with
Where it came from2004–2019
Git’s message habits come from email. Linux and Git itself were developed by mailing patches, and git format-patch turns each commit into an email whose subject is the commit’s first line. In April 2008 Tim Pope, explaining the style to Ruby on Rails contributors, wrote down the model that stuck: a capitalized summary of 50 characters or less, a blank line, a body wrapped at about 72 characters, and the imperative mood to match the messages Git generates. Git’s own SubmittingPatches guide asks for the same imperative mood, a soft 50-character subject and a body that explains the why, and one commit per logically separate change. Chris Beams’s 2014 essay boiled it down to seven rules.
The Signed-off-by: line has a legal origin. In May 2004, with SCO claiming ownership of parts of Linux, Linus Torvalds proposed that every patch carry a sign-off certifying a short “Developer’s Certificate of Origin”: that the contributor wrote the code or otherwise has the right to submit it under the project’s open-source licence. Git’s git commit -s writes the line, and Git itself uses the same certificate as the kernel.
Machine-readable messages came from the JavaScript world. The AngularJS team required a <type>(<scope>): <subject> format so changelogs could be generated from the history, and semantic-release adopted that convention as its default to automate releases. Conventional Commits generalized the idea into a specification “inspired by, and based heavily on” Angular’s guidelines, and published version 1.0.0 on September 30, 2019.
Why it’s built this way
- Imperative mood in the subject
- The messages Git writes itself, for
git mergeandgit revert, use that form, and Git’s guide asks you to write as if giving orders to the codebase: “make xyzzy do frotz”. Matching it keeps a log consistent whoever wrote each line. - A short subject and a wrapped body
- The first line appears alone in
git log --oneline, in interactive rebases, in shortlogs and as the subject of patch emails, so it must stand on its own. Wrapping at about 72 columns leaves room for indentation and reply markers on an 80-column terminal. - Types tied to semantic versioning
- Conventional Commits mapped
fixto a patch,featto a minor release and breaking changes to a major one, so a tool can compute the next version without a human reading every commit. - A sign-off instead of a contributor agreement
- Torvalds wanted a per-patch record of where code came from, to avoid ownership disputes “ten years from now”, while keeping the rules simple with no “extra paperwork”.
Timeline
- 2004
Linus Torvalds proposes
Signed-off-by:and the Developer’s Certificate of Origin (May 23) - 2005
Linus Torvalds makes Git’s first commit (April 7)
- 2008
Tim Pope’s “A Note About Git Commit Messages” (April 19)
- 2014
Chris Beams’s “How to Write a Git Commit Message” lists seven rules
- 2019
Conventional Commits 1.0.0 is released (September 30)
- Tim Pope: A Note About Git Commit Messages (Apr 19, 2008)
- Git: Documentation/SubmittingPatches
- Chris Beams: How to Write a Git Commit Message (Aug 31, 2014)
- Linus Torvalds on LKML: [RFD] Explicitly documenting patch submission (May 23, 2004)
- git/git on GitHub: Initial revision of “git” (Apr 7, 2005)
- Angular: Commit message guidelines (links the original AngularJS format)
- semantic-release README: commit message format (Angular convention by default)
- Conventional Commits: About (based heavily on the Angular guidelines)
- conventionalcommits.org on GitHub: release 1.0.0 of specification, PR #175 (Sep 30, 2019)
Sources12
- Tim Pope: A Note About Git Commit Messages (50-character summary, 72-column body, imperative)
- Git: Documentation/SubmittingPatches (imperative mood, 50-character soft limit, separate commits for separate changes, sign-off)
- Linux kernel docs: Submitting patches (separate each logical change, Developer’s Certificate of Origin)
- Developer Certificate of Origin, version 1.1
- Git docs: git commit --signoff (DCO used by the Linux kernel and Git)
- Conventional Commits 1.0.0 specification
- Angular: Commit message guidelines (types, scopes, BREAKING CHANGE)
- semantic-release README: next version, release notes and publishing from commit messages
- commitlint README: lint commit messages
- Changesets README: contributors declare how changes should be released
- Changesets docs: Introduction to using changesets (a semver bump plus changelog information)
- GitHub Docs: Configuring commit squashing for pull requests (default squash message uses the pull request title)