Easier Rebases with Git

At work, we have a policy of "no long-running feature branches." Features branches are expected to have a lifetime of no greater than a week or two, maximum; after which they are squashed into a single commit and fast-forward merged onto master.

However, rebases can be an imposing process: the thought of losing a weeks work (or worse, breaking the build!) makes folks trepidatious. To that end, here's two habits I've picked up over the years to make this workflow less painful.

Reflog has your back.

Git only very rarely deletes information associated with a hash. Branches may come and go, but commits, trees, blobs, and tag objects are as close to immortal as data on your hard drive will get. Git will only clean up those objects periodically, once the number of "unreferenced" objects gets above a certain threshold — this is known as "garbage collection".

What this means for you is that you can rely on a handy tool called the "reflog": the reflog will give you the history of how your branches have changed over time. This includes what hashed object the branch was pointing at, an alias to reach that state, and the action that caused the change. Let's take a look:

c278df2 [email protected]{0}: commit: add bloop
e33d5aa [email protected]{1}: checkout: moving from JIRA-999 to master
e33d5aa [email protected]{2}: rebase finished: returning to refs/heads/JIRA-999
e33d5aa [email protected]{3}: rebase: [JIRA-999] add busey tracker
39bfe97 [email protected]{4}: checkout: moving from JIRA-999 to 39bfe9747a317cae40000000b06b8d02ec0690d9^0
cd9dd16 [email protected]{5}: rebase -i (finish): returning to refs/heads/JIRA-999
cd9dd16 [email protected]{6}: rebase -i (squash): [JIRA-999] add busey tracker
1a8dbcf [email protected]{7}: rebase -i (squash): updating HEAD
6bf8e60 [email protected]{8}: rebase -i (squash): # This is a combination of 5 commits.
1a8dbcf [email protected]{9}: rebase -i (squash): updating HEAD
c3b1ad2 [email protected]{10}: rebase -i (squash): # This is a combination of 4 commits.
1a8dbcf [email protected]{11}: rebase -i (squash): updating HEAD
9ae823f [email protected]{12}: rebase -i (squash): # This is a combination of 3 commits.
1a8dbcf [email protected]{13}: rebase -i (squash): updating HEAD
5cf43ff [email protected]{14}: rebase -i (squash): # This is a combination of 2 commits.
1a8dbcf [email protected]{15}: rebase -i (squash): updating HEAD
3540783 [email protected]{16}: checkout: moving from JIRA-999 to 3540783
bf475c1 [email protected]{17}: checkout: moving from master to JIRA-999

The history goes from most recent to least recent ("reverse chronological"). We can see that most recently, I've committed something nonsensical ([email protected]{0}: commit: add bloop) to master. Prior to that ([email protected]{1}), I switched from JIRA-999 to master. Prior to this, everything from [email protected]{2} to[email protected]{16}is automatically created from one command:git rebase`.

So, if git rebase goes horribly awry, I can go back to [email protected]{17}. Specifically, I can do the following:

$ git checkout JIRA-999
$ git reset --hard bf475c1

... And my JIRA-999 branch will be back in the state it was before the bad rebase. Nothing is ever lost!

Squash your commits before rebasing.

Now that we can breathe easy about not losing work, let's make the actual rebase process less error prone.

One of the biggest problems I run into when rebasing is that I'm essentially replaying my commits against a much newer master, one by one. If one commit doesn't work, then I'm faced with a dilemma: which version of events should I prefer when I'm resolving the conflict? This is made worse by the knowledge that if I've chosen wrong, git will bring up merge conflicts for each subsequent commit that I'm rebasing. Each one is more stressful than the last: even if I finish the rebase, I don't trust that the code coming out of the rebase bears any resemblance to the code that was going in.

I've largely resolved this workflow by doing the following before rebasing onto master:

$ git rebase -i $(git merge-base HEAD master)

This opens up the tradiitonal rebase UI, but instead of rebasing on where master is now, I'm rebasing on top of where my branch diverged from master. This gives me the opportunity to squash my commits into as few atomic changes as are necessary (and in my situation, this usually ends up being a single outgoing commit!)

I simply choose to squash every commit past the first commit — this should always run without complaining -- and when it's done, I rebase the result onto master:

$ git rebase master

And with that, I have a commit that can be cleanly fast-forward merged onto master.

Remember, git very rarely deletes hashed data, and it keeps a breadcrumb trail for you to follow to get back to a good state, so if things go wrong you always have a way out. In addition, the result of your rebase should be only a few commits, so do yourself a favor and coalesce them before changing the base of your branch.