Beginner's Guide to Interactive Rebasing

Beginner's Guide to Interactive Rebasing
Down Arrow

Earlier this year I did an interactive rebase for the first time and I was impressed by what one can do with it. I also found it to be a little complex at first. Hopefully this guide will help remove some uncertainty around it.

Also, because it is so powerful and you can essentially rewrite history, a small warning before we begin: There are many schools of thought on Git and whether rebasing is a good idea or not. This post will not dive into those discussions and is purely meant to walk through the basics of using interactive rebasing.

TL;DR

  • Interactive rebasing can be used for changing commits in many ways such as editing, deleting, and squashing.
  • To tell Git where to start the interactive rebase, use the SHA-1 or index of the commit that immediately precedes the commit you want to modify.
  • During an interactive rebase, when Git pauses at a commit you tagged to edit, the workflow is no different than a normal commit process - you stage files and then commit them. The only difference is you use the command git commit --amend rather than git commit.
  • Interactive rebasing will create new SHA-1’s therefore it is best to use interactive rebasing on commits you have not pushed to a remote branch.

The Problem

In this example, we will work through a situation where we have been working in a feature branch and we have a couple commits we would like to change or delete. Here is what our git log looks like:

original git log

From the git log above, here are the two commits we want to change: 
4a4d705 - In this commit we accidentally committed a merge conflict
6c01350 - In this commit we removed the merge conflict

What we would like to do is go back in time to 4a4d705, remove the merge conflict in the commit, then delete 6c01350 since the merge conflict is resolved and we no longer need this commit. This will be an improvement to our commit history for two reasons:

  1. We won’t have broken commits (merge conflicts)
  2. We will only have meaningful commits, no commits related to solely fixing a missed merge conflict

The Solution

This situation is a good candidate for interactive rebasing. Scott Chacon, in his book Pro Git, describes interactive rebasing as follows: "Sometimes the thing fixed ... cannot be amended to the not-quite perfect commit it fixes, because that commit is buried deeply in a patch series. That is exactly what interactive rebase is for: use it after plenty of [work has been committed], by rearranging and editing commits, and squashing multiple commits into one."

What commits do we want to modify?

To start an interactive rebase, we need to tell Git which commits we want to modify. We do this by referencing the commit immediately prior to the earliest commit we want to modify. Or, put simply, we reference “the last commit [we] want to retain as-is,” according to Scott Chacon.

Let’s look at our example to have a better understanding. There are two ways to reference this commit:
By SHA-1 - The last commit we want to retain as-is has a SHA-1 of 528f82eso we can pass this into our interactive rebase command.
By Index - The last commit we want to retain as-is has an index position of 3 (Git uses zero-based indexing) so we can pass in HEAD~3 to our interactive rebase command.

Note - If you only have a few commits on which to interactive rebase, using the index is probably easier for most people. However, if you have many commits, using the SHA-1 is probably easier so you don’t have to count all the way down the git log.

Start interactive rebasing

Based on our example, we will run either:

$ git rebase -i 528f82e

Or

$ git rebase -i HEAD~3

Which opens up this window in Vim:

Vim window with list of unchanged commits

Notice the commits are in the opposite order of git log. In git log the most recent commit is on top. In this view, the most recent commit is on bottom. Also notice the comments below give a helpful list of the valid commands we can use on each commit.

If you don’t know Vim, just click on each word pick that you want to edit and then hit the <i> key (for insert mode). Once you’re done typing hit the <esc> key to exit insert mode.

Vim window with list of changed commits

In our example, we have changed the command to edit for the commit we want to modify and we have changed the command to drop for the commit we want delete. Then we run :wq to save and quit the Vim window.

Amend the commit

Back in the terminal we see this message:

Git rebase interactive initial message

This makes sense that we are stopped at 4a4d705. This is the oldest commit in the series of commits we want to modify. We will begin with this commit and work our way through each commit until the most recent.

As a reminder, 4a4d705 was the commit with the merge conflict we accidentally committed. When we open up our editor we see the merge conflict there:

tempChanger.js file contents

So we fix the merge conflict in the file but what do we do now? When in doubt, git status:

Git status after changing file

Cool! This is actually helpful. We see that we are currently editing 4a4d705, and we see the next two commits to be acted upon after this one.

The rest of the message is explaining a familiar workflow to us. Git tells us if we want to amend the commit we run git commit --amend. This will essentially act as our typical git commit we use in a normal workflow. At the bottom of this message we see our file was modified reflecting the changes we just made to remove the merge conflict. We need to stage the file before we commit. This no different than a normal workflow.

All we do is git add tempChanger.js to stage the edited file and then git commit --amend to commit the staged file! This will now open a Vim window again with the commit message:

Vim window of first commit

We can either edit the commit message or leave as is. Let’s choose to keep the commit message the same and we will type :wq to save and quit the window.

We have now edited our old commit. So now what? Run git status:

Git status after amending first commit

Continue interactive rebase

We don't have anything else to change in the commit so let's continue!

We run git rebase --continue and we see the following message:

Git rebase --continue

Woah, we’re done? But what about those other two commits? Well, the next commit to be acted on was 6c01350. This commit we marked to delete (drop) when we started the interactive rebase. Git automatically deleted it and moved onto the next commit, 41aa9d2. This one was never modified in the initial interactive rebase. Its default command was pick which means the commit will be used. Git applied that commit and since that was the last commit, the interactive rebase completed.

Note, if we had more commits to edit, we would’ve simply moved onto the next commit and started the process of amending it just like we did above. The cycle continues until there are no commits left.

The eject button

It is worth noting if at any point in our interactive rebase we screw things up and don’t know how to fix them, we can always abort. At any point we can run git rebase --abort in the terminal and the interactive rebase will be aborted with no changes saved. We would then need to start the interactive rebase over again.

After interactive rebase

Our git log now looks like:

Git log after interactive rebase

You will notice a few things have changed from before we started the interactive rebase:

  • We no longer have the commit 6c01350 with the commit message “Remove merge conflict”. This is the commit we deleted in our interactive rebase.
  • Our commit we edited, 4a4d705, has a different SHA-1, 2b7443d.
  • Our most recent commit from our original git log, 41aa9d2, also has a new SHA-1, 2b95e92. This commit was not changed but was simply applied to the commit before it 2b7443d.

Side effects of interactive rebasing

For the two most recent commits in our git log, because they have new SHA-1’s, Git sees them as entirely new commits. This is even true of our last commit, 2b95e92, where neither the commit message nor the files changed at all. This brings up an important point with interactive rebasing: If you modify a commit, that commit and all successive commits will have new SHA-1’s.

This won’t affect anything if the commits that you have modified haven’t been pushed to a remote branch. However, if you did in fact complete an interactive rebase on commits that were already pushed to a remote branch and then pushed your branch again you would see:

Git push error

Technically, you could get around this by using git push --force but that is very dangerous. If the remote branch has commits from other people but your local branch does not have those commits yet, you will effectively delete their commits.

Another solution is to use git push --force-with-lease which will only modify your commits but not commits belonging to others, though this can also be problematic. For example, if another developer already has those commits that were given new SHA-1’s on their local branch, when they pull the remote branch, they will have merge conflicts with each of these commits.

When to use --force-with-lease is beyond the scope of this blog post but it would be best to consult other members of your team before doing so. You can read more about git push --force-with-lease here.

The key takeaway from this section is it is much easier and safer to use interactive rebasing on commits that have not yet been pushed to a remote branch.

Originally Posted on Dev.to

Blake DeBoer

Blake DeBoer

Consulting Software Developer III

No items found.
green diamond