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:
- Rebase really means “move” commits.
- The word “move” can also mean “redo”.
- No one writes clean git logs organically.
- 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:
- Someone rebased something you depended on, thus pulling the rug from under you.
- Files got renamed and you didn’t account for that.
- 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/.