Thursday, July 19, 2012

When a git branch goes bad

Want to merge some good code into the master branch, but stymied by code you don't care about causing a bunch of conflicts?
The other day, some crap got committed to master and pushed, accidentally. It didn't belong. Since it was pushed to origin, we don't want to change history*. When it was time to do a git-flow release, a bunch of conflicts stymied the merge from release branch into master. I wanted to say, "Merge this branch into master, but take all the code from the branch; don't worry about what's currently in master."
There's a merge strategy for merging in a branch and ignoring the changes into the branch: the "ours" merge strategy. But there's no "theirs" merge strategy for choosing all the code from the branch. Here is one way to accomplish this:
  1. Start the merge
if using git-flow to do a release: git flow release finish versionName
OR
general case of merging something into master (or whatever branch you like; master is an example):
git checkout master
git merge branchWithGoodCode
This leaves the merge open, with conflicts. git status shows the files successfully merged (these are in the index) and the files with conflicts (not yet in the index). While the merge is in progress, git is in a special state, something like the below diagram. The objective is to get all the right files into the index and then commit, which will complete the merge.
  1. Get all the code from the branch 
git checkout MERGE_HEAD -- .
Here, MERGE_HEAD means "the tip of the branch we're trying to merge in." MERGE_HEAD is a ref (pointer to a commit) that exists while a merge is in process. This command pulls the files from there into the working directory and index.
The "." at the end is important: when a path is provided as the last argument to git checkout, then git updates the files without changing your current branch. git checkout with no path will switch to that branch. (The -- is optional, but it makes that . harder to miss.)
  1. Commit to finish the merge
git commit -m "Merge branchWithGoodCode, taking all files from branchWithGoodCode"
Hurray, now the tree looks like this:

* if the commits that I don't care about existed only locally, not on origin, then I could wipe those commits from the history entirely. 
git checkout master
git reset --hard goodBranch
the reset says "take my current branch and move it to point at the same commit as goodBranch." (Keep in mind that a branch is nothing but a label, a pointer to a commit.) The "--hard" says "and while you're at it, replace everything in my working directory with the goodBranch code." The commits that were only on master are gone from the tree, and eventually forgotten.

2 comments:

  1. Could you also just merge master into your branch with an 'ours' strategy, and then turn around and merge the branch back into master?

    ReplyDelete
  2. I think it'd be cleaner to have a commit that reverts those "Crap we don't care about changes" in master before merging the good branch. Then it is much more clear what happened. You can either revert them individually or use the same git checkout/commit trick you talk about here.

    ReplyDelete