What is a conventional commit?
In case you’re not aware, conventional commits is a standard format for writing commit summaries. It looks like this.
|
|
This example is from dependabot, which does
conform to conventional commits. Notice that only the first line, the commit
summary, is part of the conventional commits spec here. The last line is a
“footer”[1], or technically, a
trailer added by the git commit -s
flag, -s
short for --signoff
.
The exact spec looks like this:
|
|
Unlike many project specific standards, such as that of angular or nixpkgs, what sets conventional commits apart is that it aims to be a universal commit standard, a bit like how semantic versioning seeks to be a universal software versioning standard.
In the same sense as semantic versioning provides meaning through version
numbers, such as a major
number increasing meaning a breaking change,
conventional commits also seeks to encode information about the state of a
project in the commit summaries, and provides enough information to reasonably
tell when it’s time to do a patch, minor, or major version bump.
It further also provides ways to specify if a commit contains a breaking change, via a git footer.
It may be easiest to explain this by just showing you the spec.
The commit contains the following structural elements, to communicate intent to the consumers of your library:
- fix: a commit of the type
fix
patches a bug in your codebase (this correlates withPATCH
in Semantic Versioning).- feat: a commit of the type
feat
introduces a new feature to the codebase (this correlates withMINOR
in Semantic Versioning).- BREAKING CHANGE: a commit that has a footer
BREAKING CHANGE:
, or appends a!
after the type/scope, introduces a breaking API change (correlating withMAJOR
in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.- types other than
fix:
andfeat:
are allowed, for example @commitlint/config-conventional (based on the Angular convention) recommendsbuild:
,chore:
,ci:
,docs:
,style:
,refactor:
,perf:
,test:
, and others.- footers other than
BREAKING CHANGE: <description>
may be provided and follow a convention similar to git trailer format.Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE).
A scope may be provided to a commit’s type, to provide additional contextual information and is contained within parenthesis, e.g.,feat(parser): add ability to parse arrays
.
So basically, if there is a type fix
, that’s a PATCH
bump to your semver, a
feat
is a MINOR
bump, and a BREAKING CHANGE:
footer or a !
after the
type/scope is a MAJOR
bump.
If we for instance have commit with the summary fix: change windows-only imports to be windows-only
, that’s a PATCH
change, and if we are on version
3.2.48
, then next version will have to be at least 3.2.49
.
If we instead had a summary feat: add --no-|show-symlinks flags for filtering output
, then that would a MINOR
change, and again, if we were on 3.2.48
,
next release would be 3.3.0
.
Lastly, if we had a summary feat(flags)!: add --classify=always,auto,never
,
even thou it has a feat
type, the !
indicates this is a breaking change, and
so we must cut a new MAJOR
version on next release if we include this commit,
meaning that if we were on 3.2.48
, we’d go to 4.0.0
.
Yes. Breaking changes means a new major version. Yes, that might seem sever to you if you’re not used to being at the helm of something being used by a lot of people, but that’s how we make these sausages (at least if we’re actually trying to do it correctly). The guy who authored the semver spec, Tom Preston-Werner (also known for co-founding GitHub and creating gravatar) has a blog post about this called Major Version Numbers are Not Sacred.
What people end up doing in practice is building up a lot of breaking changes and then releasing them all at once, to not constantly destroy the API/ABI and UX of their software. How this ties in with a more continuous development is a very interesting question, e.g. how does eza manage to release a new version every week, yet also work on breaking changes like config files or moving to a new command line parser (specifically clap). I think such a discussion is far out of scope for this post, but I may write about it in the future.
Conventional Commits and Linguistic Relativity
If you were to ask wikipedia what “Behavioral Design” means, it would reply:
Behavioural design is a sub-category of design, which is concerned with how design can shape, or be used to influence human behaviour.
Now, how is that relevant to us as developers, maintainers, or just hobbyist hackers?
Well you see, the design of conventional commits, whether intentional or not, has a lot of decisions that end up shaping how contributors interact with your project.
When I initially forked eza
, I mostly moved us to conventional commits fairly
early because I was concerned with a maintenance burden. I knew that exa
had
been a project of maintainer burnout, and what I was trying to do was create
longevity for the project. That meant that if there was something I could
offload to someone or something else, I would, including changelogs.
And yes, conventional commits means I don’t have to write changelogs, I can
automate it, and it means I don’t have to think too hard about what version is
next, it’s automatically generated. In fact, I can release eza every week
because a release is just running two scripts, doing a cargo publish
, and a
nix-update eza
in the nixpkgs repo and
opening a PR.
While that release velocity is great, I found another, much more interesting perk of using conventional commits. Namely that conventional commits lead to better commits, better commit summaries, better PRs, and better structured commits.
For instance, if a project just allows a commit summary to be an arbitrary
string, what you’ll often end up with is either something like made extensions work
, a single commit that encompasses vast refactorings of the codebase,
introduces a feature, fixes 3 different bugs, and is one big monolithic and
unrevievable mess.
On the other hand, it also saves you from the series of wip
, wip 2
,
formatted code and added quantum cryptography
, made compile
. These are
equally unreviewable, and completely destroy your git log.
Now, you may have the discipline of projects like nixpkgs of asking contributors to rebase their commits, so that they each represent unique atomic changes. But even then, what conventional commits gives you is a language to express what these changes actually are.
Before I had conventional commits as something I could just write without looking at the spec, I would genuinly struggle putting words to what my changes did. It made it hard to create good PRs to projects.
Now, it’s much easier. I just go:
refactor(parser): used enums instead of bools in match logic
fix(parser): capital letters not differentiated
fix(parser): long lines ignored
feat(parser): add long flags
docs(parser): document long flag usage
style: change flag rendering
ci: check parser flags workflow
Not only does this make my PR much more structured, but by getting into that habit, I find that my work also becomes much more organized. Instead of just working on some hodgepodge mix of things at the same time and committing either to save my work or to put all of it together in a disorganized sack of a single commit, I find that I am much more able to dissect a problem into discrete solvable steps.
I’d even go as far as saying this makes you a better programmer, as breaking down a large problem into small solvable chunks is a great problem solving strategy.
What conventional commits does is it gives you this language to describe the steps it takes to solve a problem, and getting familiar with that vocabulary not only makes you a better programmer, but it makes it much easier to work with you, and it heavily reduced the burden on maintainers.
Linguistic relativity is the idea that the structure of a language influences the speakers world view and cognition, changing how you interact with your surroundings. Linguistic relativity is a hypothesis, but I find it pretty easy to prove the weak case that if you can’t signify something, you will struggle to operate with it.
In other words, conventional commits get you merged faster, and makes your code better. Hopefully you’re sold by this point.
Footnotes
[1]: There are some tools for dealing with these, such as git-footers. Personally, I don’t really use those. But it’s good to know they exist.