William Vaughn

The git-rebase manual review

I’ve spent most of my career being scared of git rebase. I looked on with envy as my git wizard colleagues used it like magic to rewrite history and time travel. The few times I mustered enough courage to imitate them I would rebase myself into a broken branch, a detached HEAD, and lost work. I simply gave up on rebase and decided that git merge was more my speed.

In the last six months I’ve made a concerted effort to read the manual and actually learn to use rebase from first principles. It has become an important part of my git workflow, allowing me to present cleaner pull requests and tell more compelling stories with my commits. The most useful realizations in my journey with rebase have been:

  1. Rebase really means “move” commits.
  2. The word “move” can also mean “redo”.
  3. No one writes clean git logs organically.
  4. Rebase is easier to manage when conflicts are unlikely.

Moving commits

Consider you’ve branched off master to a branch called my-new-work. When you’ve completed your work, you’d like to incorporate the changes that others have made since you diverged from master.

      A---B---C my-new-work
     /
D---E---F---G master

There are a few different ways to do this. The way I would ALWAYS do this prior to learning the semantics of rebase was to merge the changes from commit “G” into my-new-work.

git merge master

To arrive at a merge commit “X”:

      A---B---C---X my-new-work
     /           /
D---E---F-------G master

Now it is time to put in a pull request to incorporate my-new-work into master. However, pull requests lead to feedback, which lead to a new change, which leads to a new commit “H”.

      A---B---C---X---H my-new-work
     /           /
D---E---F-------G master

After I have made “H”, somebody else merged new code to master with commits “I” and “J”. I need those changes to make sure everything still works.

      A---B---C---X---H my-new-work
     /           /
D---E---F-------G---I---J master

So, I have to run git merge master my-new-work to get the code from “I” and “J”. I end up with another merge commit “Y”. Any merge conflicts which happen during this process have to be resolved before you can make commit “Y”.

      A---B---C---X---H---Y my-new-work
     /           /       /
D---E---F-------G---I---J master

Finally, my pull request is approved and I can hit the button to merge my-new-work into master; creating merge commit “Z”.

      A---B---C---X---H---Y my-new-work
     /           /       / \
D---E---F-------G---I---J---Z master

This is fine, in a way it’s simple because it accurately describes exactly what happened at every point in the time line, without any funny business or trickery. This process has worked for me for about 10 years, and I have never really been forced to give it a second thought. The way git rebase differs is that it lets you pretend you didn’t start my-new-work until after commit “J” occurred. It allows you to alter (or move) where it appears that you branched off from master. Let’s go through this set of commits again using git-rebase instead of git merge.

      A---B---C my-new-work
     /
D---E---F---G master

Before putting in the pull request, I run git rebase master.

              A---B---C my-new-work
             /
D---E---F---G master

Our colleague gives us feedback and we make commit “H” with some changes. Meanwhile, commits “I” and “J” have landed in master.

              A---B---C---H my-new-work
             /
D---E---F---G---I---J master

We can use git rebase master again to make “A” slide down the master line. The way this works in reality is that git “replays” commits “A”, “B”, “C”, and “H” one at a time as if you had started from “J”. If at any point a commit conflicts, the git rebase process will pause and exit. This is to allow you to fix the merge conflict manually. After fixing the conflict you can pickup the rebase where it left off by typing git rebase --continue. If you decide that that merge conflict is too gnarly you can decide to call the whole thing off with git rebase --abort.

                      A---B---C---H my-new-work
                     /
D---E---F---G---I---J master

Okay, let’s merge to master, creating the merge commit “Z”.

D---E---F---G---I---J---A---B---C---H---Z master

There you have it, a linear time line of what happened.

Rewriting history with --interactive

The majority of my time with rebase is in --interactive mode. Have you ever gotten done with something and realized you made a simple mistake like a typo? Have you ever then made a commit to fix that typo? Something with the message “typo” for instance. Then you continue reading and you discover yet another typo, and you make another commit with the message “typo”. Imagine that the “X” commits below are those kinds of meaningless commits.

  A---B---X---X---C---X my-new-work
 /
D master

An interactive rebase lets you edit, combine, or even drop commits in order to clarify the commit history of your branch. In order to perform an interactive rebase you need a place from which to start it. The commit you choose is the last commit which you want to keep intact and unchanged. In our case above “B” is a good candidate.

git rebase -i <commit hash/ref of B>

Git will then open up your editor and present you with a plan for how it’s going to apply the commits after your chosen commit. Something like:

pick bbbbbbb some meaningful commit message B
pick deadbee typo
pick fa1afe1 typo
pick ccccccc some meaningful commit message C
pick dabb1ed typo

This editor session is an opportunity to change the rebase plan by changing the word pick to another verb that rebase understands.

verb description
pick keep this commit in the history
edit rebase will stop and let you edit files and commit message
break put this on its new line and it will stop like an edit
reword change the commit message
drop deletes a commit and its changes
squash fold commit into the last picked commit, concatenates messages
fixup like squash but drops commit messages of the folded commits

With this knowledge, here is how I might apply a rebase in our branch scenario above.

reword bbbbbbb more meaningful info in message B
fixup deadbee typo
fixup fa1afe1 typo
pick ccccccc some meaningful commit message C
fixup dabb1ed typo

Here I reworded the message for commit B, and kept my typo fixes but removed the individual commits and messages for them. When I close the editor session git is going to start applying these changes.

  A---B---C my-new-work
 /
D master

Voilá! It appears that I never make mistakes and no one can ever prove otherwise!

Dangerous and magical

Seriously, go read the manual. This can be a dangerous process and you can get yourself into a mess if you’re not aware of what this command can do. The problems that the manual describes boil down to these:

  1. Someone rebased something you depended on, thus pulling the rug from under you.
  2. Files got renamed and you didn’t account for that.
  3. Commits, or ranges of commits got dropped during a rebase.

My generic advice is that you should not rebase branches that you or anyone you are working with has based their work off, and you shouldn’t do anything you don’t actually understand. Git rebase does some tricky stuff, like detecting duplicate commits and auto resolving them, it can also be used to removes entire range of commits.

Consider this example from the manual. I’ve looked at it a lot and I still don’t understand what’s happening, but I know that it’s dangerous as heck and the implication is that commits disappear.

E---F---G---H---I---J  topicA
git rebase --onto topicA~5 topicA~3 topicA

Somehow this removes “F” and “G”

E---H'---I'---J'  topicA

Conclusion

There is even more to discover about this command. In this blog I covered only what I know and use in my day to day work. I’m merely an aspiring time traveler! Thanks for reading this manual review! I hope it has been helpful.

Update: The good people at https://sourcehut.org have put together the pinnacle guide on git-rebase at https://git-rebase.io/.