This post is part of a Git 201 series on keeping your commit history clean. The series assumes some prior knowledge of Git, so you may want to start here if you’re new to git.
As I work on a branch, adding commit after commit, I’ll sometimes find that there’s a piece of work I forgot to do which really should have been part of some prior commit. The first thing to do—before fixing anything—is to save the work I’m doing. This might mean using a work-in-progress commit as described previously, or simply using the stash. From there, I have a few options, but in this post I’m going to focus on using the “fixup” command within git’s interactive rebase tool.
The essence of the process is to make a new commit, and then use git to combine it with the commit I want to fix. The first step is to make my fix, and commit it. The message here really doesn’t matter since it’s going to get replaced anyway.
> git add -A > git commit -m "fix errors"
This will add a new commit to the end of the branch, but that’s not actually what I wanted. Now I need to squash it into the older commit I want to fix. To do this, I’m going to use an “interactive rebase” command:
> git rebase -i master
This is telling git that I want to edit the history of commits back to where my branch diverged from master (if you originally created your branch from somewhere else, you’ll want to specify that instead). In response to this request, git is going to create a temporary file on disk somewhere and open up my editor (the same one used for commit messages) with that file loaded. It will wind up looking something like this:
pick 7e70c43 Add Chicken Tikka Masala pick e8cc090 Remove low-carb flag from BBQ ribs pick 9ade3d6 Fix spelling error in BBQ ribs pick b857991 fix errors # Rebase 1222f97..b857991 onto 1222f97 ( 5 TODO item(s)) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to # bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
All of these commits are those which I’ve made since I cut my branch: ordered from oldest (on the top) to the newest (on the bottom). The commented out parts give the instructions for what you can do in this “interactive” portion of the rebase. Assuming that the fix is for the Chicken Tikka Masala recipe, I’d want to edit the file to look like this:
pick 7e70c43 Add Chicken Tikka Masala fixup b857991 fix errors pick e8cc090 Remove low-carb flag from BBQ ribs pick 9ade3d6 Fix spelling error in BBQ ribs
When I save the file and quit my editor, git is going to rebuild the branch from scratch according to these instructions. The first line tells git to simply keep commit 7e70c43 as-is. The next line tells git to remove the prior commit, and replace it with one which is the combination of the prior commit and my fix-up commit, b857991. The other two commands tell git to create two new commits which result in the same end state as each of the old commits, e8cc090 and 9ad3d6.
As a bit of an aside… Why does git have to create new commits for the last two commands? Remember that commits are immutable once created, and that part of the data which makes up the commit is the parent commit it was created from. Since I’ve asked git to replace the parent commit, it will now need to create new commits for everything which follows on that same branch since each one now has to have a new parent: all the way back to the commit I replaced.
At the end of all this, if we were to inspect the log, we’d see:
> git log --oneline master.. 6a829bc3 Add Chicken Tikka Masala 29dd3231 Remove low-carb flag from BBQ ribs 0efc5692 Fix spelling error in BBQ ribs
In essence, everything appears to be the same, except that the original commit includes my fix. However, looking a little closer, you can see that each commit has a different hash. The first one is different because I modified the “diff” portion of the commit (i.e., I added in my fix) and therefore a new commit was needed (commits are immutable, so adding the fix required making a new one). The other two needed to be re-created because their parent commit disappeared, and therefore new commits were needed to preserve the effect of those two commits, starting from my fixed-up commit as the new parent.
There is one caveat I have to warn you about when using this method. Any time you rebase a branch, you’ve changed the commit history of the branch. That is, you’ve thrown out a whole bunch of commits and replaced them with a completely new set. This is going to mean loads of difficulties and possibly lost work if you’re sharing that same branch with other people, so only use this technique on your own private working branches!