Recently, Aaron talked about how we keep a published branch. Lately, we have also been having to branch our code more frequently in order to work on big messy changes without disturbing ongoing development in the main trunk. Branching is a great tool to use in those situations, but many find themselves cursing the day they decided to branch when they run into a billion conflicts when trying to merge their changes back to the trunk. Well....it doesn't have to be like that! So to help out those who've had nightmarish merge experiences, or to ease the fears of those who have yet to delve into the mysterious world of subversion branches and merges, I have decided to share my thoughts on proper branching technique.
What is Branching?
Let's start with the basics. What is branching? Why would we need to ever do it? Well...everyone knows the golden rule of using version control systems is to "Check in often". Checking in often gives us more flexibility if we make a mistake and want to go back to a previous version, and it also helps minimize conflicts when several developers are working on the same piece of code. However, as good a practice as checking in often might be, sometimes it's just hard to do, especially if we need to make a big all-or-nothing type change that will take us an extended period of time. In the meantime we can not afford to check in our half-completed change, because it would break things and prevent or make it very difficult for other developers to keep on working on their own stuff. Luckily however, we can simply make a copy of the current trunk repository and then check in our changes to this copy. We can now check in as often as we'd like without disturbing anyone. When we are done with our big change, we can merge it all back at once and everyone is happy. We call this copy a "branch".
How do we Branch?
Branching is easy. Subversion does not really have a built-in concept of a "branch". A branch is simply a copy of the your projects directory in the subversion repository. Where you copy it to is entirely up to you, but a good convention is generally a directory structure like:
The URL for your main repository would look something like:
while the URL for your branch would be:
To create the branch we would simply use the subversion cp command:
svn cp https://svn.server.com/svn/MyProject/Trunk https://svn.server.com/svn/MyProject/Branches/BranchName
now we could simply check out the new branch:
svn co https://svn.server.com/svn/MyProject/Branches/BranchName
and work on it to our heart's content Any changes we committed would go to the branch and not disturb our main trunk code.
Keeping the Branch Up to Date and Merging
Now, here is the tricky part that many people screw up. While the whole point of the branch is to keep changes we make on the branch isolated from trunk, we DO NOT want to keep changes that are going on in trunk isolated from the branch. This is a recipe for disaster...or rather... a very painful merge. You see, the longer we keep the branch completely isolated from trunk, the greater the likelihood that nasty conflicts will happen.
The idea then is to take the changes that are going on in our project's trunk and apply them to our branch. Sure, this won't guarantee that you won't see any conflicts, but it is exponentially easier to resolve conflicts incrementally as you go along than to let them pile up until the end when you've already forgotten what the heck you were actually doing in the code that is now conflicting.
Enter the "merge" command. I think a lot of people misunderstand what this command does because it's name is misleading and makes it seem more mysterious than it actually is. I personally think this command should be renamed "diff and patch", because that is exactly what it does.
So how do we we use the merge command in order to keep the branch synchronized? Well, first of all let's talk about what we are trying to do. Say we were at revision 100 when we did the "svn cp" command that created our branch. Then we worked on our branch a few hours, did several commits to it. Meanwhile, other developers have continued working on trunk and also made several commits. At the end of the day we want to incorporate all changes that have been made to the trunk since we branched. This is easy! We go into the root of our branch directory and execute:
svn merge https://svn.server.com/svn/MyProject/Trunk@100 https://svn.server.com/svn/MyProject/Trunk@HEAD
Actually, there are shorter ways to write this, but lets stick to the long way for the time being and explain what the command actually does. The svn merge command needs only two parameters to indicate what changes we want. In this case we are asking svn to get all the changes that happened in trunk from revision 100 to the HEAD revision and then apply them to our current working copy.
It is important to understand that merge does not actually modify the repository at all, it simply patches our working copy with the result of diffing /Trunk@Head and /Trunk@100. Now you can take a look at the results, resolve any conflicts, make sure everything compiles fine and then commit all changes to the branch. You will probably want to add a commit message such as "merged in changes up to Trunk@####", where #### would be whatever version number your Trunk was at when you ran the merge command. Why do this? Well the next time you merge you'll only want to incorporate changes that happened to trunk since your last merge, so it's a good idea to keep track of what changes you have already merged so you don't try to merge them again.
It's a good idea to keep merging changes from trunk into your branch as often as possible, although probably not as often as you'd run an svn update under normal circumstances. Once a day should be good enough for most people, but if you start to see lots of conflicts you might want to do it more often.
Merging it all back to Trunk
So you finished your big change, everything works, and it's time to merge all your changes back to the trunk so that all your fellow developers can be amazed by how great a job you have done. Well, if you followed my advice and regularly merged changes from trunk into your branch, then this is actually a very painless task. When you think about it, you've already incorporated all the changes the other developers made in trunk into your branch, so your branch already looks like what you want the final result to be. All we have to do now is make our trunk look exactly like our branch and we are done. To do this, we should go into the directory in which we have our trunk checked out and run the following command:
svn merge https://svn.server.com/svn/MyProject/Trunk@HEAD https://svn.server.com/svn/MyProject/Branches/MyBranch@HEAD
WHAT?? I think this command needs a little explanation, but it's not hard if we understand the diff+patch concept. Basically we are asking subversion to diff our branch and the trunk and then patch these changes into Trunk (you did remember to cd to trunk's working dir right?). In other words, we are telling subversion to give us all the changes needed to get us from Trunk@HEAD to MyBranch@HEAD and then apply those changes to Trunk's working dir. If we patch Trunk with the changes necessary to go from Trunk -> MyBranch then what does Trunk end up looking like? That's right..... it should end up being exactly like MyBranch. Now all that is left to do is commit the changes and we should have successfully accomplished a branch/merge.
Other fun uses of Merge
Once you understand that merge is simply diff+patch, you can find other creative uses. How many times have you committed something and then realized you screwed up and wished you could undo that commit? Well merge can be used to quickly undo your commit:
svn merge @HEAD @PREV
svn commit -m "Undoing my last commit"
or you might just undo one file
svn merge MyBadMistake.cs@HEAD MyBadMistake@PREV
The possibilities are endless!
I hope I've managed to demystify the svn merge command somewhat. If you already knew all this... well.. sorry to bore you. But if I saved at least one poor soul out there from the headaches of branching/merging the hard way, then I have accomplished what I set out to do.