A couple of weeks ago, I wrote about changing old commit messages. This week, we are going to take another look at changing commits.

Often when I am looking at git commit logs they look something like this:

4ec4b0d fix tests for `add`
34a7ef4 change `multiply` to accept arbitrary number of paramters
661591f change `add` to accept arbitrary number of paramters
45fd1a3 print welcome text on program start

Someone made a change in commit, committed it (661591f) and went on with their work. After making another commit, they realized that they had forgotten/broken something with their previous commit. To fix this, they would now create a new commit. This of course is okay, but it makes reviewing pull requests unnecessarily hard. I think that a pull request should tell a story and it should tell it in the cleanest way possible. In our example from above, commit 4ec4b0d should not stand on its own but it should be a part of 661591f. This is so that reviewers, when they are looking at 661591f can not only see the changed code but can also see the effect that this changed code has on the tests. Integrating these changes in the original commit also has the upside of the build never being broken, no matter what commit is currently checked out.

In order to achieve this, we can use git’s interactive rebase feature that you already saw in the blog post about changing old commit messages. To do so, we could take the commits from above and run git rebase -i 45fd1a3. We need to pass a commit hash here that is one commit before the one that we would like to change. Running this will open a file with the following content in your $EDITOR:

pick 4ec4b0d fix tests for `add`
pick 34a7ef4 change `multiply` to accept arbitrary number of paramters
pick 661591f change `add` to accept arbitrary number of paramters
pick 45fd1a3 print welcome text on program start

# Rebase d1c2ff7..50177db onto d1c2ff7 (3 commands)
#
# 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
# d, drop = remove commit
#
# 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

Here, git shows us a list of the commits that we can now work on and the commands that we can execute on them. What we want to do in our case is to change the first lines to look like this:

pick 34a7ef4 change `multiply` to accept arbitrary number of paramters
pick 661591f change `add` to accept arbitrary number of paramters
fixup 4ec4b0d fix tests for `add`

By moving commit 4ec4b0d below 661591f and changing the command to fixup (we could have also used the short form f) here, we tell git that it should take the contents of the commit and move them into the commit above. After doing this, git log is going to show the following:

3209223 change `multiply` to accept arbitrary number of paramters
c583151 change `add` to accept arbitrary number of paramters
45fd1a3 print welcome text on program start

As you can see, our fixup commit is now gone and the commit hash of what was previously 661591f is now c583151. This is because the hash is (in part) calculated from the content of the commit. We changed the contents of our commit and therefore got a new hash. Another input to the hash is the hash of the previous commit which is why the most recent commit now also has a new hash. This also has another side effect though: When the hash that your branch is currently pointing at, is different to the one that the upstream is pointing at (local will now point to 3209223 where upstream still points to 4ec4b0d), you need to push your changes in a special way. When you try to push now, git will warn you about the issue that I just described and will refuse to push. To make sure that your commits actually end up on the remote, you need to push with the --force-with-lease flag (you could also use --force here but --force-with-lease makes it less likely that you are overriding change that someone else had made in the meantime). If you don’t pay attention, force pushing can lead to code that was pushed by others being overridden. This is why it is usually advised to only push on feature branches and not on base branches like master or develop.

One more thing: If you are just working on your branch and you realize that you need to fixup a previous commit, you can pass a special flag to commit. In our example above, instead of creating commit 4ec4b0d manually, we could have called git commit --fixup 661591f. This will create a commit with the following commit message: !fixup change add to accept arbitrary number of paramters. Now you can run git rebase -i --autosquash 45fd1a3. This will open your options like above but it will already move the new commit below 661591f and will also change its command to fixup.

To make this even easier, I wrote the following fish-function:

function auto_fixup
  git commit --fixup $argv; and git rebase -i --autosquash $argv~1
end

With this function, I can now run auto_fixup 661591f and the function will create the commit and start the interactive rebase for me automatically.

With this new knowledge at your hand, why don’t you start into the week with the goal of making your pull requests just a little easier to review by making sure that they are always as precisely bundled as they can get. Rebase - as scary as it might seem in the beginning - is your - and your coworkers - friend.