Search code examples
gitwindows-subsystem-for-linux

How to configure git difftool to work properly on WSL?


My git diff configuration is:

mark@L-R910LPKW:~/.kube$ git config --list | grep diff
diff.tool=bc3
diff.guitool=bc3
difftool.prompt=false
difftool.bc3.path=/mnt/c/Program Files/Beyond Compare 4/BComp.com
mark@L-R910LPKW:~/.kube$

When I run git difftool from WSL I get something like this: enter image description here

Obviously when I run git diff I see the expected output on the console.

Now, according to ps the actual command line is /tools/init /mnt/c/Program Files/Beyond Compare 4/BComp.com /tmp/maHQTa_config config

So it looks like config was successfully translated to a WSL full path "understood" outside of WSL - \\wsl.localhost\Ubuntu-20.04\home\mark.kube\config. However, not so for /tmp/maHQTa_config, even though it maps to \\wsl.localhost\Ubuntu-20.04\tmp\maHQTa_config

How this can be fixed if at all?

EDIT 1

Even though I only showed the git configuration for the diffing, I have merging covered as well:

mark@L-R910LPKW:~/.kube [master ? +1 ~1 -0 !]$ git config --list | grep merge
merge.tool=bc3
merge.guitool=bc3
mergetool.prompt=false
mergetool.keepbackup=false
mergetool.bc3.path=/mnt/c/Program Files/Beyond Compare 4/BComp.com
mark@L-R910LPKW:~/.kube [master ? +1 ~1 -0 !]$

