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:

  1. save the commits that exist on your local branch, but not on the remote, to a temporary location
  2. reset the state of your local branch to match the remote branch
  3. 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>
  1. save commits <upstream>..<branch> to a temporary location
  2. reset <branch> to <newbase>
  3. 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

  1. 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) ↩︎

  2. Unless someone is micro-managing you, or you find that information useful for yourself in some way. ↩︎

  3. Meaning all the commits for a given feature/bugfix are always beside one another, and not intermingled. ↩︎

  4. 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. ↩︎