Rebasing in Git is basically replaying commits on top of each other. But with interactive rebase you are in control of how they should be replayed and what should be done with them. You can re-arrange them, skip them, reword them, edit their changes and squash them together. This is a very powerful tool.

Remember, when you’re rebasing you’re rewriting history, so I would not advice to do this on a shared branch or on commits that you’ve already pushed to a remote. I only use this on local commits and branches that I haven’t pushed to a remote.

When you go into the interactive rebase menu, you are given the following options:

# 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

I’m only going to talk about pick, reword, edit, squash and fixup. These are the most common ones. The exec option is for power users.

To start off, the command is git rebase -i <base> where base is the point where you want to replay your commits on to.

So if I have the following commits and I want to change the top 3 commits, I would have to specify 5d255a1 as the base, or HEAD~3 . You can think of the tilde ~ as a minus, so it’s basically HEAD minus 3 – which would be 5d255a1 .

* 48f3b48 - (HEAD -> master) Change 3 * 390275c - Change 2 * d2f7add - Change 1 * 5d255a1 - Initial commit

So the command would be: git rebase -i HEAD~3 When you enter the command the interface looks something like this:

Notice when you specify a command for each commit, you don’t have to write the whole word (e.g. pick), you can just specify p.

If you’re not familiar with vim you have to press i to enter insert mode. To exit insert mode press ESC on your keyboard. To save changes and quit, type :wq

So let’s dive into the commands.

pick

This is the default option for commits when you enter interactive rebase. This means that you want to replay/use this commit as it is. If I don’t want to pick a certain commit, I would just delete the line.

reword

This allows you to change commit messages of commits. But take in notice, by changing the commit message Git needs to re-create the commit (with a new hash). Because Git doesn’t change the old commit, it re-creates it and allows you to specify a new commit message for the new commit.

So from the original commit history below:

* 48f3b48 - (HEAD -> master) Change 3 * 390275c - Change 2 * d2f7add - Change 1 * 5d255a1 - Initial commit

If I enter git rebase -i HEAD~3 and specify r (reword) infront of 390275c (Change 2) it allows me to change the commit message for 390275c . But after I change the message and finish the rebase, I’m left with this history:

* fd0bfcd - (HEAD -> master) Change 3 * bce9b79 - Change 2 - EDIT * d2f7add - Change 1 * 5d255a1 - Initial commit

Take a look at the top two commits, they both have changed. Change 2 now has a new commit message: Change 2 - EDIT and a new hash bce9b79 (was 390275c ). But the commit for Change 3 has the same commit message but a new hash fd0bfcd (was 48f3b48 ). Why is that? Because like I said before, Git created a new commit for Change 2 and because Change 2 is the parent commit of Change 3 it had to create a new commit for Change 3 which points to the re-created Change 2 – EDIT commit.

To explain, take a look at the following picture. This is the graph for our original commit history from above:

After rebasing with reword the graph now looks something like the following:

New commits are red

Because we decided to change the commit message of 390275c (Change 2) Git needed to re-create that commit (now bce9b79 ). Also because the parent of 48f3b48 (old Change 3) was 390275c (old Change 2) Git also needed to re-create that commit (now fd0bfcd ) so its parent would point to the new re-created commit for Change 2 – EDIT ( bce9b79 ).

The important part to know is that the old commits still exist. They’re just not in scope because the commit chain changed. master now points to the new commit fd0bfcd , which points to the new commit bce9b79 , which points to d2f7add , which points to 5d255a1 . Then again, if we would point master back to 48f3b48 (original Change 3) the history would be the same as the original. This is why Git is awesome. It’s just pointers which you can move around.

edit

This allows you to replay commits, but when you reach the commit which is marked as edit, you tell Git to stop there so you can change that commit. Not only can you change the message, but the content in the commit itself.

Let’s take an example. With the original commit history below:

* 48f3b48 - (HEAD -> master) Change 3 * 390275c - Change 2 * d2f7add - Change 1 * 5d255a1 - Initial commit

