extdiff: support tools that can be run simultaneously
authorLudovic Chabant <ludovic@chabant.com>
Sat, 02 Feb 2019 21:58:49 -0800
changeset 41584 a4cd77a425a3
parent 41582 7b2580e0dbbd
child 41585 549af2fa089f
extdiff: support tools that can be run simultaneously
hgext/extdiff.py
tests/test-extdiff.t
tests/test-extension.t
--- a/hgext/extdiff.py	Tue Feb 05 11:17:11 2019 -0800
+++ b/hgext/extdiff.py	Sat Feb 02 21:58:49 2019 -0800
@@ -59,6 +59,22 @@
   [diff-tools]
   kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
 
+If a program has a graphical interface, it might be interesting to tell
+Mercurial about it. It will prevent the program from being mistakenly
+used in a terminal-only environment (such as an SSH terminal session),
+and will make :hg:`extdiff --per-file` open multiple file diffs at once
+instead of one by one (if you still want to open file diffs one by one,
+you can use the --confirm option).
+
+Declaring that a tool has a graphical interface can be done with the
+``gui`` flag next to where ``diffargs`` are specified:
+
+::
+
+  [diff-tools]
+  kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
+  kdiff3.gui = true
+
 You can use -I/-X and list of file or directory names like normal
 :hg:`diff` command. The extdiff extension makes snapshots of only
 needed files, so running the external diff program will actually be
@@ -71,6 +87,7 @@
 import re
 import shutil
 import stat
+import subprocess
 
 from mercurial.i18n import _
 from mercurial.node import (
@@ -105,11 +122,19 @@
     generic=True,
 )
 
+configitem('extdiff', br'gui\..*',
+    generic=True,
+)
+
 configitem('diff-tools', br'.*\.diffargs$',
     default=None,
     generic=True,
 )
 
+configitem('diff-tools', br'.*\.gui$',
+    generic=True,
+)
+
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
@@ -176,13 +201,26 @@
         cmdline += ' $parent1 $child'
     return re.sub(regex, quote, cmdline)
 
-def _runperfilediff(cmdline, repo_root, ui, do3way, confirm,
+def _systembackground(cmd, environ=None, cwd=None):
+    ''' like 'procutil.system', but returns the Popen object directly
+        so we don't have to wait on it.
+    '''
+    cmd = procutil.quotecommand(cmd)
+    env = procutil.shellenviron(environ)
+    proc = subprocess.Popen(procutil.tonativestr(cmd),
+                            shell=True, close_fds=procutil.closefds,
+                            env=procutil.tonativeenv(env),
+                            cwd=pycompat.rapply(procutil.tonativestr, cwd))
+    return proc
+
+def _runperfilediff(cmdline, repo_root, ui, guitool, do3way, confirm,
                     commonfiles, tmproot, dir1a, dir1b,
                     dir2root, dir2,
                     rev1a, rev1b, rev2):
     # Note that we need to sort the list of files because it was
     # built in an "unstable" way and it's annoying to get files in a
     # random order, especially when "confirm" mode is enabled.
+    waitprocs = []
     totalfiles = len(commonfiles)
     for idx, commonfile in enumerate(sorted(commonfiles)):
         path1a = os.path.join(tmproot, dir1a, commonfile)
@@ -228,14 +266,32 @@
             parent1=path1a, plabel1=label1a,
             parent2=path1b, plabel2=label1b,
             child=path2, clabel=label2)
-        ui.debug('running %r in %s\n' % (pycompat.bytestr(curcmdline),
-                                         tmproot))
 
-        # Run the comparison program and wait for it to exit
-        # before we show the next file.
-        ui.system(curcmdline, cwd=tmproot, blockedtag='extdiff')
+        if confirm or not guitool:
+            # Run the comparison program and wait for it to exit
+            # before we show the next file.
+            # This is because either we need to wait for confirmation
+            # from the user between each invocation, or because, as far
+            # as we know, the tool doesn't have a GUI, in which case
+            # we can't run multiple CLI programs at the same time.
+            ui.debug('running %r in %s\n' %
+                     (pycompat.bytestr(curcmdline), tmproot))
+            ui.system(curcmdline, cwd=tmproot, blockedtag='extdiff')
+        else:
+            # Run the comparison program but don't wait, as we're
+            # going to rapid-fire each file diff and then wait on
+            # the whole group.
+            ui.debug('running %r in %s (backgrounded)\n' %
+                     (pycompat.bytestr(curcmdline), tmproot))
+            proc = _systembackground(curcmdline, cwd=tmproot)
+            waitprocs.append(proc)
 
