Why I write commit messages

1. Intro

When I'm working on professional software projects, I like to include git commit messages for my changes. From my experience so far[1], most engineers do not write commit messages at work, but I've always found it hard to see a strong argument against doing it.

What do I mean by a commit message? I don't mean using git commit -m to add a one-line subject like fix: #123 fix db null issue. I mean letting git commit open your $EDITOR and typing in sentences or bullet points to write a body for the message, similar to how you'd include a description in your pull requests.

I've written messages for a lot of the changes I've made in my software career so far, and I find myself doing it more as time goes on. Coming into my first proper software job, I had read content like Tim Pope's[2] post on writing well-formed commit messages, and I assumed that other engineers were going to expect this of me, because it sounded like a sensible professional practice. This turned out to be wrong: amongst the engineers I've worked with – good and bad, from CTOs to interns – hardly anyone ever includes a message body in their commits.

This post will run through the kind of things I write about, why I find it useful, and some arguments against writing messages.

2. What I write in the message

I try to focus on providing the most important context that can't quickly be inferred from the diff. I'm writing for two audiences: the engineers I'm working with now, and unknown readers in the future. I don't know who the future reader will be or how much they already know, so I don't assume much existing familiarity. I also try to lay things out clearly and explicitly, just in case they're trying to debug a production issue late at night.

Changes, context, other considerations

I usually focus on three things:

  • What I'm changing: yes, this is technically described in the code, but if your change is more than a couple of lines, it puts a lot of work on the reader to make them figure it out from the diff, and that gets amplified if the reader is looking at multiple commits. And, the literal code committed won't necessarily have the impact you were trying to make, so it's helpful to describe the intent of the change.
  • Context for why I'm making the change: the "why" is often harder to infer than the "what", so requires some description. I try not to just link to tickets, because again that puts all the work on the reader.
  • Considerations for the reader or for code review: the risks of merging the change, gotchas, anything non-obvious that's worth highlighting. I try to make it explicit if there are parts of the change that I'm unsure about or don't understand well, or if I intend to follow up this change with further work.

I often format the message to show the what vs why vs other considerations separately, as this makes it easy to read at a glance, and it's also a nice format to copy straight into a PR description.

Examples

This is the kind of thing I'll write. I've made these up, but they're inspired by changes that I worked on in past roles:

Mitigating a production issue

fix: IM-456 change error handling order to defend OOMs and similar

Changes:
- In the main ingestion process, change the processing order to exit
  early: fetch the job, check if it's been taken off the queue more than
  3 times already, and if so bury it straight away instead of trying to
  process it.

Context:
- New payloads from account X are causing this process to OOM when we
  try to update their customer list. The way error handling was working
  was a big try/except: if it errors, catch the exception, and check if
  we've attempted the job more than 3 times, then bury it. This was
  problematic because the OOM was preventing us from ever hitting the
  "bury it" stage.

- This is currently affecting production and significantly decreasing
  throughput of the ingestion pipeline because the process keeps picking
  these messages up and OOMing. In theory burying should be our
  dead-letter solution but it's preventing that. Need to fix ASAP.

Considerations:
- One consequence I'm unsure about: we do this thing where we catch
  postgres lock exceptions and retry them indefinitely. They will now
  only retry 3 times -- what's the impact of that? We're pretty happy
  that it's safe temporarily.

- I've NOT deleted the old code as part of this. I want to minimise
  surface area of this change to reduce risk as test coverage isn't
  great.

- Alternate investigation ongoing: what is causing the OOM and how do we
  fix it. That's a bigger change.

A basic refactor

refactor(corelibs): DEF-789 move functions from helpers to lib

Changes:
- Move the db and error helper files to live in the lib directory.

- Delete the helpers directory.

- While I'm here, delete a few old util functions that aren't used.

Context:
- We have three separate places that we use to store util functions. The
  duplication is confusing and it's not clear if there's a difference
  between them or where we should put new functions. Nobody is sure, so
  this removes one of them to start simplifying.

Considerations:
- More work to do here, I'm just starting it as I have a spare 30
  mins. This at least moves it in a more sensible direction.

A performance improvement

perf(shipments): ABC-123 improve latency of shipments SQL query

Changes:
- In the SQL query used to power the shipments page, pull the aggregate
  subquery up into the top-level query.

Context:
- This allows postgres to optimize by only running the JSON aggregation
  functions on the selected rows, instead of on the whole underlying
  tables.

