A blog by Gary Bernhardt, Creator & Destroyer of Software

Rebase Is Safe

09 Dec 2010

A falsehood about Git is spreading: that git rebase isn't safe. Not the kind of unsafe where you rebase pushed changes and everyone gets a nasty merge bubble. That's a real, well-known danger, and it's accepted that you just don't do it.

This falsehood is that git rebase inherently destablizes your history, potentially introducing changesets that don't compile or don't pass the tests, and that this is a serious problem. The main consequence is that git bisect stops working: if you have revisions with broken tests, your bisect will skip them if you're very careful, or give you false positives if you're not.

That certainly sounds bad. But if the problem is that you don't know whether the tests pass for the newly rebased commits, why not run the tests? Like this, for example:

(set -e;
  git rev-list --reverse origin/master..master |
    while read rev; do
      echo "Checking out: $(git log --oneline -1 $rev)";
      git checkout -q $rev;
      python runtests.py;
    done)

This isn't hard—it's just a while loop at the shell. It checks out every revision between origin/master and master, running the tests for each revision. Running it on a couple revisions of Expecter Gadget gives this output:

Checking out: 4f56581 Split tests into many files
....................
-------------------------------------------------
Ran 20 tests in 0.019s

OK
Checking out: 44de2c2 pyflakes
....................
-------------------------------------------------
Ran 20 tests in 0.019s

OK

This only took 1.1 seconds. If it's slow, that's because your unit tests are slow (not Git's fault). Of course, test slowness will also slow down your bisects, your CI, your inner-loop development workflow, and lots of other stuff. Rebase isn't the bottleneck there.

The command will stop whenever a test fails—that's what the set -e at the top is for. So, if any revision doesn't pass the tests, you'll see the test output, the command will stop, and you'll be left with that revision checked out. You'll probably want to git checkout master and do an interactive rebase to fix that revision. This rarely happens to me in practice; the rebases almost always work without modification, but checking is still important because you really don't want to break the history.

You might worry that the checkouts will smash stuff in the working copy or otherwise cause chaos. Again, set -e saves us. If the working copy is dirty, the first git checkout will fail and the subshell will exit:

Checking out: 4f56581 Split tests into many files
error: You have local changes [...]; cannot switch branches.

I've used this on multiple projects with great success, both by myself and on small teams of around six people. It originally came from a similar command for running tests over all revisions in a Mercurial patch queue, which is just another way to rebase commits, lest the Mercurial users think that this issue is Git-specific.

In the past, I haven't even bothered turning the command into a script. I have a huge Zsh history size (100,000 commands) and would just hit ^R to do a reverse history search for "rev-list". However, I've just scriptified the command in my dotfiles repository, so you can use it easily in your own projects.

The lesson here is that you need to engage deeply with your tools: understand Git, but also understand Unix. Learn how to use them well (for Unix examples, see my screencasts "Python to Shell to Ruby" and "A Raw View into My Unix Hackery"), and learn the fundamental models of both (for a Unix example, see "The Tar Pipe"), and especially learn how to use them together. Don't learn only one, or learn them both halfway, and then blame the tools when you have problems. Programming is hard, but let's not go shopping.

(If you enjoyed this, you may also like "Why I Switched to Git From Mercurial".)