Date

A couple of coworkers (Sean O'Connor, Dan Frank) bailed me out of a sticky git scenario. I'd been working on a branch for way too long and somebody else made wildly incompatible changes to a couple of the more important files I'd changed. When this happens, I'm prepared to accept responsibility for digging out of the mess I got myself into by sitting on the branch too long.

But our standardized Git workflow is heavily based on rebasing rather than merging.

I'd never had to resolve conflicts this bad during a rebase before. I've learned the hard way that whenever merging or rebasing gets difficult, it's really important to save a copy of your original branch in case you screw things up and need to start over with a different approach. Or first try out your plan on a scratch copy of the branch. In my plan, I'll also need my pre-rebase changes available so I can figure out how to recreate them against the upstream changes. So:

  git rebase --abort
  git checkout -b experimental-copy-for-rebasing

At first I tried the usual approach: git rebase, hand-edit each conflict, and then git rebase --continue. But since I'd made many edits to the files in question, and the upstream changes were pretty extensive, that just caused git rebase to bail out when applying my very next commit - which meant I'd have to hand-edit conflicts for many more commits, and almost certainly get something very wrong along the way.

I decided that the best solution would be to just throw away my changes to the conflicting files and re-do them by hand after finishing the rebase, so I'd at least have a sane starting point. But how?

It turns out there's an easy way to do this. Each time git rebase bails out on a conflict, just do this:

  git checkout file-that-conflicts --ours
  git add file-that-conflicts
  git rebase --continue

This throws away my changes to that file and uses the upstream version unchanged. The syntax is kind of surprising, because "--ours" here means not our local changes, but the changes we're rebasing on top of - exactly the opposite of what I'd expect. But it works.

One wrinkle is that for commits where I only touched the conflicting file, git rebase won't allow you to --continue after doing the checkout --ours, because rebase can't do anything with a commit that includes no changes. So when git rebase --continue complains with "No changes - did you forget to use 'git add'?", just do git rebase --skip to leave out that commit entirely.

Whew.

This solution isn't perfect - for one thing, it means that my working history has some broken partially-undone commits, where some files had changes that depended on the changes I'd thrown away and re-done later. But I wasn't worried about that because our workflow also involves using rebase to later squash many commits into one when we're happy with a set of changes.

It's also a bit laborious - I have to check each conflict to be sure it's something I want to discard, edit it if not, and run those commands if so -- tedious for 20 conflicts, but much better than trying to hand-edit 20 times.

(update: I have since changed to a job where rebase is not the norm. It's a lot more sane.)


Comments

comments powered by Disqus