390275c (Change 2) only holds change where I added Line 2 to foo.txt . Let’s say I want to add a new file, bar.txt . That’s not a problem. Enter git rebase -i HEAD~3 and type e or edit infront of commit 390275c .

Git starts to replay the commits, but when it reaches 390275c it stops and displays the following message:

Stopped at 390275c6f22f2d8dad69a869dff0b4e90ac9e1d3... Change 2 You can amend the commit now, with git commit --amend Once you are satisfied with your changes, run git rebase --continue

To make sure I’m located on 390275c (Change 2) we can type git log --decorate and you can see that the HEAD pointer points to 390275c .

* 390275c - (HEAD) Change 2 * d2f7add - Change 1 * 5d255a1 - Initial commit

Now we create bar.txt , type git add bar.txt to add our new file to the staging area, and then git commit --amend to amend the changes to our commit. We get a window asking us to enter a commit message. I’m gonna add – NEW FILE after Changes 2, save and quit. But we’re not done. We’re still rebasing and still located on our changed Changes 2 commit. Now we need to tell Git to continue rebasing, by typing git rebase --continue

Now if we check our commit history, we notice Git has created two new commits, 7689b9a and 5c56862 .

* 5c56862 - (HEAD -> master) Change 3 * 7689b9a - Change 2 - NEW FILE * d2f7add - Change 1 * 5d255a1 - Initial commit

The same happened as in the reword chapter. Git re-created the commit for Change 2 - NEW FILE because as I said before, Git doesn’t change commits, it re-creates them and points them to the old parent.

So the graph would look something like:

New commits are red

Like before, the old commits still exist. They just aren’t in scope, because the commit chain has changed.

squash

This allows you to meld commits together, so they are combined into one. Let’s take an example. With the original commit history below:

* 48f3b48 - (HEAD -> master) Change 3 * 390275c - Change 2 * d2f7add - Change 1 * 5d255a1 - Initial commit

I want to combine d2f7add (Change 1) and 390275c (Change 2) into just one commit.

Enter git rebase -i HEAD~3 and type s or squash infront of commit 390275c . This means Git will meld 390275c (Change 2) into the previous commit – which is d2f7add (Change 1).

Git starts to replay the commits, but when it reaches 390275c it will squash/combine/merge it into d2f7add by creating a new commit and prompting you to enter a new commit message for the new squashed commit. But the cool thing is Git keeps the old commit messages seperated, but if you’d like you can change/add to it. The screen looks something like:

I added a new line that reads Squash. The commit history now looks something like this:

* 7a21e47 - (HEAD -> master) Change 3 * b3d489d - Squash * 5d255a1 - Initial commit

The same happened as in the reword and edit chapters. Git created a new commit where the changes from d2f7add (Change 1) and 390275c (Change 2) were combined into b3d489d (Squash). It also created a new commit for Change 3 because the old one was attached to the old Change 2. As I said before, Git doesn’t change commits, it creates new ones and re-arranges pointers.

So the new graph should look something like this:

New commits are red

Like before, the old commits still exist. They just aren’t in scope, because the commit chain has changed.

fixup

This is exactly like squash but it discards the commit message of the commits you mark with fixup, so it uses the commit message of the commit you squash into.

So with the original commit history below:

* 48f3b48 - (HEAD -> master) Change 3 * 390275c - Change 2 * d2f7add - Change 1 * 5d255a1 - Initial commit

I want to combine d2f7add (Change 1) and 390275c (Change 2) into just one commit.

Enter git rebase -i HEAD~3 and type f or fixup infront of commit 390275c . This means Git will meld 390275c (Change 2) into the previous commit – which is d2f7add (Change 1) and keeps the commit message of Change 1.

Then the commit history (after the fixup) should look something like this:

* 7a21e47 - (HEAD -> master) Change 3 * b3d489d - Change 1 * 5d255a1 - Initial commit

Conclusion

Interactive rebase is a very powerful tool if you would like to clean up your history, by re-arranging commits, rewording them, editing them or squashing them together. But be adviced, it rewrites history and creates new commits, so only do this to local commits and branches that you haven’t pushed to a remote.