Subversion merge tracking with svnmerge
Branching and merging are rough in Subversion. Better than they were in CVS, but still rough. You can branch easily enough, but when it comes time to merge, you currently have to tell it exactly what to merge. In other words, you can’t say, “bring everything over from trunk”, you have to say “bring modifications 3, 4, 5 from trunk”. That means you have to know that you’ve already applied revisions 1 and 2 — subversion doesn’t keep any record of that for you.
As Version Control with Subversion puts it,
Ideally, your version control system should prevent the double-application of changes to a branch. It should automatically remember which changes a branch has already received, and be able to list them for you. It should use this information to help automate merges as much as possible.
Unfortunately, Subversion is not such a system. Like CVS, Subversion does not yet record any information about merge operations. When you commit local modifications, the repository has no idea whether those changes came from running svn merge, or from just hand-editing the files.
So while subversion nicely moves over changes for you, it does not know what changes you need to move over. There is a third party tool, which is included in Subversion’s contrib directory, that does keep track of this. It’s called svnmerge. Why hasn’t subversion integrated this feature? I hope the reason that they’re working on a way of doing it that is just so cool that they don’t want to include something that’ll go away anyway.
At any rate, I wanted to find out just how well svnmerge works. So, I created a test repository with a very nice, modular HelloWorld application in it. Then, I branched it, made some changes to trunk and the branch, merged them, and played a little more with svnmerge. Here’s how it went.
First, I made a HelloWorld application with four files in it, in a standard subversion layout.
|-- branches
|-- tags
`-- trunk
|-- HelloModule.py
|-- PunctuationModule.py
|-- WorldModule.py
`-- helloworld
To make the project highly extensible, I’ve separated out the logic of saying Hello World into several files. The helloworld module is executable, bringing the components together:
#!/usr/bin/env python import HelloModule import WorldModule import PunctuationModule print '%s%s%s%s' % (HelloModule.getWord(), PunctuationModule.getSeparator(), WorldModule.getWord(), PunctuationModule.getEnding())
Let’s say I want to create a branch of my Hello World project for internationalization.
kkinder@lennon:~/HelloProject$ svn cp trunk branches/intl A branches/intl kkinder@lennon:~/HelloProject$ svn commit -m "Branching trunk to internationalize" Adding branches/intl Committed revision 2.
Now, I tell svnmerge about the branch. You can tell svnmerge about the branches after the fact, but if you do it up front, you won’t need to tell it from what revision numbers the branches came.
kkinder@lennon:~/HelloProject/trunk$ svnmerge init ../branches/intl && svn commit -F svnmerge-commit-message.txt && rm svnmerge-commit-message.txt property 'svnmerge-integrated' set on '.' Sending trunk Committed revision 3. kkinder@lennon:~/HelloProject/trunk$ cd ../branches/intl kkinder@lennon:~/HelloProject/branches/intl$ svnmerge init ../../trunk && svn commit -F svnmerge-commit-message.txt && rm svnmerge-commit-message.txt property 'svnmerge-integrated' set on '.' Sending intl Committed revision 4.
Now, if we make a change to trunk, we can see it. In the trunk, I’ll add a docstring to the helloworld application, then commit it.
kkinder@lennon:~/HelloProject/trunk$ svn diff Index: helloworld =================================================================== --- helloworld (revision 1) +++ helloworld (working copy) @@ -1,4 +1,7 @@ #!/usr/bin/env python +""" +Prints hello world just for you. +""" import HelloModule import WorldModule import PunctuationModule kkinder@lennon:~/HelloProject/trunk$ svn commit -m "Add docstring" Sending trunk/helloworld Transmitting file data . Committed revision 5.
Now, on the branch, I can see that this patch is available.
kkinder@lennon:~/HelloProject/branches/intl$ svnmerge avail -l ------------------------------------------------------------------------ r5 | kkinder | 2006-05-09 12:12:19 -0600 (Tue, 09 May 2006) | 1 line Changed paths: M /trunk/helloworld Add docstring
Now, I can tell it to do the merge, without having to tell it what revision numbers I need.
kkinder@lennon:~/HelloProject/branches/intl$ svnmerge merge U helloworld property 'svnmerge-integrated' set on '.' kkinder@lennon:~/HelloProject/branches/intl$ svn commit -F svnmerge-commit-message.txt && rm svnmerge-commit-message.txt Sending intl Sending intl/helloworld Transmitting file data . Committed revision 6.
That was easy. Let’s make some changes to the branch and merge those back into the trunk:
kkinder@lennon:~/HelloProject/branches/intl$ svn diff Index: HelloModule.py =================================================================== --- HelloModule.py (revision 2) +++ HelloModule.py (working copy) @@ -1,2 +1,6 @@ +import gettext +_ = gettext.gettext + def getWord(): - return 'Hello' + return _('Hello') + Index: WorldModule.py =================================================================== --- WorldModule.py (revision 2) +++ WorldModule.py (working copy) @@ -1,2 +1,5 @@ +import gettext +_ = gettext.gettext + def getWord(): - return 'World' + return _('World') Index: PunctuationModule.py =================================================================== --- PunctuationModule.py (revision 2) +++ PunctuationModule.py (working copy) @@ -1,5 +1,8 @@ +import gettext +_ = gettext.gettext + def getSeparator(): - return ', ' + return _(', ') def getEnding(): - return '!' + return _('!') kkinder@lennon:~/HelloProject/branches/intl$ svn commit -m "Use gettext for strings" Sending intl/HelloModule.py Sending intl/PunctuationModule.py Sending intl/WorldModule.py Transmitting file data ... Committed revision 7.
Keep in mind, that’s in the branch only now. So now we want to merge the internationalization support back into trunk.
kkinder@lennon:~/HelloProject/trunk$ svnmerge avail -bl ------------------------------------------------------------------------ r7 | kkinder | 2006-05-09 12:15:47 -0600 (Tue, 09 May 2006) | 1 line Changed paths: M /branches/intl/HelloModule.py M /branches/intl/PunctuationModule.py M /branches/intl/WorldModule.py Use gettext for strings kkinder@lennon:~/HelloProject/trunk$ svnmerge merge -b property 'svnmerge-integrated' set on '.' U HelloModule.py U WorldModule.py U PunctuationModule.py property 'svnmerge-integrated' set on '.' kkinder@lennon:~/HelloProject/trunk$ svn update At revision 7. kkinder@lennon:~/HelloProject/trunk$ svn commit -F svnmerge-commit-message.txt && rm svnmerge-commit-message.txt Sending trunk Sending trunk/HelloModule.py Sending trunk/PunctuationModule.py Sending trunk/WorldModule.py Transmitting file data ... Committed revision 8.
And so easily enough, both the trunk and the branch have been painlessly merged, without having to keep record of any revision numbers.
Very helpful. This is the first “full” svnmerge example that I have found.
Thanks!
- Lowell
[Reply]
How very useful!
I wrapped this very helpful example into a simple shell script (as I am too lazy to type several lines of command):
#!/bin/sh
case $# in
1 ) ;; # branch name
* ) print “usage: $0 new-branch-name”; exit 1 ;;
esac
set -e
svn copy trunk “branches/$1″
svn commit “branches/$1″ -m “Branching trunk to $1.”
(cd trunk;
svnmerge.py init “../branches/$1″
&& svn commit -F svnmerge-commit-message.txt
&& rm svnmerge-commit-message.txt)
(cd “branches/$1″;
svnmerge.py init ../../trunk
&& svn commit -F svnmerge-commit-message.txt
&& rm svnmerge-commit-message.txt)
[Reply]
This is indeed a very good full example but what if I have several branches that I need to merge back into the trunk? Do I have to re initialize the trunk for each branch that I need to merge?
[Reply]
doing research on auto-merging. this was helfpul. thanks.
[Reply]
Nice example, however note that you should use the -b (bidirectional) flag for avail and merge in both directions (not just in the intl -> trunk direction as you have it). It works in your example because you only merge once, but as soon as you merge back and forth you’ll need to suppress “reflected” changes.
[Reply]