
Photo by Yancy Min
What are Conventional Commits
Conventional Commits is a specification for formatting git commit messages with the aim to make them easily understandable to both humans and machines. Below is the commit message structure:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
In our git subject, we must include a supported type:
- build: Changes that affect the build system or external dependencies
- ci: Changes to our CI configuration files and scripts
- docs: Documentation only changes
- feat: A new feature
- fix: A bug fix
- perf: A code change that improves performance
- refactor: A code change that neither fixes a bug nor adds a feature
- style: Changes that do not affect the meaning of the code
- test: Adding missing tests or correcting existing tests
For example fix(html): center div or feat(auth): support github OAuth2.
The idea is that we can also programmatically parse our commit messages to
produce a Semantic Version, which takes the format MAJOR.MINOR.PATCH.
We increment the MAJOR when we encounter a breaking change (more on that later),
the MINOR when we see the “feat” type, and the PATCH when we see the “fix” type.
This method to versioning can make consuming your software more manageable. From the consumer’s point of view, breaking changes, or changes to the MAJOR version, should be handled manually. As such, your consumer will likely only automatically apply updates from the same MAJOR version, granting them stability. At the same time, they can permit automatic updates to the MINOR version and PATCH version to get new improvements and bug fixes. Your consumer may also specify minimum PATCH versions to guarantee the features they require.
Additionally, you could use this structured way of writing commit messages to generate changelogs.
Caveats
Generally, I think this is a great approach to maintaining a structured git log; however, there are certainly some drawbacks. A common complaint is that it can already be challenging to write a meaningful and succinct commit subject using just 50 characters; now we must include a type, and potentially a scope too. Inevitably, you will come to violate this semi-arbitrary git commit subject character limit, or perhaps worse, you may find yourself compromising on the contents of your subject, writing worse commit subjects. Honestly, I have caught myself doing both, even in the same commit subject.
Though not required, it’s a good idea to begin the commit message with a single short (less than 50 character) line summarizing the change...
man git-commit
feat & fix only?
Another caveat is that only types “feat” and “fix” bump the MINOR and PATCH versions. This makes sense at first, but I find it a bit restrictive. Sure, when we add more testing, we don’t need to release the project again, needlessly use CI resources, or bother our consumers to update. But ask yourself, is there any project or scenario, when using the other types, that you would want to release your project? If yes, then it would be appropriate to bump the version again.
Take for instance the “build”, “ci”, or “perf” types. Perhaps something about your build or CI process is broken, but it does not relate to your code. You want to use an accurate type, but it won’t produce a new version number, and therefore, a release would never make it out. Surely if you add significant performance improvements to your project, you would want to issue a release?
The point is, you may decide your project needs a release, and therefore may be forced to compromise on what you feel is the best commit type. Updating dependencies without a known vulnerability may not be a feature or a fix, but updating dependencies with a known vulnerability would be a fix, even though they may be the exact same process or CI job.
Our goal of meaningful commits for humans is dropped because we need the robot to properly parse and release our application.
BREAKING CHANGE
Another caveat is how breaking changes are implemented.
To bump the major version via the git commit subject, we can add an !, e.g.
fix(api)!: foo no longer bars.
That is quite a nonchalant way to communicate a breaking change.
We owe it to our consumers and our future selves to write clear commit messages,
and I believe this is a bit esoteric.
What are git footer’s?
There is another method to introduce a breaking change, using a “git footer”, but what’s that? Well, it appears to be a convention invented by Conventional Commits, or perhaps a cargo cult that the authors borrowed and loosely defined.
Each footer MUST consist of a word token, followed by either a :
Conventional Commits v1.0.0or # separator, followed by a string value (this is inspired by the git trailer convention).
What is a “word token”? Semantic Versioning actually includes a regex directly in their specification. Sure, there might be an oversight in the regex, but the pattern we consider valid is set in stone. It would be appropriate for the Conventional Commits specification to define a regex or character class too.
Let’s ask Perl-Compatible Regular Expressions, one of the most popular flavors, what is a “word”?
A “word” character is an underscore or any character that is a letter or digit.
man 3 pcrepattern
What about POSIX Extended Regular Expressions?
Word-constituent characters are letters, digits, and the underscore. ... The symbol \w is a synonym for [_[:alnum:]]
man 1 grep
What do we gain by creating the new concept of git footer’s?
It was inspired by a seemingly suitable alternative,
git trailers.
So the Conventional Commits summary instructs us to use the text “BREAKING CHANGE:”
in our “git footer”, e.g. BREAKING CHANGE: foo no longer bars.
Is “BREAKING CHANGE” what they consider a word token?
Does git consider this a trailer?
So we have two different methods to indicate a breaking change and bump our
MAJOR version, but wait, there’s more buried in the final spec bullet:
BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer
Conventional Commits v1.0.0
What a strange phrase “word token” is to use in a specification, when both proposed keys “BREAKING CHANGE” and “BREAKING-CHANGE” violate definitions of a “word” that have been established in popular specifications between 28 and 39 years ago.
git interpret-trailers
git provides a builtin command git interpret-trailers to properly parse git
trailers, which is actually used by git log to format text using the
placeholder %(trailers), for example git log --pretty='format:%s %(trailers).
The manual for interpret-trailers is fairly thorough in its definition of git
trailers and even references a another specification for inspiration:
Add or parse trailer lines that look similar to RFC 822 e-mail headers[...]. When reading trailers, there can be no whitespace before or inside the
man 1 git-interpret-trailers.... Note that trailers do not follow (nor are they intended to follow) many of the rules for RFC 822 headers.
RFC 822: Standard for ARPA Internet Text Messages
So Conventional Commits uses “git footers”, which are inspired by “git trailers”, which are inspired by RFC 822, neither of which support spaces in the “token”!
This means when we use git log --pretty='format:%s %(trailers), we will never
see a “git footer” with a space in the key, i.e. “BREAKING CHANGE”.
Ultimately, three methods to bump the MAJOR version, one quite esoteric and
nonchalant, another which is completely broken, and finally one that works.
This issue has been raised in #413.
How did we get here?
Well, there was a time when git was quite lazy in parsing trailer tokens. Commit e431956 made the parsing much stricter:
Currently, a line is interpreted to be a trailer line if it contains a separator. Make parsing stricter by requiring the text on the left of the separator, if not the empty string, to be of the "<token><optional whitespace>" form.
November 29, 2016
But the documentation was not updated until late 2022 in commit b46dd17, reflected in version 2.38 of the documentation. Also, the git documentation at the time still aimed the trailer syntax at RFC 822’s e-mail headers, which did not support spaces. Checking the git history of Conventional Commit spec, it looks like it went live around August 2016, so it’s understandable where the confusion came from.
Conclusion
Who might have thought that the intersection of four different specifications could lead to such confusion? I cannot place blame on any organization, software evolves over time and we have to work together to maintain it.

XKCD issue #927 Standards
I do find it amusing that the Conventional Commit specification centers around maintaining a structured git log and an automatic versioning system, yet is seemingly stuck at v1.0.0. It’s strange, because they have quite the git log and use Conventional Commits themselves.
It might be the right choice, after all, what would happen if they fix #413? It certainly would be a breaking change, as it may stop recognizing the token “BREAKING CHANGE”, which could cause consumer releases to not accurately reflect the historical MAJOR version, potentially releasing newer MAJOR version with breaking changes, under older MAJOR versions.
So did we gain much from universally conforming to this specification and the tools it provides? I would argue you should own the means of your versioning system and Conventional Commits is a lightweight specification that enables us to do exactly that. You the author know the best commit message, type, and when to release your project; take the concepts from Conventional Commits specification and apply them how they best fit your project’s needs.
See how I’ve written my own automated version management system using Conventional Commits and Semantic Versioning for a Go project, make version.