Keep your git commit history clean with squash
Don’t let your git history become a mess
It’s 8 am and you are ready to work on a new task. You pull the latest updates from the main branch and checkout a new branch (if not, shame on you), eager to start coding. You make some changes, commit. Keep working, another change then another commit. I guess you got the point.
You might think that’s nothing wrong with it, but also all your team mates will do the same and in a jiffy your git history will be a mess.
Still can’t see the problem? Ok, imagine that after a while you need to investigate to find which commit caused an issue looking through an endless list of a useless commit. Of course git bisect
can come to the rescue but without a clean history it can be ineffective.
I strongly believe that git history is part of code documentation and it should effectively describe what was changed and why. So there is no reason to have a git commit history that looks like this:
You have 5 commits, all related to a single feature and 4 of them have no reason to exist since they don’t provide any useful information about what you have added or changed. Wouldn’t it be better to have just one?
What is git squash
Squashing n commits will create a new commit obtained by combining all the selected commits. A picture is worth a thousand words:
Looks cool, how can I do it?
Easy tiger, there is an important thing we need to consider first. We are about to rewrite the history here, so it will be safe as long as we are working on our local machine or we are the only one working on a branch. In fact, squashing already pushed commits will fail unless a --force
option will be provided.
That being said it’s time to squash! We will use git rebase
command, with an interactive option. A rebase is a way to replay commits, one by one, on top of a branch. An interactive rebase is a way to replay commits, one by one, choosing interactively what to do with each one. Possible actions are:
- pick (p): use commit
- reword (r): use commit, but edit the commit message
- edit (e): use commit, but stop for amending
- squash (s): use commit, but meld into the previous commit
- fixup (f): like “squash”, but discard this commit’s log message
- exec (x): run command (the rest of the line) using shell
- drop (d): remove a commit
Now we have two options here: specify how many commits we want to squash git rebase -i HEAD~[number of commits]
or pick our target commit by his hash git rebase -i [hash of "target" commit]
. No matter what option do you choose, we are saying to git that we want to do an interactive rebase, so your configured editor will pop up showing something like that:
Commits are shown in order so keep in mind that we will need to pick at least one commit before the commits we want to squash (other actions are out of scope for this post). Saving changes and closing the file will open a new editor window where we can work on the message for the new commit that will be created. Once we are happy with it just save & close and we are done. Obviously don’t forget to push it to remote.
Make life easier
If you plan to use this approach I strongly suggest to setup git in order to “reduce” required work when we perform an interactive rebase. First of all, configure autosquash with git config --global rebase.autosquash true
. From now on you if you already know that your next commit(s) will be squashed you can simply prepend a commit message like these: git commit -m 'squash! commit message goes here'
(same applies to other actions like fixup!
, edit!
, …). Doing so will end up in an already pre-populated task list for interactive rebase, saving you some time fighting with vim.
Nice, but still a lot of work
Ok, someone is being lazy here. If you like the idea of having a clean commit history but you don’t want to do a rebase for every feature branch you can simply use the --squash
option when merging: git merge --squash
. You will get the same result but with slightly less control over what you are doing.
Also, services like GitHub, GitLab, and Bitbucket offer the possibility to select squash as a merge strategy for pull requests.
Wrap it up
- git history is part of the documentation, leave it as you would like to find it
- Be extra careful when performing rebase on a shared branch
- Consider using/force squash as the default merge strategy
- Write a meaningful commit message
As for most of the best practices in software development, squashing commits will be effective only if shared and adopted by all of the team members.