- The result on prod data is that loading the base report on account X
  I see a decrease from ~400ms to ~40ms, and it improves the more you
  narrow the base query. If I add filtering for the "customer segment"
  attribute, the difference becomes 400ms vs 1ms.

Considerations:
- I've used `select * from old_version except select * from new_version`
  to confirm these return the same results. If tests pass I see no risk.

- We see regressions on this query sometimes - really need to get some
  performance testing in place to prevent that, but that's out of scope
  here.

Things not to care about

Some of the more prominent commit message advice is based around formatting: writing in a particular tense, keeping subjects at 50 characters, bodies at 72 characters, etc. Personally I like to see that consistency, but it's not where the value is, and if anything it creates barriers to entry if you're dogmatic about message style. You have to pick your battles when collaborating at work, and as long as someone doesn't break standard git tools, then the part that's valuable is providing the information.

3. How I find commit messages useful

So, we have a git log filled with these messages… why is this useful? Over the years I've seen various benefits:

Knowing my past intentions

Even if nobody else read my commits, I would continue to write messages for myself. I will not remember every change I worked on in a few months, let alone a few years, so it's helpful to have those messages from the past. If I think my old code is terrible, then I can hit git blame or read the log for that file, and see that I articulated a particular reason for doing it that way two years ago – or more likely, see that there wasn't a good reason and the code really is just terrible. Either way it helps me make better decisions.

Another consequence of this is that reading a proper description jogs my memory a lot more than looking at a one-line link to a ticket number. Or, sometimes the reverse happens: a colleague will ask me a question, and I won't remember the exact answer but I'll remember that I worked on the relevant code change and I can find the answer quickly by grepping the git log.

Providing context for other engineers

Commit messages scale nicely, not just through time, but also out to a lot of people: the messages are useful for other engineers who don't know what was in my head when I made the change.

I believe there should be professional expectation amongst software engineers that we'll provide some context for our changes somewhere. There's nothing more annoying than being confused about a piece of code that's breaking production, and finding the author made absolutely no effort to communicate what the change was and why it was made: no commit message, no PR description, no detail on the ticket, no code comments, no documentation, and they left the company last month – great, I guess I'll spend the rest of my day figuring that out then. Better hope this is happening at 10am and not 10pm.

My hope with my own work is that, if I'm away, and even if it's a few years on, other engineers on the team will have an easily-accessible window into some of my rationale and thought process: what I was trying to do, why I was trying to do it, risks I knew about, anything I thought was non-obvious at the time. My reasons might hold up, they might not – what's important is that other engineers can see what I was thinking.

This is especially important if the team has low confidence when making code changes: maybe the relevant experts have left, or there's legacy sections of the codebase that haven't been changed for a few years, or test coverage isn't good enough to provide certainty when refactoring code, or there's an ongoing incident and everyone is anxious about pushing the right fix. Again, the benefit is being able to make better decisions – quicker and with more confidence – because you have immediate access to information that improves your understanding of the situation.

Looking back at accomplishments

This is another personal benefit. If I run git log --author=duck, I see a clear chronological list of all the code changes I've made on a project, with much higher fidelity than I could possibly provide if I had only written a one-line subject.

I find this a useful tool for understanding what I've actually done this quarter, or year, or even in my time at a company[3]. Whenever I've left a job, I've always checked the git logs shortly before leaving, and each time I've found changes that I had forgotten about but that were valuable contributions or interesting technical items, suitable for pulling into future interview conversations, or to include in my CV.

Spreading knowledge and awareness

I'll often run git log to see what changes have happened in a project recently. Usually I come away with limited information – I can see who is committing to a repo and some of the keywords in the commit subject lines. But when those messages contain just a few sentences on what the change is and why it's being made, then I build up better awareness of what's being done and I learn more from my co-workers. Particularly in a remote-heavy environment, you have to be explicit about building that kind of awareness in a team by repeating the information and making it easy to find, and the git log is an ideal place to do that because people will see it naturally.

This kind of knowledge-sharing benefit also applies to historic commits: the git log can be a great learning resource when somebody new joins the team, as long as it contains appropriate detail.

Focusing my code changes

The act of writing a commit message forces me to think about the work: if I can't write a concise explanation of what I'm doing and why, then maybe I shouldn't be doing it, or maybe I need to be breaking the work into smaller logical changes. Or, like rubber duck debugging, I might realise something new about my implementation when I try to explain it. Or maybe I can't articulate a message because I don't understand the technical or product details well enough, and I need to take a step back and do some learning. By doing this in the commit message, it forces me to confront it early and explicitly.

