Rewriting History: git interactive rebase

By its very nature, history is always a one-sided account. Dan Brown, The Da Vinci Code

Before committing that final version of your pull or merge request, it is often useful to know how to use a feature of git called interactive rebasing. According to the documentation, rebasing is just another way to do a merge:

With the rebase command, you can take all the changes that were committed on one branch and replay them on a different branch.

Why Interactive Rebase?

When I am working on something for a few days I usually will commit to my remote branch every day at least. In my research process I often come up with multiple solutions before settling upon the ideal one. An interactive rebase makes it possible to squash multiple commits together and make your commit history cleaner.

This is in essence engaging in revisionist history. If squashing is applied (which I highly recommend) this can be done for code even if it was part of a series of commits. As stated in the documents, DO NOT use interactive rebasing for code that has already been shared with others:

The Perils of Rebasing

Ahh, but the bliss of rebasing isn’t without its drawbacks, which can be summed up in a single line:

Do not rebase commits that exist outside your repository and that people may have based work on.

If you follow that guideline, you’ll be fine. If you don’t, people will hate you, and you’ll be scorned by friends and family.

Interactive rebasing can be overdone. On projects in the real world I recommend doing squashes based on the modules they are a part of. In other words: no squashing occurs across modules.

My Implementation

To provide an approximation of a real-world example, I’m going to use the Advent of Code example from Day 1 of 2022.

(Disclaimer) Note that no claim is made as to the efficiency of the solution to the Advent of Code problem; I know for a fact that more efficient solutions exist. We are trying to learn git interactive rebasing, not working on Advent of Code.

Also note that this is not a tutorial on python packages nor python modules. Again, to keep things brief I’m focusing on git interactive rebasing.

I’ll be doing this on Windows 10, in VS Code, and in Python. A complete example is provided here. I highly recommend using it to help you follow along.

Step 1: Set up

I’ll do this in more steps than necessary so we can better perceive the intricacies associated with interactive rebasing. I know I could do this in fewer steps so please bear with me.

To begin, add the input file provided by the Advent of Code site to a git working branch. If you don’t use my example you’ll have to sign up for an account on the site https://adventofcode.com/ to get access to the input files.

In this example <path-to-code> indicates where the code lives. If working only locally the git push is unnecessary:

cd <path-to-code>
git init .
git add input1-1.txt
git commit -m "Initial advent of code input file"
(optional) git push

Next let’s establish a branch for the first interactive rebase:

git branch rebase-example
git checkout rebase-example
(optional) git push --set-upstream origin rebase-example

In English what these commands have done is:

I’d recommend pausing at this point and taking a look at a GUI tool such as gitk to review what we’ve accomplished thus far.

I’m showing the example based off of my remote on GitHub. If this tutorial goes well, I should be able to merge it into the main keypuncher repo when it is done!

Anyway, my screen shows:

Figure showing my first commit into the rebase-example branch.

Depending on how your naming convention works, “main” might be called “master,” or even something else. If all is as expected, then next add the helper python module Elf.py as follows:

git add Elf.py
git commit -m "Added helper class Elf"
git push (optional)

After that, reopen or reload you GUI tool. Mine looks like this:

Figure showing my second commit in the rebase-commit branch.

Next add the file solve1.py:

git add solve1.py
git commit -m "Part 1 of AoC solution."
git push (optional)

My GUI after performing those steps:

Figure showing 3 commits in the rebase-example branch.

Now we are ready to perform the interactive rebase!

Step 2: Simple rebase without conflicts

Before we dive into rebasing, it is important to understand what HEAD means in the context of rebasing. HEAD comes from tape readers where the head would point at the next sector to read off the magnetic tape. A good summary is on stackoverflow: What is HEAD in Git? - Stack Overflow

Furthermore, the default editor for git is typically vi or Vim. If that’s not your editor of choice you can change it to another one with the command:

git config --global core.editor "nano"

However if you are a Windows user you may not have easy access to Unix tools like nano, so you can follow the instructions on this web site to change the default git editor to Notepad or Notepad++.

We want to now instruct git to perform an interactive rebase by rewinding the HEAD back three spaces. Enter at the prompt:

git rebase -i HEAD~3

Which should result in something similar to this screen:

Figure showing screen 1 of my git interactive rebase.

First notice that commits in this prompt are in reverse order. Also notice that a lot more is possible with interactive rebases (examples include rewording a commit message, dropping commits, etc.) but I find squashing to be sufficient for most of my purposes. To squash the last 2 commits into the first one you should edit this screen as follows:

Figure showing how to switch the pick operation to a squash (input the character ‘s’).

Note the ‘s’ characters that have replaced the “pick” operations.

If you are in the default git editor (vim) simply enter “<Esc> “ followed by “:wq” (without quotes). That should result in something similar to this screen:

Figure showing the 3 commits of the rebase-example branch.

As you can see, git is dutifully squashing your commits and giving you one final opportunity to edit the commit messages. Enter something like the following:

Figure demonstrating a squashed commit.

Then (if you are using vim) enter <ESC> and “:wq” (without quotes). When I did this it resulted in the following output:

[detached HEAD 1dd3bba] 3 commits squashed together

 Date: Mon Jan 2 13:52:53 2023 -0500
 3 files changed, 2258 insertions(+)
 create mode 100644 git/rebase/Elf.py
 create mode 100644 git/rebase/input1-1.txt
 create mode 100644 git/rebase/solve1.py
