Saturday 15 January 2011

Subversion and vimdiff: a little improvement

Vim is commonly used as a diff visualisation tool for Subversion. Often, this is accomplished by putting a line like
diff-cmd = svndiff_helper
into the [helpers] section of your ~/.subversion/config file, where svndiff_helper would contain
gvimdiff -f "$6" "$7"
We pass the sixth and seventh parameters to gvimdiff because that's where svn diff passes us the names of the files to diff. Incidentally, the -f parameter is needed to prevent Vim from forking on startup, since svn would then delete the temporary files too quickly.

So what do the other parameters contain? Let's find out:

$ echo 'for a in "$@"; do echo "$a"; done' >pargs
$ chmod +x pargs
$ ls -A
aa  rand  readme  .svn
$ svn diff -r1 --diff-cmd=./pargs readme 
Index: readme
===================================================================
-u
-L
readme (.../readme) (revision 1)
-L
readme (.../branches/mybranch/readme) (working copy)
.svn/tmp/tempfile.tmp
readme

Interesting – svn gives us a nice textual description of both files in arguments three and five respectively. How about we use those to name the buffers in gvimdiff?

With this script, you can. There are a few tricks to it:
  • I use the --cmd option to Vim to make it run a command on startup.
  • Specifically, I set up autocommands (autocmd, abbreviated to au) to change both buffer names as appropriate when the BufReadPost event is triggered, which happens when the files are read.
  • I use the :file command (abbreviated to just f) to change the buffer name. Unfortunately, Vim doesn't handle tabs in the name very well, so I replace those by spaces. Those in turn I have to escape in order for the :file command to accept them.
  • Vim also gets confused if the buffer name contains a slash, so I replace those by backslashes.
One other issue you may find with the "naïve" wrapper script shown at the start is that it makes Vim not apply syntax highlighting, because the temporary filenames like tempfile.tmp do not have the appropriate extension. My script addresses that too, by temporarily creating a symlink with the right extension to the temp file and passing that to Vim.

11 comments:

  1. Cool, and pretty darn useful. I also use this in a non-graphical environment and took out the -g. And in general I like to do edits back and forth with modifications so dropped the -R.

    One thing is that I get a prompt that requires hitting ENTER before I get to the diff, which is a bit frustrating.

    ReplyDelete
  2. If you drop the -R, you will also have to take out the two --cmd options which make Vim rename the buffers, because Vim uses the buffer names as the file names. So when you write the file, it will write it to the wrong name.

    As for the "Press ENTER or type command to continue" prompt, I've fixed that now.

    ReplyDelete
  3. Great, it works well for me, that's what I want!
    I use '-dR' option for this script, since I don't run it on gvim

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. One issue i have with this script is that i can't make inline edits to the working-copy file. that is, if i have:

    "vimdiff file.java file.java.svn-base"

    your script will load it that as

    "vimdiff -R file.java\ \(working copy\) file.java.svn-base.java"

    so now if i look at the diff and realize "oh, i want to undo a diff between the working copy and what's in svn" i can' really do that, even if i turn off the -readonly, i still have the problem that the file will be saved as file.java\ \(working\ copy\), and then i have to manually diff back into file.java. that's kind of a PITA

    ReplyDelete
  6. How do I get this to automatically save changes back to the working copy?? When I write out, it saves a file with the buffer name to the current working directory, instead of back to the sym-link.

    e.g.
    $ svn diff src/filename.c
    ## edit, quit
    $ ls
    ... src\filename.c (working copy)

    And then I have to vimdiff that file with the working copy... :(

    ReplyDelete
  7. to spleach, or anyone else reading this, if you want to fix the problems change lines 38-42 to the following:

    vim_rc = subprocess.call(['vim', '-df',
    '--cmd', "au BufReadPost " + lf2 + "silent f " + sanitise(lf2),
    '--cmd', "au BufReadPost " + lf1 + "silent f " + sanitise(n1),
    lf2, lf1])


    so i've done 2 things here, 1, i flipped lf1 & lf2 so that the 'working copy' file is on the left split. to me this makes WAY more sense (you can easily flip it back if you like)
    and 2nd, i've changed the sanitise(n2) with sanitise(lf2), the reason here is that i don't want it to use the file's label for display because apparently the label is what vim uses when saving the filename.

    ReplyDelete
  8. What was the fix to remove the hitting Enter Key? I still see that

    ReplyDelete
  9. great post, and the script works like a charm, thanks!

    ReplyDelete
  10. A minor improvement to Vat's suggestion is to make the buffer naming of the left hand side (lhs) buffer conditional on whether it is a "working copy" file or not:

    svn_diff_name1, svn_diff_name2 = sys.argv[3], sys.argv[5]
    file1, file2 = sys.argv[6], sys.argv[7]
    link_to_file1, link_to_file2 = map(lntemp, [file1, file2], [svn_diff_name1, svn_diff_name2])
    lhs_buffer_name = link_to_file2 if "working copy" in svn_diff_name2 else sanitise(svn_diff_name2)
    rhs_buffer_name = sanitise(svn_diff_name1)

    cmd = ['vim', '-df',
    '--cmd', "au BufReadPost " + link_to_file2 + " silent f " + lhs_buffer_name,
    '--cmd', "au BufReadPost " + link_to_file1 + " silent f " + rhs_buffer_name,
    link_to_file2, link_to_file1]

    What this does is makes working copy diffs work the way Vat recommended, while still naming the buffers properly in the case that you are doing a diff of 2 remote files.

    ReplyDelete