Having the information at my fingertips

Some of the value I've mentioned applies to the practice of writing PR messages more generally on a git forge service like Github or Gitlab. And, those services do have benefits that aren't easily replicated in the git log – eg. videos and images, or comment threads.

But the big advantage git has is that, if you're using it, it's at your fingertips through the whole process of writing software. I will probably check the git log after I've run pull. I'll certainly check it as part of any rebasing or merging work. If I'm curious about a line of code, I'll run git blame and look at the commit that introduced it. It's there throughout the development process, and there's value in having that extra information right in front of you in the tool you use every day, in a way that's seamless to access.

I don't think having a merge commit that links to a PR or a ticket is an adequate replacement. Maybe as a one-off, the few extra seconds clicking through the link aren't a problem, but for something that I do many times per day it adds a lot of friction. If I'm debugging a production issue and I have five commits that I'm interested in, and they all just link out to other PRs, and then some of those PRs just link through to other tickets, it significantly reduces how quickly I can understand details about the codebase and its changes, and increases the cognitive load at the exact time that I need things to be quick and easy to understand so I can debug an urgent problem.

Another place where git has an advantage is searching: yes, I can go to Github, search for a term, queue up browser tabs for all the found PRs, and look through each of them individually to find what I want. But searching the git log in my terminal is absolutely trivial and instant: I type git log, it opens less or another pager, I hit forward slash, I type my search term, and I can page through the entire history of the repo interactively in a few seconds. Or I can run it through grep, or narrow it by directory or a particular set of files. The experience isn't comparable – it's literally 10x faster to find what I want using git, the data is all local so I can do it offline, and we don't need any special tools other than git itself.

4. Arguments against?

I think there are valid and less-valid arguments against including message bodies. I'll include them roughly in order of how strong I think the argument is:

Production is broken, I don't have time for this

That's fair – if it's very urgent then link to a ticket or postmortem doc that can be updated later. If you can spare a few minutes though, keep in mind that production incidents are a time when clear, explicit reasoning and communication around the code change are particularly important.

I've already included this info in code comments

That's a pretty good reason. If my diff contains useful comments or docstrings I'll sometimes just mention that in the commit message. You can also copy useful info from the code comments into the commit message, the duplication isn't a problem.

Laying out my thought process is intimidating

For more junior engineers, I think this is understandable. The times where I've really not wanted to explain myself are times where I've not entirely understood what I'm doing or why I've been asked to make a particular change. If I just don't leave a message, then I avoid the risk of getting it wrong and looking silly, I don't have to admit to anyone that I don't understand, and I won't have my ignorance encoded in writing forever. It's easier to not do it.

This is just something you have to overcome: the career involves a lot of not knowing things and having to learn them, it's normal. For me it's become easier over time to share when I don't know something, because I'm more secure that there are things that I do know, and I've had times where I've seen the payoff in those old messages – the value has outweighed the embarassment.

I prefer to put this information in the PR or somewhere else

This is the reason I've heard the most – people think it's valuable to leave some contextual information somewhere, but prefer to do it on the PR or in an issue tracker.

The git log certainly has limitations. It's not a place for video or images, it doesn't contain your review comment threads, it's not very accessible for Product or other orgs, and it only captures state at the point in time that you made the code change. By using a tool that's decoupled from the commit, you can use richer media or access information that wasn't available when the commit was made – eg. maybe you can see deployments in your git forge tool or alongside a ticket. That information can be extremely valuable for understanding the particular code change and its impact, and so there are more suitable places to put this contextual information than the git commit.

To this I'd argue: if your tooling is consistent through the team, it works and it puts all that information in front of you instantly in the same way that running "git log" does, then that's excellent and it's a workflow to aspire to.

But, just because PRs and other tools can hold similar information, it doesn't make the git log useless. If you're using git to commit changes, then people will very likely want to look at the log at some point, and it doesn't hurt to duplicate some information so that the commits can be read more easily.

