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.
git commit --amendrather than
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:
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:
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."
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.
Based on our example, we will run either:
$ git rebase -i 528f82e
$ git rebase -i HEAD~3
Which opens up this window in Vim:
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.
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.
Back in the terminal we see this 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:
So we fix the merge conflict in the file but what do we do now? When in doubt,
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:
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
We don't have anything else to change in the commit so let's continue!
git rebase --continue and we see the following message:
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.
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.
Our git log now looks like:
You will notice a few things have changed from before we started the interactive rebase:
6c01350with the commit message “Remove merge conflict”. This is the commit we deleted in our interactive rebase.
4a4d705, has a different SHA-1,
41aa9d2, also has a new SHA-1,
2b95e92. This commit was not changed but was simply applied to the commit before it
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:
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.
Orignally Posted on Dev.to