If you have ever inspected the Git history of some of your projects (doing git log
) I am sure that you may have encountered something like this in your main
branch:
f1e2d3a fix
a9b8c7d try again
0a1b2c3 Update
d4e5f6g stuff
7h8i9j0 oooops
k1l2m3n Final final fix
o5p6q7r more changes
s8t9u0v Asdf
w1x2y3z WIP
z4x3c2v This should work
This looks really unprofessional, but the problem goes beyond aesthetics. Having poor descriptions like this is a problem that is more common than it should be. I believe that there are two main reasons why developers tend to do this:
- A lack of understanding of the importance of clear, meaningful commits often leads people to skip them, not realising the value outweighs the effort.
- A lack of knowledge to improve their commits, even if they wanted to, as discussed here: https://jm.armijo.au/dev/blog/2024/10/30/the-impact-of-mastering-our-day-to-day-tools/
In this article, I will explore what defines a good commit, why we should care about them, and how we can write good commits with the help of Git.
Good commits
A good commit requires a clear message, and that the code can run. Those things seem obvious, but they are actually not. Let’s discuss them in detail.
Commit Messages
In my opinion, a great commit starts with a great commit message. To qualify as such, it should have at least these two things:
- A reference to the task that prompted the change (e.g., Jira or Trello ticket).
- A clear, meaningful description of the changes made.
When someone makes a commit, they usually have all the context about the task, so writing the obvious may seem like a waste of time, but it is not. A clear description allows people to understand what was changed without having to read the code, and a reference to a well maintained ticket to allow them to quickly find relevant documentation about the change, like architectural or UI designs, decision records, etc.
When you take the time to write good commits you are saying:
To whoever reads this, I’ve done my best so you know what changes are in this commit and why I made these changes.
When you don’t write a good commit, you say this instead:
To whoever reads this, I don’t care if you understand this or not. I just want to ship my code. Good luck.
Personally, I like to use a format where the first line has a summary of the change, and the following lines provide more details about what really changed. For example, instead of This should work
, we could write something like:
JM-1234: Add user profile customisation options
- Add `ProfileController`
- Add routes for handling profile updates.
That said, the exact format is not important, as long as you or your company have a standard and you consistently follow it.
I have been in situations where the code base is so old that everyone who worked on that project already left the company, so there is nobody to ask. I’ve also experience cases when I need to find that commit now, but the person who wrote the code is in another timezone, probably partying or sleeping, and the task does not justify paging them.
So trust me, there is always someone that will have do get their hands dirty. This may even be your future self, and yeah, I’ve also had to look for commits that I have made with no clue of what I’m looking for until I read the message that gives me the answer.
The same reason applies for using a fixed formatting for our commit messages. We can write simple scripts to help us in our quest to find a commit as long as they have useful information to search for and have a structured format that makes parsing easier.
Running Commits
Last but not least, I believe that every commit in main
should contain a valid copy of the software. This means that if we check out that commit we should be able to successfully run the software and all tests should pass.
The reason for this is not always straightforward, after all, what matters is that the last commit in the build works, right? Well, it depends on who you ask.
If your sole goal is to ship code, yeah, maybe this would be true, but as we saw earlier we also want everyone to be able to inspect the history of changes, and that includes finding bugs.
Yeah, I have often gone through the commit history trying to identify the commit that introduced a tricky bug. It’s kind of simple, you just check out a commit, run some code to reproduce the failure (usually a test), and voilà! Except that if the commit temporarily broke the code then it’s no longer possible to test if the bug I was looking for was present in this commit or not.
Just to be clear, I’m not saying that commits will be bug free. We do our best to avoid bugs, but they get in regardless. What I am talking about is when we have a branch where we break something and then we fix it in the next commit, but the bad and the good commit are shown in the pull request and land as separate commits in the main
branch.
Some developers I have spoken to justify merging their list of changes as a bookkeeping, to record the history of their changes. But trust me, in 17 years of writing and debugging code, I have never ever had to know in what order a change was made, nor seen or heard of anyone who has benefited from having a detailed list of commits for a single PR.
So, should you only commit when you know your changes work? Not really. Please keep committing as frequently as you can, that’s actually good! We’ll solve this dilemma with Git.
Git To The Rescue
Git provides a way to combine two or more commits into one. So, you can commit as many times as you want, and when your code works, you can combine all these commits into a single commit.
Good commits in any intermediate commits are useful to you, but in a combined commit they are useful to your fellows developers. There’s no need to specify that you tried something and then changed your mind. What really matters is the final result, as this is what you will merge (when your PR gets approved).
Squashing commits is very simple. Let’s pretend that we want to squash the commits in our example:
$ git log --oneline # top commit is newest
f1e2d3a fix
a9b8c7d try again
...
s8t9u0v Asdf
w1x2y3z WIP
z4x3c2v This should work
You can execute this command to squash all these commits:
git rebase -i z4x3c2v^
So, we use the rebase
command to squash commits. We specify the -i
flag to go to interactive mode (you’ll see what this is in a minute), and then we specify the first commit that we want to squash, plus the ^
character to specify that this is inclusive of this commit.
This will open your editor for you to tell Git what you want to do with those commits (this is why this is interactive). You will ses something like this:
pick z4x3c2v This should work
pick w1x2y3z WIP
pick s8t9u0v Asdf
...
pick a9b8c7d try again
pick f1e2d3a fix
Note two things: commits are shown in reverse order, and before every commit there is a pick
word, which means that Git will use this commit normally. If you save this file and exit, everything will stay the same.
Additionally, below the commit messages, there is a list of all valid options that you can use, with a description of what they do. In this example we’ll just use one of them.
To squash your commits you need to change the pick
word to squash
in all but the first commit, like this:
pick z4x3c2v This should work
squash w1x2y3z WIP
squash s8t9u0v Asdf
...
squash a9b8c7d try again
squash f1e2d3a fix
Then you can save the file, and Git will ask you to edit the commit message. It will include all existing messages so you can use them if needed, or you can just discard them and write an entirely new commit message. When you save, this new commit will replace all the commits that you specified, leaving a clear history before you send your changes for review.
Of course, for this to work you need to test your code. Execute the linter, unit tests, and any other checks you can before you push your changes. You can do this using a pre-commit hook, so you don’t even have to remember to do it.
GitHub allows us to squash our commits when merging. This should not be taken as permission to avoid cleaning our work before raising a PR, but as an opportunity to do an extra cleanup due to review comments. If we clean up our commit history before raising a PR we help prevent the consequences of an “oops, I forgot to squash when merging”; and it also shows respect for our teammates.
Clear, meaningful and functional commits are more than just good practice, they’re a way to respect your team, yourself, and the codebase. We can foster collaboration and make debugging far less painful if we follow these simple steps.
Good commits may take a little extra effort, but the payoff in efficiency and professionalism is well worth it. Let’s aim for a cleaner, more maintainable Git history, one commit at a time!
Cheers!
JM
Share if you find this content useful, and Follow me on LinkedIn to be notified of new articles.