As mentioned above, I think git puts the information in front of you in a more naturally accessible way than forge tools – I don't find PRs an adequate replacement. But even if you prefer the UX of web-based tools, there's another argument for git: open-source version control tools are likely to outlive the data in SaaS services. This is admittedly low down on the list of problems for startups, but people do make repos private, they do misconfigure Jira and lose state, git forge companies accidentally delete production databases, license or pricing changes can force you out of a service, servers go down, accounts get closed, companies go out of business, and sometimes you just decide to change to another provider – shit happens and that data can go away, or the ties between the data and the commit can be broken. Meanwhile every engineer in the team has their own immutable offline copy of the git history, where the commit messages will persist for as long as the code does.

I'd have to significantly change my git workflow

Some personal git workflows make it harder to write messages. If that's the case for you, then that probably means the workflow also makes it hard to make and merge useful logical commits, and I'd argue that you should consider changing it[4].

Some team workflows also make it harder to write messages, and if you can't influence the team to change conventions this one can be awkward to work around. For example, if your team's tooling is forcing a squash of your commits when the PR is merged, this will replace your deliberate commits. If that's the case, hopefully you can disable that feature on a per-PR basis – if not one thing you can do is include a clear message in the merge commit description.

I explain all this stuff over video calls or IRL

Face to face communication can be higher-signal, and some things can be better communicated verbally, but unless you're recording the call and providing a transcript or notes, it's all lost the moment the call ends, and it's not accessible beyond the participants of that one meeting. If I come across the commit in six months time, I won't remember the full content of that discussion, and I might not even remember that we had a meeting about this change. Writing is more permanent and increases in value over time as people forget. At the very least, record the video or audio and link to it.

It takes a long time to write a message

If you don't want to spend 10 minutes extra writing a message for a logical code change, I think you should reconsider as the ROI for the team is significantly higher than that investment[5]. If you really struggle and it takes hours then this could be a valid reason. If you have a way to share screencasts with your team, that can be an alternative solution.

If you already write notes on your PRs, then there's almost no extra cost at all, as you're already spending time writing about the change.

Commit messages can be wrong so you should just look at the diff

Useful commit messages will highlight context that can't be reliably inferred from just reading through the diff – that's the value of them. Certainly it's possible to write bad messages, but I don't think the potential existence of bad messages should prevent you from trying to write a good one.

I prefer seeing a one-line commit log without the extra noise

I want that too sometimes – there are various ways to achieve it, including the git log --oneline flag.

I see no value in the messages

I do appreciate that not everyone will want to read messages or find them useful, but I've seen it help myself and other engineers in the past, and I've seen the cost and frustration when key context about a change hasn't been made easily available. Even if you personally never want to read the messages, there's no big cost to adding them: you're not making a tradeoff where you're losing something by including extra information in your commit message, and they're easier to write than code comments because you don't have to worry about how they'll change in the future – you just have to capture what you're thinking now. The only thing you'll lose is a small amount of time invested in writing and sometimes editing the messages.

5. I'm sold! How can I get started?

The short answer is just don't use the -m flag, write message bodies in your editor or tool of choice, and focus on information that won't be apparent to the reader from the diff. For me this usually falls under one of three categories: the changes I'm making, the context of why I'm making it, and non-obvious considerations for the reviewer or other future readers. The more you do it, the easier it will become.

I started writing more detail on this section, but nobody wants to read a post this long about commit messages. Maybe another time.

6. Summary

TL;DR: The best time to write a commit message was 20 years ago[6], the second best time is now. I find having detailed commit messages very valuable, and encourage everyone to do it if you're collaborating on professional software that needs maintaining. It's not much different to writing PR descriptions – you just put the info in the commit message body so it's easily accessible in git commands like log and blame. The benefits include:

  • Being able to see what you were thinking months or years ago when you made a change.
  • Being able to see what other engineers were thinking when they made their changes.
  • Having a clear chronological log of work you can look back through.
  • General spreading of knowledge and awareness in a team.
  • Forcing you to explicitly understand what changes you're making and why by writing it down.
  • Having this information at your fingertips in the tool we all use to manage changes in our codebase every day, and in a format that will persist for as long as the code does.

I've long given up thinking I can convince everyone to do this, and sometimes I wonder if I'm missing something because I've worked with plenty of good engineers who don't do it. Sure, it's not as important as shipping features, and you can certainly do good work without caring about commit messages. But my view is that the pros outweigh the cons and the ROI on writing messages for your git log is well worth it. It doesn't cost anything, it scales well across people and timezones, its value increases over time, and it presents useful information in an easily-accessible way that you just don't get from just using the web tools like Github.

2023-Sep-04