git rebase
is one of the most versatile (and underutilized) git
operations. Let’s review the description from the manpages:
Reapply commits on top of another base tip
Pulling a Branch
This simple concept applies in so many varying scenarios, whether you’re
working with a team or on your own. I find one of the most common is
when pulling a branch. By default, git pull
will perform a merge,
which will preserve your local history by intermingling commits from
your local history with those from the remote. A byproduct of this
technique is the creation of a single new commit, known as a “merge
commit”. Now, if you are regularly pulling from the default branch, your
history will become littered with these merge commits, which serve
little purpose save to inform you that a merge occurred1.
If instead you pull using the --rebase
flag, or update your git
configuration using:
$ git config pull.rebase true
When you pull changes, git will do the following:
- save the commits that exist on your local branch, but not on the remote, to a temporary location
- reset the state of your local branch to match the remote branch
- apply the commits from (1) on top of the local branch
This is the general pattern of rebase
: Save, Reset, Apply.
The --onto
flag
The --onto
flag is particularly useful, allowing you to choose what
step (2) will be reset to. The command looks like this:
git rebase --onto <newbase> <upstream> <branch>
- save commits
<upstream>..<branch>
to a temporary location - reset
<branch>
to<newbase>
- apply saved commits
<upstream>..<branch>
Edit history
If you make a change that you realize belongs with the changes from a previous commit (not the most recent one), you can use rebase to reapply a range of commits from the current branch, on top of the current branch:
$ git rebase --interactive HEAD~<count>
This will open a text file in your editor with the last <count>
commits, and will look like this:
pick 68dfdc9 second
pick ded066f third
You can mark a commit with edit
instead of pick
, and the rebase
operation will pause for that commit and allow you to edit it in
whatever way you would like (add new files, new changes, edit message,
etc). When you’re done you can tell git to continue with the rebase
using:
$ git rebase --continue
Gotchas
I’m not going to sit here and tell you that rebase
is unequivocally
better than it’s alternatives. A clean git history sure makes a
compelling case to use it, but it’s not perfect. First up, every time
you run rebase
, you are altering history. Now personally I don’t
see this as much of an issue. As long as you are only altering history
on your local branch, you’re not really hurting anyone2. And
altering history not only lets you avoid merge commits, it enables you
to keep a linear git history3, and refine your history before it
is delivered4.
Far more problematic is conflict resolution. We are all told that you
shouldn’t have long-lived branches, but that isn’t always something in
your control. You may end up needing to get changes from $default
into
your branch multiple times during its lifetime. Due to the nature of
rebase
, that might mean you encounter the same conflict multiple times
as well. Thankfully this problem is mitigated to some extent by
git rerere
. With rerere
, git will remember how you resolved a
conflict, and if the same conflict crops up again, applies it for you
without you having to remember how you resolved it last week. I would
recommend enabling rerere
in your git configuration:
$ git config --global rerere.enabled true
Conclusion
So that’s it. The tip of the iceberg when it comes to learning about
rebase
. Hopefully you found this useful, but if you want to learn more
I’d strongly encourage reading the manpages:
$ man git-rebase
$ man gitrevisions
$ man git-pull
$ man git-rerere
$ man git-merge
-
A merge commit does serve a purpose, but in this case it just isn’t very useful to you (or others depending on the git history) ↩︎
-
Unless someone is micro-managing you, or you find that information useful for yourself in some way. ↩︎
-
Meaning all the commits for a given feature/bugfix are always beside one another, and not intermingled. ↩︎
-
We have all made mistakes. Maybe those three commits really should have been just one? Or maybe a test was failing on one commit, but the fix lives in another. If we want every commit to be an operational unit of work, we should address this before it goes to the default branch. ↩︎