Successfully rebased and updated refs/heads/rebase-example.

If you’d like, verify that everything looks OK in the gitk GUI. Mine looks like this:

Figure showing the GUI result of the interactive rebase. Notice the commits have been squashed together.

Note that I haven’t yet pushed up my remote origin yet. If you have one you can do so now, BUT you will have to do a force push because you are rewriting history! Depending on the configuration of the remote server, you may have to secure the admin’s permission to do a force push.

To do a force push user the -f option for the “git push” command:

git push -f

If successful, the gitk GUI should reflect the change:

Figure showing the 3 commits collapsed into 1 both locally and remotely on the rebase-example branch.

That’s it! You’ve just successfully executed a git interactive rebase!

Step 3: Rebase with conflicts

Things get slightly more complicated when you have conflicts arise when 2 or more people work on the same files. Turns out if you are the only developer working on this code that gets kind of tricky to simulate. Not to worry though, we’ll walk through an example.

To do so I’ll be uploading my solution for part 2 of the December 1 Advent of Code -- which has a bit more code in my solution than part 1.

First let’s create a new branch FROM THE MAIN branch (this is important if we are to simulate conflicts) as follows:

git checkout main
git checkout -b rebase-2

Then let’s add the Elf.py module and my solution to the AoC to the git staging area, then commit the change, and then (optionally) push to the remote:

git add Elf.py solve1.py
git commit -m "Part 2 of AoC solution"
(optional) git push --set-upstream origin rebase-2

Now we’ve got to introduce some conflicts. Let’s put two in there. Edit the __init__ method of the Elf module to as follows (note that we have substituted ‘n’ for ‘name’):

def __init__(self, n):
    self.name = n

Now we are going to show off VS Code’s “rename symbol” feature a bit. If you don’t use VS Code then other IDEs often have a similar feature (named refactor or similar).  On around line 3 there is a variable called elf_list. Right click on this variable and select “Rename Symbol” from the list. In the resulting box edit this symbol to ‘elf_l’ and push enter. You should have renamed every instance of elf_list to elf_l. Alternatively you can just search and replace all instances of elf_list in the solve1.py file. To be on the safe side, you should ensure your code still runs.

Add the changes, commit them, and (optionally) push them:

git add Elf.py solve1.py
git commit -m "Introduced some conflicts"
(optional) git push

After that my gitk GUI output is:

Figure showing my rebase-2 branch.

Though I already showed you how to do one, let’s practice another interactive rebase. At the terminal enter:

git rebase -i HEAD~2

The resulting screen should look something like:

Figure showing the first screen of my second interactive rebase.

Squash the second commit into the first. In other words, make your screen look similar to this:

Figure demonstrating how squashing works.

To do so, replace the 2nd “pick” with an ‘s’ character. Close the editor (in vi enter <ESC> “:wq” without quotes).

Finalize the squash by making one last edit marking the commit as the combination of 2 commits. Once that’s done your gitk GUI should look similar to the following:

Figure showing my 2nd interactive rebase has been squashed locally but not remotely.

At this point your rebase is ready to go into the remote rebase-2 branch!

git push -f

Resolving The Conflicts

We could resolve the conflicts on either of the two branches but I’ve chosen to do so from the rebase-example branch as follows:

git checkout rebase-example
git checkout -b rebase-merge
git merge rebase-2

CAUTION: git is a .357 Magnum (as my Unix teacher used to say); you’re perfectly free to shoot yourself in the foot if you don’t know how to use it.

The branch that is created with the middle step is pretty important. A lot of steps have been compressed together in that single statement. Make CERTAIN to understand what’s going on there! If not, read up on branching and merging again. What sets git apart from other software revision control systems is its uncanny ability to merge disparate branches of code together, but it MUST be understood how to do it first.

Anyways, git should tell us we have conflicts on those two branches:

Auto-merging git/rebase/Elf.py
CONFLICT (add/add): Merge conflict in git/rebase/Elf.py
Auto-merging git/rebase/solve1.py
CONFLICT (add/add): Merge conflict in git/rebase/solve1.py
Automatic merge failed; fix conflicts and then commit the result.

And indeed, conflicts have been detected in the two files we expected. Your gitk GUI should now something like:

Figure showing that conflicts have been detected on my rebase-merge branch.

We are going to employ VS Code here once again to resolve these conflicts, but your IDE almost certainly is equipped with similar functionality if you are using another one. Let’s do the simpler one first, Elf.py. In VS Code the conflicts appear as follows:

Figure demonstrating git conflict resolution in VS Code

What this these symbols mean is covered in detail here.

In this case, we want the change from the Incoming versus the Current change, so select “Accept Incoming Change” for this one. Accept the incoming changes (not the Current ones) for every conflict in the solve1.py file as well.

Once we have saved those changes we can add them then commit them as follows:

git add Elf.py solve1.py
git commit -m "Resolved conflicts from merge"
git push --set-upstream origin rebase-merge

The gitk GUI should now show something like:

Figure demonstrating the resolved conflict in the rebase-merge branch.

And that’s it! The merge conflict has successfully been resolved!

Summary

You have navigated down the twisted road of interactive rebases! Here are the pull requests on my GitHub site that summarize what has been done:

Previous
Previous

VirtualBox Red Hat Guest on Windows Host

Next
Next

Ignorance is bliss: the .gitignore file