JAW Speak

Jonathan Andrew Wolter

How to do 3-way merges with Subversion and Kdiff3

without comments

Reading time: 4 – 7 minutes

I do not endorse branch based development. I prefer trunk based development. Specifically I like what my colleague Paul calls Branch By Abstraction, coined by Stacy Curl, and recently mentioned by Martin Fowler (All one time ThoughtWorkers, and 2 currently).

If you’re stuck with merging though, 3-way merges make it much easier. Doing it with subversion is easy. Instructions are for Linux.

  1. apt-get or yum install kdiff3.
  2. Edit your /etc/subversion/config and fin the line with diff3-cmd, set it to: diff3-cmd=/usr/local/bin/svndiff.sh
  3. Next, create the file /usr/local/bin/svndiff.sh. See below for the script you’ll want to enter in it.

Now when you get a merge conflict you will choose M and merge will open in kdiff3. On the left is the base revision, in the middle is your working copy, and on the right the incoming change. This is a little more to look at, but it is invaluable when dealing with merges. I wouldn’t go back to 2 way diff ever again.

#!/bin/bash
 
# NOTE: I DO NOT SUPPORT BRANCH BASED DEVELOPMENT, but sometimes you're stuck with it.
 
# tim/paul: this is a copy of the file located at <a href="http://www.yolinux.com/TUTORIALS/src/svndiffwrapper.txt">http://www.yolinux.com/TUTORIALS/src/svndiffwrapper.txt</a>
 
# modified to do a non-conflicting merge automatically. see #HERE#
# INSTALLATION:
# 1) Place this file in /usr/local/bin
# 2) Add to /etc/subversion/config a line: diff3-cmd=/usr/local/bin/svndiff.sh
# Then, svn should use this script, which will use kdiff3 to do three way merging.
 
# Return an errorcode of 0 on successful merge, 1 if unresolved conflicts
# remain in the result. Any other errorcode will be treated as fatal.
# Author: Michael Bradley
 
#NOTE: all output must be redirected to stderr with "1&gt;&amp;2" as all stdout output is written to the output file
 
VDIFF3="kdiff3"
DIFF3="diff3"
DIFF="kdiff3"
 
promptUser ()
{
    read answer
    case "${answer}" in
 
        "M" )
        echo "" 1&gt;&amp;2
        echo "Attempting to merge ${baseFileName} with ${DIFF}" 1&gt;&amp;2
        $VDIFF3 $older $mine $theirs  L1 $labelOlder  L2 $labelMine  L3 $labelTheirs -o $output 1&gt;&amp;2
        bLoop=1
        if [ -f $output ]; then
if [ -s $output ]; then
                #output succesfully written
                bLoop=0
            fi
fi
if [ $bLoop = 0 ]; then
cat $output
            rm -f $output
            exit 0
        else
echo "Merge failed, try again" 1&gt;&amp;2
        fi
 
        ;;
 
        "m" )
        echo "" 1&gt;&amp;2
        echo "Attempting to auto-merge ${baseFileName}" 1&gt;&amp;2
        diff3 -L $labelMine -L $labelOlder -L $labelTheirs -Em $mine $older $theirs &gt; $output
        if [ $? = 1 ]; then
            #Can't auto merge
            rm -f $output
            $VDIFF3 $older $mine $theirs  L1 $labelOlder  L2 $labelMine  L3 $labelTheirs -o $output  auto 1&gt;&amp;2
            bLoop=1
            if [ -f $output ]; then
if [ -s $output ]; then
                    #output succesfully written
                    bLoop=0
                fi
fi
if [ $bLoop = 0 ]; then
cat $output
                rm -f $output
                exit 0
            else
echo "Merge failed, try again" 1&gt;&amp;2
            fi
