Subversion diff with vimdiff improved

Almost three years ago, I published a bash wrapper function for the svn command on this blog. This shell function allows to use external tools when calling svn diff, for example colordiff, Apple’s FileMerge on OS X or vimdiff.

Since then, I improved the file by using less forks with type -fp, added quotes where I encountered filenames with spaces, etc. However, it is basically still the same as back then. Here, I left out the parts about the other diff functions so you get an idea how this works without requiring you to read the other blog post:

function svn() {
    local svn=$(type -fp svn)
    case "$1" in
        diff-vim)
            shift;
            $svn diff --diff-cmd $HOME/libexec/svndiff -x vimdiff "$@"
            ;;
        *)
            $svn "$@"
            ;;
    esac
}

This approach has one drawback for practical use of the svn diff-vim command: sometimes, svn does not use the original file name, but creates a temporary file first. This hinders editing of such a diff using vimdiff, as edits can not be saved easily to the actual specified file.

[Side note: this happens as soon as a file has a svn:keywords property and keywords such as $Id$ were expanded. In this case, the diff does not use the original file path but a temporary file to avoid changes introduced by keyword expansion. (details)]

This behavior became annoying for me, so I improved the original approach and present the new version below. This time, I am using svn cat to manually retrieve the older version of the file to a temporary location and then call vimdiff with the file specified and the older version. This became more complex as I also wanted to support the -r syntax to specify the revision to compare against:

function svn() {
    local svn=$(type -fp svn)
    case "$1" in
        diff-vim)
            shift;
 
            local file=""
            local rev="-rBASE"
 
            while [ $# -gt 0 ]; do
                case "$1" in
                    --)
                        break;
                        ;;
                    -r*)
                        rev="$1"
                        if [ "$1" == "-r" ]; then
                            shift
                            rev="-r$1"
                        fi
                        ;;
                    --revision)
                        shift
                        rev="-r$1"
                        ;;
                    -*)
                        echo "svn: invalid option: $1"
                        return 1
                        ;;
                    *)
                        if [ -n "$file" ]; then
                            echo "svn: diff-vim works with one single filename only" >&2
                            return 1
                        fi
                        file="$1"
                        ;;
                esac
                shift;
            done;
 
            local tmp=$(mktemp -t svn-diff-vim || return 1).${file##*.}
            $svn cat $rev "$file" > $tmp || return 1
            chmod a-w $tmp || return 1
            vimdiff "$file" $tmp || return 1
            rm -f $tmp || return 1
            ;;
        *)
            $svn "$@"
            ;;
    esac
}

A known drawback of this approach is that you can no longer use the -c option to view changes introduced in a specific revision, or specify a peg revision with the file@rev syntax. For me, these aren’t showstoppers, as the most common use case for me is to revert only some parts of a file. Using the presented function above, this is quite easy with svn diff-vim and the vim commands moving changes from one file to the other: do for diff-obtain and dp for diff-put.

You can also get the full file with all implemented diff commands as a download. This file is meant to be sourced from your .bashrc. You can share and use this script as you will, it is hereby placed into Public Domain.

As always, I am happy to hear about your experiences with this script, suggestions or better solutions in the comments below!

5 thoughts on “Subversion diff with vimdiff improved

  1. Victor Hooi

    Hi,

    I’m trying to use this – I’ve source it in my console, and I then run “svn diff-vim” in a Subversion directory. I then use “:qa” to quit out of vimdiff.

    However, the second time I try to run it, I get:

    mktemp: cannot create temp file /tmp/svn-diff-vim: File exists
    -bash: .: Is a directory

    I can remove that temporary file, and use it again – however, something tells me I’m Holding It Wrong…lol.

    That, or it’s not cleaning up it’s tmp files? I put an echo line before the rm tmp line, doesn’t seem to get there, and at the command-line I see:

    svn diff-vim
    svn: Try ‘svn help’ for more info
    svn: Not enough arguments provided
    2 files to edit

    any ideas?

    Cheers,
    Victor

  2. Rainer Müller Post author

    Hm, that sounds almost correct what you did. You are supposed to pass a single filename to the command – the file you want to diff. There is no good way to handle multiple files in vimdiff, so this function works on a single file only.

    I just noticed due to the way the temporary filename is substituted, there is no way to diff a file in a subdirectory. The argument has to be in the current directory.

    Use the following command for debugging:

    set -x; svn diff-vim filename; set +x

    This will print each line evaluated in the function.

  3. Maik

    Thanks for this great script. Exactly what I needed.
    Maybe one day I will enhance it to support dir-diff as it works with git + meld out of the box.

  4. Maik

    I had the the same problem as Victor Hooi. The reason is, because possibly on some systems the mktemp does not work as it should. Normally it should build a standard template file name if no template is given. However on my system I have to change “mktemp -t svn-diff-vim” to “mktemp -t svn-diff-vim.XXXXXXXX”. Then it works.

  5. Rainer Müller Post author

    Ah, I see. The function expects you have the standard mktemp from OS X. I guess you manually installed GNU mktemp from GNU coreutils and put it into your PATH first. I can confirm that the GNU mktemp does not understand the syntax I used, but the change you mentioned works around that limitation.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.