-def dodiff(ui, repo, cmdline, pats, opts):
+    if waitprocs:
+        with ui.timeblockedsection('extdiff'):
+            for proc in waitprocs:
+                proc.wait()
+
+def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
     '''Do the actual diff:
 
     - copy to a temp structure if diffing 2 internal revisions
@@ -382,7 +438,8 @@
         else:
             # Run the external tool once for each pair of files
             _runperfilediff(
-                cmdline, repo.root, ui, do3way=do3way, confirm=confirm,
+                cmdline, repo.root, ui, guitool=guitool,
+                do3way=do3way, confirm=confirm,
                 commonfiles=common, tmproot=tmproot, dir1a=dir1a, dir1b=dir1b,
                 dir2root=dir2root, dir2=dir2,
                 rev1a=rev1a, rev1b=rev1b, rev2=rev2)
@@ -446,7 +503,13 @@
     to its parent.
 
     The --per-file option runs the external program repeatedly on each
-    file to diff, instead of once on two directories.
+    file to diff, instead of once on two directories. By default,
+    this happens one by one, where the next file diff is open in the
+    external program only once the previous external program (for the
+    previous file diff) has exited. If the external program has a
+    graphical interface, it can open all the file diffs at once instead
+    of one by one. See :hg:`help -e extdiff` for information about how
+    to tell Mercurial that a given program has a graphical interface.
 
     The --confirm option will prompt the user before each invocation of
     the external program. It is ignored if --per-file isn't specified.
@@ -475,20 +538,22 @@
     to its parent.
     """
 
-    def __init__(self, path, cmdline):
+    def __init__(self, path, cmdline, isgui):
         # We can't pass non-ASCII through docstrings (and path is
         # in an unknown encoding anyway), but avoid double separators on
         # Windows
         docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
         self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
         self._cmdline = cmdline
+        self._isgui = isgui
 
     def __call__(self, ui, repo, *pats, **opts):
         opts = pycompat.byteskwargs(opts)
         options = ' '.join(map(procutil.shellquote, opts['option']))
         if options:
             options = ' ' + options
-        return dodiff(ui, repo, self._cmdline + options, pats, opts)
+        return dodiff(ui, repo, self._cmdline + options, pats, opts,
+                      guitool=self._isgui)
 
 def uisetup(ui):
     for cmd, path in ui.configitems('extdiff'):
@@ -503,7 +568,8 @@
             cmdline = procutil.shellquote(path)
             if diffopts:
                 cmdline += ' ' + diffopts
-        elif cmd.startswith('opts.'):
+            isgui = ui.configbool('extdiff', 'gui.' + cmd)
+        elif cmd.startswith('opts.') or cmd.startswith('gui.'):
             continue
         else:
             if path:
@@ -517,15 +583,20 @@
                     path = filemerge.findexternaltool(ui, cmd) or cmd
                 cmdline = procutil.shellquote(path)
                 diffopts = False
+            isgui = ui.configbool('extdiff', 'gui.' + cmd)
         # look for diff arguments in [diff-tools] then [merge-tools]
         if not diffopts:
-            args = ui.config('diff-tools', cmd+'.diffargs') or \
-                   ui.config('merge-tools', cmd+'.diffargs')
-            if args:
-                cmdline += ' ' + args
+            key = cmd + '.diffargs'
+            for section in ('diff-tools', 'merge-tools'):
+                args = ui.config(section, key)
+                if args:
+                    cmdline += ' ' + args
+                    if isgui is None:
+                        isgui = ui.configbool(section, cmd + '.gui') or False
+                    break
         command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
                 helpcategory=command.CATEGORY_FILE_CONTENTS,
-                inferrepo=True)(savedcmd(path, cmdline))
+                inferrepo=True)(savedcmd(path, cmdline, isgui))
 
 # tell hggettext to extract docstrings from these functions:
 i18nfunctions = [savedcmd]
--- a/tests/test-extdiff.t	Tue Feb 05 11:17:11 2019 -0800
+++ b/tests/test-extdiff.t	Sat Feb 02 21:58:49 2019 -0800
@@ -22,6 +22,10 @@
   > opts.falabala = diffing
   > cmd.edspace = echo
   > opts.edspace = "name  <user@example.com>"
+  > alabalaf =
+  > [merge-tools]
+  > alabalaf.executable = echo
+  > alabalaf.diffargs = diffing
   > EOF
 
   $ hg falabala
@@ -144,6 +148,42 @@
   diffing */extdiff.*/a.46c0e4daeb72/b a.81906f2b98ac/b (glob) (no-windows !)
   [1]
 
+Test --per-file option for gui tool:
+
+  $ hg --config extdiff.gui.alabalaf=True alabalaf -c 6 --per-file --debug
+  diffing "*\\extdiff.*\\a.46c0e4daeb72\\a" "a.81906f2b98ac\\a" (glob) (windows !)
+  diffing */extdiff.*/a.46c0e4daeb72/a a.81906f2b98ac/a (glob) (no-windows !)
+  diffing "*\\extdiff.*\\a.46c0e4daeb72\\b" "a.81906f2b98ac\\b" (glob) (windows !)
+  diffing */extdiff.*/a.46c0e4daeb72/b a.81906f2b98ac/b (glob) (no-windows !)
+  making snapshot of 2 files from rev 46c0e4daeb72
+    a
+    b
+  making snapshot of 2 files from rev 81906f2b98ac
+    a
+    b
+  running '* diffing * *' in * (backgrounded) (glob)
+  running '* diffing * *' in * (backgrounded) (glob)
+  cleaning up temp directory
+  [1]
+
+Test --per-file option for gui tool again:
+
+  $ hg --config merge-tools.alabalaf.gui=True alabalaf -c 6 --per-file --debug
+  diffing "*\\extdiff.*\\a.46c0e4daeb72\\a" "a.81906f2b98ac\\a" (glob) (windows !)
+  diffing */extdiff.*/a.46c0e4daeb72/a a.81906f2b98ac/a (glob) (no-windows !)
+  diffing "*\\extdiff.*\\a.46c0e4daeb72\\b" "a.81906f2b98ac\\b" (glob) (windows !)
+  diffing */extdiff.*/a.46c0e4daeb72/b a.81906f2b98ac/b (glob) (no-windows !)
+  making snapshot of 2 files from rev 46c0e4daeb72
+    a
+    b
+  making snapshot of 2 files from rev 81906f2b98ac
+    a
+    b
+  running '* diffing * *' in * (backgrounded) (glob)
+  running '* diffing * *' in * (backgrounded) (glob)
+  cleaning up temp directory
+  [1]
+
 Test --per-file and --confirm options:
 
   $ hg --config ui.interactive=True falabala -c 6 --per-file --confirm <<EOF
--- a/tests/test-extension.t	Tue Feb 05 11:17:11 2019 -0800
+++ b/tests/test-extension.t	Sat Feb 02 21:58:49 2019 -0800
@@ -823,7 +823,13 @@
       the working directory files are compared to its parent.
   
       The --per-file option runs the external program repeatedly on each file to
-      diff, instead of once on two directories.
+      diff, instead of once on two directories. By default, this happens one by
+      one, where the next file diff is open in the external program only once
+      the previous external program (for the previous file diff) has exited. If
+      the external program has a graphical interface, it can open all the file
+      diffs at once instead of one by one. See 'hg help -e extdiff' for
+      information about how to tell Mercurial that a given program has a
+      graphical interface.
   
       The --confirm option will prompt the user before each invocation of the
       external program. It is ignored if --per-file isn't specified.
@@ -905,6 +911,20 @@
     [diff-tools]
     kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
   
+  If a program has a graphical interface, it might be interesting to tell
+  Mercurial about it. It will prevent the program from being mistakenly used in
+  a terminal-only environment (such as an SSH terminal session), and will make
+  'hg extdiff --per-file' open multiple file diffs at once instead of one by one
+  (if you still want to open file diffs one by one, you can use the --confirm
+  option).
+  
+  Declaring that a tool has a graphical interface can be done with the "gui"
+  flag next to where "diffargs" are specified:
+  
+    [diff-tools]
+    kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
+    kdiff3.gui = true
+  
   You can use -I/-X and list of file or directory names like normal 'hg diff'
   command. The extdiff extension makes snapshots of only needed files, so
   running the external diff program will actually be pretty fast (at least