else
            #We can automerge, and we already did it
            cat $output
            rm -f $output
            exit 0
        fi
        ;;
 
        "diff3" | "Diff3" | "DIFF3" )
        echo "" 1&gt;&amp;2
        echo "Diffing..." 1&gt;&amp;2
        $VDIFF3 $older $mine $theirs  L1 $labelOlder  L2 $labelMine  L3 $labelTheirs 1&gt;&amp;2
        ;;
 
        "diff" | "Diff" | "DIFF" )
        echo "" 1&gt;&amp;2
        echo "Diffing..." 1&gt;&amp;2
        $DIFF $mine $theirs -L $labelMine -L $labelTheirs 1&gt;&amp;2
        ;;
 
        "A" | "a" )
        echo "" 1&gt;&amp;2
        echo "Accepting remote version of file..." 1&gt;&amp;2
        cat ${theirs}
        exit 0
        ;;
 
        "I" | "i" )
        echo "" 1&gt;&amp;2
        echo "Keeping local modifications..." 1&gt;&amp;2
        cat ${mine}
        exit 0
        ;;
 
        "R" | "r" )
        echo "" 1&gt;&amp;2
        echo "Reverting to base..." 1&gt;&amp;2
        cat ${older}
        exit 0
        ;;
 
        "D" | "d" )
        echo "" 1&gt;&amp;2
        echo "Runnig diff3..." 1&gt;&amp;2
        diff3 -L $labelMine -L $labelOlder -L $labelTheirs -Em $mine $older $theirs
        #Exit with return vaule of the diff3 (to write out files if necessary)
        exit $?
        ;;
 
        "S" | "s" )
        echo "" 1&gt;&amp;2
        echo "Saving for later..." 1&gt;&amp;2
        cat ${mine}
        #Exit with return vaule of 1 to force writting of files
        exit 1
        ;;
 
        "Fail" | "fail" | "FAIL" )
        echo "" 1&gt;&amp;2
        echo "Failing..." 1&gt;&amp;2
        exit 2
        ;;
 
        "H" | "h" )
        echo "" 1&gt;&amp;2
        echo "USAGE OPTIONS:" 1&gt;&amp;2
        echo " [A]ccept Accept $labelTheirs and throw out local modifications" 1&gt;&amp;2
        echo " [D]efault Use diff3 to merge files (same behavior as vanilla SVN)" 1&gt;&amp;2
        echo " [Fail] Kills the command (not suggested)" 1&gt;&amp;2
        echo " [H]elp Print this message" 1&gt;&amp;2
        echo " [I]gnore Keep your locally modified version as is" 1&gt;&amp;2
        echo " [M]erge Manually merge using ${VDIFF3}" 1&gt;&amp;2
        echo " [m]erge Same as "M" but attempts to automerge if possible" 1&gt;&amp;2
        echo " [R]evert Revert to base version (${labelOlder})" 1&gt;&amp;2
        echo " [S]ave Same as 'I' but writes out rold, rnew, and rmine files to deal with later" 1&gt;&amp;2
        echo " [diff] Type 'diff' to diff versions $labelMine and $labelTheirsthe before making a descision" 1&gt;&amp;2
        echo " [diff3] Type 'diff3' to diff all three versions before making a descision" 1&gt;&amp;2
        echo "" 1&gt;&amp;2
        ;;
 
        * )
        echo "'${answer}' is not an option, try again." 1&gt;&amp;2
        ;;
    esac
}
 
if [ -z $2 ]
then
echo ERROR: This script expects to be called by subversion
    exit 1
fi
 
if [ $2 = "-m" ]
then
    #Setup vars
    labelMine=${4}
    labelOlder=${6}
    labelTheirs=${8}
    mine=${9}
    older=${10}
    theirs=${11}
    output=${9}.svnDiff3TempOutput
    baseFileName=`echo $mine | sed -e "s/.tmp$//"`
 
#HERE#
    diff3 -L $labelMine -L $labelOlder -L $labelTheirs -Em $mine $older $theirs &gt; $output
    if [ $? = 1 ]; then
        #Can't auto merge
        #Prompt user for direction
        while [ 1 ]
        do
echo "" 1&gt;&amp;2
            echo "${baseFileName} requires merging." 1&gt;&amp;2
            echo "" 1&gt;&amp;2
            echo "What would you like to do?" 1&gt;&amp;2
            echo "[M]erge [A]ccept [I]gnore [R]evert [D]efault [H]elp" 1&gt;&amp;2
            promptUser
        done
else
        #We can automerge, and we already did it
        cat $output
        rm -f $output
        exit 0
    fi
else
L="-L" #Argument option for left label
    R="-L" #Argument option for right label
    label1=$3 #Left label
    label2=$5 #Right label
    file1=$6 #Left file
    file2=$7 #Right file
 
    $DIFF $file1 $file2 $L "$label1" $L "$label2" &amp;
    #$DIFF $file1 $file2 &amp;
    #wait for the command to finish
    wait
fi
exit 0

Note: I also posted this to a gist on github: svndiff.sh.

Bookmark and Share

Written by Jonathan

September 17th, 2009 at 8:49 pm

Posted in automation, code

Leave a Reply