I’m not a fan of conflicts, especially source control merge conflicts. Recently for a project, I had to merge some code into our main trunk in svn from a maintenance branch and ran into bunch of problems due to merge conflicts. Since it wasn’t my code that conflicted and there were alot of conflicts over many, long lines, looking at svn’s diff output made me a bit confused and dizzy. The time had come to start using a more graphical merge tool with svn.

I looked around and found several merge tools. Some were shareware, some free. I settled on trying Diffmerge: it was free, available on mac (and windows and linux) and seemed pretty feature rich.

I’ve always found merging a bit confusing, and to make matters worse, it’s not altogether straightforward as to how one gets SVN to use external merge tools. There’s some info in the manual, notably here and here

First, download diffmerge. Once you’ve downloaded the diskimage file, open it and you’ll see a DiffMerge.app directory. Drop that in your /Applications folder.

Next, you’ll need a little wrapper shell script to invoke DiffMerge from svn. I’m using this, and placing it in my home directory: ~/scripts/diffmerge/diffmerge-svnmerge.sh:

#!/bin/bash
DIFFMERGE_PATH=/Applications/DiffMerge/DiffMerge.app
DIFFMERGE_EXEC=${DIFFMERGE_PATH}/Contents/MacOS/DiffMerge
# svn will invoke this with a bunch of arguments.  These are:
# $1 - path to the file that is the original
# $2 - path to the file that's the incoming merge version
# $3 - path to the file that's the latest from trunk (current working copy)
# $4 - path to where svn expects the merged output to be written
${DIFFMERGE_EXEC} --nosplash -m -t1="Incoming Merge Changes"  -t2="Original (merged)" -t3="Current Working Copy changes" -r="$4" "$2" "$1" "$3"

As noted in the comments, svn will call this wrapper script with arguments. These are briefly described in a paragraph in here.

Now, we need to get subversion to call this script at the right time. To do so, we can either set the SVN_MERGE environment variable to point to the path to the script or edit our subversion config file. I’ll choose the latter, so open ~/.subversion/config and add a line under the [helpers] section like this:

merge-tool-cmd = <HOME>/scripts/diffmerge/diffmerge-svnmerge.sh

Make sure your path is correct, and you should probably make it absolute.  Also make sure your script has its executable script set by running:

chmod +x ~/scripts/diffmerge/diffmerge-svnmerge.sh

Now, we’re all set. Let’s run through a bit of a contrived example of how this might be used. I will use example addresses and locations for the svn urls.

Let’s assume we have an svn repository at a url like this : https://svn.example.org/svn/my-example-repo/trunk/test

We will add a new file to the test directory called test.txt with the following contents:

apple pie
orange sherbet
pear sorbet
banana pudding

We’ll add and commit this. Now let’s create a branch to work on this privately with the following command:

svn copy https://svn.example.org/svn/my-example-repo/trunk/test https://svn.example.org/svn/my-example-repo/branches/my_private_test_branch

We will now checkout and work on the branch, editing the branch’s test.txt in the branch so that it looks like this:

peaches and cream   #branch
apple pie
cherry              #branch
orange sherbet
kiwi freeze         #branch
pear sorbet
banana pudding

We’ve clearly marked the line changes with a #branch ‘comment’. Then, we commit this to the branch.

Now let’s say someone else has been changing the trunk version of the file, like so:

peach pie              #trunk
apple pie
orange sherbet
blueberry pancakes     #trunk
pear sorbet
strawberries and cream #trunk
banana pudding

they committed their changes to trunk. For clarity, I’ve marked the changed lines with a #trunk comment

Merge Conflict Time

It’s time to move the changes from the branch to the trunk. We go into our local working copy of the trunk, make sure we have no local changes and we’re up to date with what’s in trunk. From the root of our local copy, we run a merge, resulting in:

mymac:test user$ svn merge https://svn.example.org/svn/my-example-repo/branches/my_private_test_branch
Conflict discovered in 'test.txt'.
Select: (p) postpone, (df) diff-full, (e) edit,
        (mc) mine-conflict, (tc) theirs-conflict,
        (s) show all options:

If we select diff-full, we get this:

Select: (p) postpone, (df) diff-full, (e) edit,
        (mc) mine-conflict, (tc) theirs-conflict,
        (s) show all options: df
--- /var/folders/IL/ILlIoHEjED4wET70+ahtVE+++TM/-Tmp-/tempfile.3.tmp	Wed Oct  7 13:31:33 2009
+++ .svn/tmp/test.txt.tmp	Wed Oct  7 13:31:33 2009
@@ -1,4 +1,16 @@
+<<<<<<< .working +peach pie              #trunk +======= +peaches and cream   #branch +>>>>>>> .merge-right.r10787
 apple pie
