Ken Kinder's personal website

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.

5 Comments to Subversion merge tracking with svnmerge

  1. Lowell Alleman's Gravatar Lowell Alleman
    May 16, 2006 at 2:36 PM | Permalink

    Very helpful. This is the first “full” svnmerge example that I have found.

    Thanks!

    - Lowell

  2. August 4, 2006 at 10:08 AM | Permalink

    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)

    • Klaus Dalang's Gravatar Klaus Dalang
      December 4, 2006 at 1:46 AM | Permalink

      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?

  3. spazdaq's Gravatar spazdaq
    September 19, 2007 at 6:06 PM | Permalink

    doing research on auto-merging. this was helfpul. thanks.

  4. Clemens W.'s Gravatar Clemens W.
    October 9, 2007 at 10:50 AM | Permalink

    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.

Leave a Reply

You can use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">