Wednesday, November 23, 2016

Forcing a correct Subversion branch merge despite tree conflicts

Let's say your development team is following the common pattern for maintaining a "clean" releasable trunk in Subversion. All your development is done in "feature branches", which you merge into a "development integration" branch. From there code is merged into a QA branch for further testing, and is eventually merged into trunk and released.

So your bog-standard SVN repository would look something like this:

/trunk
/branches
    /DEVELOPMENT
    /QA
    /feature1
    /feature2
/tags
   /v1.0.0
   /v1.1.1

Now, what frequently happens in this situation is issues are merged from /branches/DEVELOPMENT to /branches/QA out of order. That is, sometimes you have a bug-fix that you want to move rapidly into production, so it gets plucked from /branches/DEVELOPMENT by itself without merging some of the commits that came before it.

This "cherry picking" of code can be problematic in almost any version control system (including Git), but in Subversion it can result in merge conflicts or even the dreaded tree conflict because you are pulling in issues out of order. If some files or folders were refactored or renamed in /branches/DEVELOPMENT, and the "later" bug-fix commit has a dependency on that rename, you have to do some manual conflict resolution to get things to merge into /branches/QA in a working fashion.

Now if you have dozens of developers working on the same application, with features and bug-fixes being committed to /branches/DEVELOPMENT every few minutes, problems escalate rather quickly. You often get into a case where almost every merge from /branches/DEVELOPMENT to /branches/QA generates multiple conflicts of one form or another. The branches have basically "diverged" significantly due to code being merged out-of-order and doing manual conflict resolution.

The only real solution is to periodically "catch up" /branches/QA to /branches/DEVELOPMENT somehow.

Over the years, I have seen developers pull out file comparison tools and "manually" do this, which actually causes a gap in the internal Subversion history as renamed files "appear" in the history instead of being copied from /branches/DEVELOPMENT. This seems to work intially, but generates more merge conflicts in the future, because the relationship between certain files in the two branches is lost, and ultimately doesn't really fix anything.

I've also seen developers delete /branches/QA entirely and re-branch it from /branches/DEVELOPMENT. This approach ultimately forces them to delete and re-create /trunk as a branch of /branches/QA so merging can still take place! Obviously this leaves a big "discontinuity" in the history, and makes many repository maintenance and conversion tools break horribly. Just try using the git-svn integration tool on such a repository.

Here's the correct solution:



  1. Roll back the history of /branches/QA to it's very "beginning" in a working copy. That is, go back to the commit where the branch was initially created. This is easy in TortoiseSVN using "Revert to this revision" from the GUI log viewer. You can also do it from the command line with a reverse merge. Do not commit just yet. This is a fast-rewind and should never generate any conflicts; it also rolls back the svn:mergeinfo to the same state as when the branch was created
  2. Now, merge all commits from /branches/DEVELOPMENT to /branches/QA in that same working copy. You may get warnings about there being local modifications in your working copy before doing your second merge. Don't worry, that is what we want! This second merge updates all the files to be exactly the same as they are in /branches/DEVELOPMENT, as well as updating the critical svn:mergeinfo properties correctly. Assuming /branches/QA was initially created from /branches/DEVELOPMENT, this is a "fast-forward" merge and will never generate conflicts.
  3. Now, before committing, use diff tools to examine the status of svn:mergeinfo as well as the files. You'll see that the resulting "diff" which will be committed is just the changes that make your old-and-busted /branches/QA identical to /branches/DEVELOPMENT, with the additional merge information that "we are now totally up to date with /branches/DEVELOPMENT". You can also do a double-check file compare between this working copy and a fresh checkout of /branches/DEVELOPMENT if you want to be sure (ignore the .svn directory during the file compare).
  4. Commit! You now have a fixed /branches/QA with a single small catch-up delta commit that can also be merged cleanly into /trunk