+cherry              #branch
 orange sherbet
+<<<<<<< .working +blueberry pancakes     #trunk +======= +kiwi freeze         #branch +>>>>>>> .merge-right.r10787
 pear sorbet
+strawberries and cream #trunk
 banana pudding
Select: (p) postpone, (df) diff-full, (e) edit, (r) resolved,
        (mc) mine-conflict, (tc) theirs-conflict,
        (s) show all options:

Which, even in our simple but contrived example, is a bit confusing and definetely an eyesore. It’s time to use our custom external merge software.

Use option l, which, due to our config file changes, will launch DiffMerge ('l' is not seen as an option, but it is one. Use 's' to see it listed). DiffMerge should launch and you should see something like this:

what was passed to diffmerge were three files:

  • The Original file from which both versions were derived from (ie, the file as it stood when the svn copy was made)
  • The file from the branch (as it is now).  This is the Incoming Merge changes.
  • The working copy, which is basically the latest copy from the trunk

Since svn could not automagically merge it, we’re passing it to DiffMerge. The report mentions that DiffMerge has automatically merged 2 changes, but 2 others could not be done automatically. Those are left for us to merge with the help of the DiffMerge display. Click OK and we get a screen that looks like the following:

The center column is marked ‘Original (merged)’. This is the base/original file from which the branch version and Current working copy (trunk) version are derived. However, since DiffMerge automatically made two merges on it’s own, it’s no longer exactly like the original. You can see that DiffMerge merged in the ‘cherry’ line from the branch copy and the ‘strawberries and cream’ from the working trunk copy.

However, since both the branch and the working copy have incoming changes to the same line, that is the line that we need to merge manually. There are two cases of this (2 conflicts):

For the conflict in the middle of the file (‘kiwi freeze’ vs. ‘blueberry pancakes’), let’s say we want both the changes from the right and the changes from the left to be in the merged (middle) version. So in this case, click on the line ‘kiwi freeze’, then either right click and select ‘insert this’ or press the ‘apply change from right’ button. That gets us this:

We also want the change from the right, so right click on the ‘blueberry pancakes’ line and select ‘Prepend this’.

Selecting that will prepend the ‘blueberry pancakes’ line to what we just moved. Now it looks like this:

That takes care of the first conflict, let’s move on to the other one (the one in the first line). You can see that line 1 in both the right and left have changes, and could thus not be automatically merged. (Notice how DiffMerge tells you what parts of the line match and what parts don’t by the colors, which is very helpful for long lines of configuration options where only one character or word changed). Let’s take the one from the right, so click on ‘peach pie’ and then hit the ‘apply change from right’ button. You’ll get this:

However, let’s change it in the final merge, from ‘peach pie’ to ‘peach cobbler’. To do so, click in the middle column, right after peach pie. you’ll get a text cursor. Delete ‘pie’ and make it ‘cobbler’. You now see this:

Perfect, now the middle column represents exactly how we want this merged.

Just to point out a feature, if we hit the ‘Reference View’ button in the bottom center, we get to see all three files as they were passed into DiffMerge from svn. It looks like this:

That can be a handy view, since it shows the state of the files prior to any automatic or manually done merge editing occured.

Clicking back to the Edit View, we’re now ready to save this (when we save, it saves what is in the middle column). Save it using the save button or File | Save. Quit out of DiffMerge and we’re back at the command prompt with another message like this:

Select: (p) postpone, (df) diff-full, (e) edit, (r) resolved,
        (mc) mine-conflict, (tc) theirs-conflict,
        (s) show all options:

We know that we just manually merged this file, and since we saved it, we should be good to go, select 'r' for resolved and we get:

Select: (p) postpone, (df) diff-full, (e) edit, (r) resolved,
        (mc) mine-conflict, (tc) theirs-conflict,
        (s) show all options: r
--- Merging r10785 through r10787 into '.':
U    test.txt
 U   .
mymac:test user$

The file as it now stands in the working copy is:

mymac:test user$ cat test.txt
peach cobbler              #trunk
apple pie
cherry              #branch
orange sherbet
blueberry pancakes     #trunk
kiwi freeze         #branch
pear sorbet
strawberries and cream #trunk
banana pudding
mymac:test user$

That’s exactly what we wanted, also indicated by the diff:

mymac:test user$ svn diff test.txt
Index: test.txt
===================================================================
--- test.txt	(revision 10787)
+++ test.txt	(working copy)
@@ -1,7 +1,9 @@
-peach pie              #trunk
+peach cobbler              #trunk
 apple pie
+cherry              #branch
 orange sherbet
 blueberry pancakes     #trunk
+kiwi freeze         #branch
 pear sorbet
 strawberries and cream #trunk
 banana pudding
mymac:test user$

We check this in and we’re done.

Advertisements