Rewrite History With Git Rebase Interactive And Reflog

By Clémentine Pirlot

Image credit: https://www.pexels.com/photo/person-typing-on-typewriter-958164/

Before I started interactive rebasing my commits, I had heard scary things about it. But coding is about telling stories and rebasing is part of the journey. It helps your code tell a clearer story and makes your repository’s commit history easier to read. And it doesn’t have to be dangerous thanks to reflog.

Git allows you to rearrange your commits with the interactive rebase. First look at your log with the command:

git log

This will open the list of commits in your terminal. Press enter to scroll and then type q to quit the list and come back to your terminal. Let’s say your log looks like this:

commit ca82a6dff817ec66f44342007202690a93763949
Author: Clementine Pirlot <emailaddress>
Date: Mon Mar 17 21:52:11 2008 -0700

Fix typo

commit 605bd3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Clementine Pirlot <emailaddress>
Date: Sat Mar 15 16:40:33 2008 -0700

Add unit tests
commit 085bb3bcb608e1e8451d4b2432f8ecbe4324e7e7
Author: Clementine Pirlot <emailaddress>
Date: Sat Mar 15 16:40:33 2008 -0700

Do something that no longer applies
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Clementine Pirlot <emailaddress>
Date: Sat Mar 15 10:31:28 2008 -0700

Add functionality

You don’t want the typo commit and the one that no longer applies to show up in your history, they don’t bring any value and add unnecessary commits to the functionality. You can do many interactive rebases in a row but for this example let’s say we want to modify these 4 commits. The command to type is

git rebase -i HEAD~4

The -i is for interactive and the HEAD~ followed by a number indicates the number of the last commits you want to include in this rebase (you can also specify a commit hash directly, in this case HEAD~4 is only a reference to commit a11bef06). This command will open the default git text editor which is usually vim. If you’re not a fan of vim you can change it with git config --global core.editor “code --wait” replacing code with whichever editor you want. Once the rebase file is open you will see a line for each of these commits and some helpful documentation.

pick a11bef0 Add functionality
pick 085bb3b Do something that no longer applies
pick 605bd3b Add unit tests
pick ca82a6d Fix typo
# Rebase eef025f..52d795e onto eef025f (4 commands)
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# 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

Let’s take care of the no longer relevant one first. This is code that is no longer useful for this branch, something that you tried but then turned out to not need after all (for example, after a refactor commit that undid these changes). We want to discard this commit and its changes entirely and erase it from the branch history. To do that you replace the pick keyword with either drop or the abbreviation d.

drop 085bb3b Do something that no longer applies

Now for the typo commit, these are changes we want to keep in the code but we don’t want to see an entire commit just for this. So we want to integrate the code changes into another commit, the one where we made the typo. In our file here the typo commit doesn’t come directly after the commit it belongs to. There’s the test commit and the no longer applies commit in between. So to make sure our typo commit goes into the first commit in this list we move the entire line right under it. This can also be used to simply change the order of commits without altering them with a keyword. It could make the history easier to read if it follows a logical progression.

Here we have several options to make this commit part of the first one. We can choose to fixup, which will merge our changes with the previous commit and discard our commit message, or we can squash it, which will do a fixup and a reword so we can rewrite the previous commit’s message. What we will do here is a fixup because the commit message of the first commit doesn’t need to be changed. So we replace pick with our new keyword, either fixup or f.

After all these changes our file looks like that now:

pick a11bef0 Add functionality
fixup ca82a6d Fix typo
drop 085bb3b Do something that no longer applies
pick 605bd3b Add unit tests

When you’ve done all your changes you can save the document and in your terminal type git rebase --continue. You may have conflicts to solve, it sometimes happens to me when I change the order of commits. In that case fix the conflicts, save the file, stage it and type git rebase --continue again. It then should say:

Successfully rebased and updated refs/heads/master.

You now have the clean history you wanted!

If you haven’t pushed these 4 commits you can now push and if you had already pushed them you have to push --force as your history has been changed. It’s very important that you never force push on a branch that anybody else uses. For example, you usually want to do that when your PR has been accepted and you want to clean it up before merging it.

There are many other useful commands than drop and fixup, as you can see in the documentation you get in every interactive rebase file. I often use reword and edit, the first one to change a commit message, and the second one for more refined changes, for example when I need to remove a file or a change from a certain commit.

What if everything goes to hell, have I lost all these 4 commits? No, git being the awesome tool it is, it’s got your back. First, if you get conflicts and realize it’s too complicated to be worth it you can cancel the whole thing with git rebase --abort and it will put your 4 commits back to how they were. I’ve used it several times when I was trying to change the order of my commits but it turned out to be difficult and after I aborted I would interactive rebase again with just my fixup type changes.

What if you’ve rebased and realize you made a mistake, the commit you dropped was useful after all, there was one line you needed? That’s when reflog comes in. You can still come back to the state when you had your 4 commits, even after you pushed! Reflog is similar to git log but with a difference: think of it like actions, every action you do in git is stored there. You could see your branch changes, your pulls, and thankfully your rebases too! To go back to before the rebase you have to find this action in your reflog. Type git reflog in your terminal and look for the line right before you started your rebase:

52a395e (HEAD -> master, origin/master, origin/HEAD) HEAD@{3}: rebase -i (start): checkout HEAD~4
56d785e (HEAD -> master, origin/master, origin/HEAD) HEAD@{4}: pull: checkout 52d795ea1809abc941a9d267c1e8b024b9cae89e

Here we have it as this at HEAD@{4}. Now that you have it you can type git reset --hard HEAD@{4} and you will come back to this point in time (be careful when doing that as you will forever lose any changes that were not committed). You can push with force since you have again rewritten your history or you can rebase again differently before you push.

Thanks to reflog, interactive rebasing doesn’t have to be scary and dangerous. You can be the boss of your own history and merge clean branches into your repository. We don’t code for computers, they couldn’t care less if it was C# or binary, we code for humans. Just like writing code with understandable names and progression is important for you and other people who will read your code, having a logical progression in your history is easy to read and maintain. You can tell your story the way it should be told, with no noise or distractions and take your reviewers and everyone who will look at this app’s history from the beginning to the end smoothly.