A's Commit Messages Guide: Location, Action, Rationale
By Artyom BologovI'm writing and searching lots of commit messages every day. It's easy to get lost in them. Yet I don't. The reason is that I write reasonable commit messages. (I'm saying it in such a blunt way because it's not necessarily my merit—I had great teachers on Nyxt team and Guix community!) And I figured I should explain the reasoning behind my style. Might be helpful to someone starting out or looking for better conventions.
The heuristic is simple:
- Location
- Localize the change to file/function/object etc.
- Action
- Explain what the change does.
- Rationale
- Clarify the reasons and context for it.
Yes, all in one message without description (if possible.) I'll go through these parts in the order. Hopefully explaining things clearly enough for you to embrace the system.
Location: Isolating the Change
Filesystem is a huge helper and marker for project structure. Java classpaths, C include paths, conventional Open Source project files (README, LICENSE/COPYING, CONTRIBUTING.) Locating the change via file structure is half of the problem for change explanation. That's why my commit structure is so elaborate on locations.
First, file paths. Include full paths to files so that it's clear where the change happened. If your project structure is (already) reflecting the code structure then listing the file/directory should be enough to show change scope. Some exceptions I use:
- Omit file extension if it's obvious.
- Omit full directory path if it's The Directory,
like
src/
for code-oriented project,www/
orpublic/
for pages, anddata/
for datasets. - Use shell wildcards (
something.*.something
) or brace expansion (file.{c,h}
) to mark groups of files. - Or outright omit the location when the commit involves multiple files without any main one.
Second, exact change location. This one is elaborate! There are several syntactic markers for the location that I use: Parentheses, square brackets, angle brackets, and some regex syntax. Mostly in the order of increasing specificity:
- Parentheses are for toplevel entity highlighting.
In case the language is function/procedure-oriented (like Scheme or C), function name is such an entity.
In case the language is object-oriented (like Commmon Lisp or C++), it's the class that matters.
In other cases... whatever the general structure unit for the file is.
While writing for this blog, I often use section IDs as this toplevel entity markers.
As in
commitmsg(#location): ...
for commit relating to the section you're reading right now. - Square brackets are for sub-units of the parenthesized thing. For OOP codebase, these usually are class methods or slots/properties/fields. For functional ones, it might be local functions, arguments, or blocks. It's rare enough seeing these, you should focus on parenthesized location first.
- Angle brackets are for sub-sub-entities. Like individual arguments or variables in the body of the function/method. Barely ever useful, I'm only including these for completeness.
- Here's an atypical bit: I use regex-y syntax
(as suggested in my Regex Pronouns post)
to shorten/condense the location.
Writing
describe(describe-*): ...
(from Nyxt) involves all thedescribe-something
functions. Or doing a change to function namedhello-there
in filehello.lisp
might behello(-there): ...
Noticed the reuse of file path as function name prefix? Easy to get carried away with regex, but I try to remain relatively sane about locations.
Some examples:
The trend is somewhat traceable: simple projects often only need file/function locations. Complex projects (like SRFIs and Nyxt) need more concrete locations.
Action: Explaining the Change
Commit messages are the part most commit guidelines focus on. Writing in imperative mood, expressing the "why" etc. It's Writing 101 all over again. The vital thing I took (and ignored on this blog) from writing classes? Put important things first. With commits/changes/alterations/modifications... the most important thing is the action. Verb, in other words.
Some possible verbs/actions:
- Add.
- Remove/Delete.
- Update/Amend.
- Refactor.
- Optimize.
- Fix.
- Unfuck.
The pattern is simple—whatever happens to the code, ends up in commit message. The person reading the logs immediately gets the idea of what you did (and where, if you followed previous section.) Commits tell a story of what one did to the code/data/narrative.
The action-ability of my commit messages is the reason I dislike
Conventional Commits and the kind.
You don't need to say whether the thing is a feature or a bugfix, you just say what's there.
Directly.
Add
and Optimize
also reads better than feat
and perf
.
Oh, and, this goes without saying? You can only distill the commit to one verb if it does only one thing. Make changes atomic and self-contained. You'll have no trouble explaining what you did then.
Rationale: Contextualizing the Change
Put rationale (the "why") into commit message whenever possible. That's where my style breaks some of the conventions. The reason? No one reads commit descriptions! What was the last time you, dear reader, intentionally looked through commit descriptions? Especially so—using the un-ergonomic CLI git display? Ugh.
That's why I try to put rationale for the change into commit message. The reader (often the future Artyom himself!) will thank me later.
Luckily, verb/action-orientation is good for rationale. All the "Fix" messages almost automatically hint at what was broken. All the "Optimize" messages hint at what was slow/over-allocating. And the "Remove" messages (my favorite!) often end up with "Remove X—unused!" or "Remove Y—useless!" A relief.
My favorite composition of the three things I suggested is "Add" message.
- Location (often precise to the new function or slot) shows what was added,
- Action tells that there was a signification addition,
- And there's a looooot of space for rationale after that! And you might not even need that space—location and action already tell a lot.
Formalities: Capitalization, Punctuation, etc.
The main structure (Where, What, Why?) out of the way. Time to get to formalities:
- Separate location from the rest of the message with a colon and a space.
- Capitalize the first word of the message (usually verb), unless it's a code entity or something.
- Finish messages with whatever you like. I tend to use period out of stubbornness. Most other guides recommend not using punctuation, so I guess don't use it?
- Use Unicode when necessary—even terminals are Unicode-enabled now. Add some m-dashes, won't you?
- Try to cap commit message length at 72 chars. Note that I'm not saying 50 here, like all the other guides. My style requires much more space, but also provides more information to the log reader. And then, our screens are much larger now than they used to be when 50 char restriction was useful.
Real World Inspiration: Guix!
I am not trying to say that Guix uses my convention! It's rather the reverse: my style is heavily influenced by Guix. And stripped down for my small scale projects.
So what Guix does is
- Add location (usually a toplevel directory like
gnu/
) to the front. - Add the summary for the change after location.
- And list all the files, functions/variables (in parentheses), slots (in square brackets), and minor details (in angle brackets) with detailed changes—all in the commit description.
Here's a sample commit:
It's big and has lots of syntax, but it also gives you a lot of information about what's changed! That's what I strive for in my commits—maximum meaning per minimum space. Preferably fitting things into one commit message without description (unlike Guix):
Summary: Write Commits!
- Localize the change to file/function/class/slot/variable.
- Highlight the action you've commited.
- And explain why you did it.
Hopefully y'all learned something from this posts and I can see more informative commits around! Thanks for your care 🖤