Solution

  • To get around this Windows annoyance, I wound up writing a quickie program to translate the file args. I use the Windows p4merge.exe as my mergetool, but this should work for bc3 or just about anything.

    (I'm running Ubuntu under WSL 2 under Win 10. This code might need some small changes if you're running a different version. I notice that for me, WSL files translate to \Wsl$... but for you it's \Wsl.localhost)

    //  Problem:
    //      It's difficult to launch a Windows GUI program from WSL.
    //  Take p4merge for example.
    //  I have the Windows version of p4merge installed, and I would
    //  like to set that as my git difftool, for comparing versions
    //  of source files.
    //  If I install it as my git difftool
    //      git config --global --add diff.tool p4merge
    //  Git will try to launch it, but it won't work because the
    //  file paths passed as part of the command are WSL paths,
    //  that the Winodws app can't open.
    //  p4merge will fail with an error, e.g.:
    //      '/tmp/vT1r3J_foo.cpp' is (or points to) an invalid file.
    //
    //  Solution:
    //      This helper program can be installed on the WSL Ubuntu
    //  $PATH as p4merge.  Or you can create a symlink in your
    //  $PATH named p4merge that points to this program.
    //
    //  This will scan all of the command line arguemments and translate
    //  any file paths to paths that Windows apps can open, then
    //  launch the target with the modified command line.
    //
    //  You can use this wrapper around any windows GUI program.
    //  It will figure out what target to launch from argv[0].
    //  HOWEVER!  If this wrapper is in the $PATH and the target
    //  is also in the $PATH, they CANNOT HAVE THE SAME NAME!
    //  That would cause an infinite recursion where the wrapper
    //  kept launching itself.
    //  Recommended usage:  Have "program" (without extension) in
    //  your path pointing to the wrapper, and "program.exe" in
    //  your path pointing to the Windows GUI target.
    //  This wrapper will automatically append the ".exe" suffix
    //  to the name in argv[0], and will quit without launching
    //  if argv[0] already contains an ".exe" extension.
    //
    //  Note:  When the linux shell launches a program via a symlink,
    //  argv[0] will have the name of the link, not the link target.
    //
    //  Any argument on the command line that looks like a file or
    //  path will be translated if necessary.   A path that is 
    //  already in Windows format will just be copied.
    //
    //  The WSL file system is not directly part of your computer's
    //  Windows namespace.  The WSL file system is accessed as if
    //  it was a network file.
    //  A path that starts with "/" -- the WSL root directory translates
    //  to \\wsl$\${WSL_DISTRO_NAME}\
    //  A path that starts with "~" -- translates to 
    //  \\wsl$\${WSL_DISTRO_NAME}\${HOME}
    //  And of course paths that start with "." translate to
    //  \\wsl$\${WSL_DISTRO_NAME}\${PWD}
    //  
    //  The Windows file system is accessable from linux as mounted
    //  devices.   A path like /mnt/<drive-letter>/... translates to
    //  <drive-letter>:\...
    //
    //  All forward-slashes are translated to back-slashes.
    //
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
    #include <string.h>
    #include <ctype.h>
    
    #include <string>
    #include <sstream>
    
    //  This really oughta be in libc
    //
    char * stristr (const char * text, const char * match)
    {
        char * strText = strdup (text);
        for (char * p = strText; !!(*p); ++p) {
            *p = tolower (*p);
        }
        char * strMatch = strdup (match);
        for (char * p = strMatch; !!(*p); ++p) {
            *p = tolower (*p);
        }
        char * result = strstr (strText, strMatch);
        if (result) {
            result = (char *) text + (result - strText);
        }
        free (strText);
        free (strMatch);
        return result;
    }
    
    
    int main (int argc, char ** argv) {
        for (int i = 0; i < argc; ++i) {
            fprintf (stdout, "arg[%2d] : %s\n", i, argv[i]);
        }
        fflush (stdout);
    
        struct _local {
            const char * env_WSL_DISTRO_NAME;
            const char * env_HOME;
            const char * env_PWD;
            char * target;
            std::stringstream command;
    
            _local()
            :   env_WSL_DISTRO_NAME (nullptr)
            ,   env_HOME (nullptr)
            ,   env_PWD (nullptr)
            ,   target (nullptr)
            {}
            ~_local() {
                if (target) {
                    auto ptr = target;
                    target = nullptr;
                    free (ptr);
                }
            }
    
            static void get_env (const char * & member, const char * name) {
                member = getenv (name);
                fprintf (stdout, "%s=%s\n", (const char *) name,
                    member ? member : "");
            }
    
            void get_target (const char * me) {
                const char * tmp = basename (me);
                size_t n = strlen (tmp) + 5;
                n = (n + 32) & 0x1f;
                target = (char *) malloc (n);
                memset (target, 0, n);
                --n;
                strncpy (target, tmp, n);
                n -= strlen (target);
                strncat (target, ".exe", n);
                command << target;
            }
    
            void translate (const char * arg) {
                if (! arg)  return;
                char c = *(arg++);
                if ('~' == c) {
                    translate (env_HOME);
                } else if ('.' == c) {
                    translate (env_PWD);
                    ++arg;
                } else if ('/' == c) {
                    if (0 == strncmp ("mnt/", arg, 4)) {
                        c = toupper (*(arg += 4));
                        command << c;
                        command << ":";
                        arg+=2;
                    } else {
                        command << "\\\\\\\\wsl$\\\\";
                        command << env_WSL_DISTRO_NAME;
                    }
                }
                for (c = arg[-1]; !!c; c = *(arg++)) {
                    if ('/' == c) {
                        command << "\\\\";
                    } else {
                        command << c;
                    }
                }
            }
    
            void do_arg (const char * arg) {
                command << ' ';
                translate (arg);
            }
            
        } local;
    
    
        local.get_env (local.env_WSL_DISTRO_NAME, "WSL_DISTRO_NAME");
        local.get_env (local.env_HOME,            "HOME");
        local.get_env (local.env_PWD,             "PWD");
    
        if (stristr (argv[0], ".exe")) {
            fprintf (stderr, "argv[0] = \"%s\"\n", (const char *) argv[0]);
            fputs ("The wsl_wrapper has the same name as the implied target.\n"
                "This would trigger infinite launch recursion!\n", stderr);
            return 1;
        }
        local.get_target (argv[0]);
        for (int i = 1; i < argc; ++i) {
            local.do_arg (argv[i]);
        }
    
        fputs ("\033[1;33m", stdout);   // YELLOW
        fputs (local.command.str().c_str(), stdout);
        fputs ("\033[0m\n", stdout);
        fflush (stdout);
    
        int result = system (local.command.str().c_str());
        if (result == -1) {
            fputs ("\033[1;31m", stdout);   // RED
            perror ("Failed to launch:");
            fputs ("\033[0m\n", stdout);
        }
    
    //  fputc ('\n', stdout);
        return result;
    }