Mercurial Diffs with vim on Windows

Using vim with Mercurial for diffs

UPDATE 2017-Jan-17 for newer versions of DirDiff.

I had the devil of a time getting vimdiff and mercurial to play nicely with with each other on Windows for visual diffs. It worked great on posix systems using instructions from the mercurial wiki both to diff single files as well as a multiple files. But on Windows, it would only work for single files, giving me rather strange errors when trying to diff multiple files. So, I spent a bit of time getting it to work.

According to the instructions, the following changes to your mercurial.ini or .hgrc ( depending on platform ) should enable you to use vimdiff for visual diffs with mercurial. Note that you need to install the DirDiff plugin for vim.

[extensions]
hgext.extdiff=

[extdiff]
cmd.vdiff=gvim
opts.vdiff=-f '+next' '+execute "DirDiff" fnameescape(argv(0)) fnameescape(argv(1))'

If you try using the above changes as are on Windows, all you'll end up with is vim opening 7 empty buffers with names corresponding to the parts of opt.vdiff that follow the -f parameter. This happens because of differences between the way bash and cmd.exe treat single quotes and escaping. This part was relatively easy to figure out especially with the use of the --debug option to mercurial like this: hg --debug vdiff, which tells you exactly how the command line is invoked.

So what I tried next was to replace the single quotes with double quotes and escaping the double quotes surrounding DirDiff, like so:

opts.vdiff=-f "+next" "+execute "DirDiff" fnameescape(argv(0)) fnameescape(argv(1))"

That resulted in slightly more informative errors that looked like this:

Illegal file name
Error detected while processing command line:
E119: Not enough parameters for function: <SNR>12_DirDiff

Definitely an improvement since vim was correctly getting and interpreting parameters passed. This indicated I had fixed the shell escaping and any further errors were in vim land.

I spent a bit of time looking at the DirDiff plugin code - wasn't too hard, since it was just one file. But I was spinning my wheels, coz the issue occurred in the invocation of the entry point function defined within the DirDiff plugin. So, the issue was between what was being passed to vim and how vim was interpreting it.

Here's are the approximate steps ( determined empirically ) in how mercurial invokes the visual diff program specified using the extdiff plugin:

  1. make a directory in the user's $TMP ( or %TMP% ) folder - let's call this hgdiff-base
  2. within hgdiff-base create 2 directories - let's call these repo.original and my.changes
  3. copy files that have changed into the my.changes folder and put unchanged, repository versions of those files into the repo.original folder
  4. pass the two folders as arg0 and arg1 to the external diff program ( in our case vimdiff )
  5. as soon as the external diff program exits, remove hgdiff-base and all contents

Since mercurial would cleanup the temp directories that contained the files with differences, I just fudged my tests by using existing files that as passed as parameters to vimdiff exactly as mercurial would have.

In a couple of attempts, I'd narrowed it down to the trailing backslash that was getting passed to vimdiff when multiple files were involved. Trailing slashes are fine when working with posix systems, since they use the forward slash as the path separator and not backslash like Windows does. The directory names are enclosed in double quotes when passed as arguments to vim to account for spaces in folder and file names. So, what vim sees in the case of multiple file diffs is the folder names with a trailing backslash followed immediately by a double quote. Vim ( correctly ) misinterprets the escaped double quote as part of the file or path name of the parameter passed to DirDiff. This is what resulted in the "Illegal file name" error I got earlier.

The simple solution to the problem was to remove the trailing backslash if it was present. The easiest way to do that is to use the vim built-in function substitute. This function is the same one that is invoked within the editor when you do a find-replace. So, I modified the arguments passed to vimdiff as shown below and things were hunky dory.

opts.vdiff=-f "+next" "+execute 'DirDiff' fnameescape(substitute(argv(0), '\\$', '', '')) fnameescape(substitute(argv(1), '\\$', '', ''))"

UPDATE 2017-Jan-17

If you still run into an issue after the above fix and get an error message "There is no diff at the current line!", you are most likely using a newer version of DirDiff which forces UNIX-style locale specification at the command line without checking if it's running on Windows. The fix is to add the following line to your _vimrc or .vimrc file:

let g:DirDiffForceLang=""

Mixing MSYS and ''native'' Windows vim

If you're using MinGW under MSYS and use the Windows native version of gvim.exe supplied by vim.org, then note that when launching gvim from the MSYS shell, $HOME is your MSYS home directory and that the Windows version of gvim will look for plugins in the $HOME/vimfiles/plugin - I spent a bunch of time wondering why gvim couldn't find DirDiff even though I put it in the vimfiles/plugin folder under my Windows home directory. That happens coz the MSYS shell changes your HOME directory to be your home directory in MSYS. If you were to use the vim.exe that ships with MSYS, then it looks for user plugins in $HOME/.vim/plugin where $HOME is naturally your MSYS home folder.

You might just be better off if you don't mix MSYS with native Windows vim, unless you're like me and are willing to bang your head figuring out these kinds of quirks. :-) Enjoy.


Previous: Building Ogre3D 1.8.1 on MinGW

links