hgext/extdiff.py
changeset 41584 a4cd77a425a3
parent 41487 fa471151d269
child 43076 2372284d9457
equal deleted inserted replaced
41582:7b2580e0dbbd 41584:a4cd77a425a3
    57   kdiff3 =
    57   kdiff3 =
    58 
    58 
    59   [diff-tools]
    59   [diff-tools]
    60   kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
    60   kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
    61 
    61 
       
    62 If a program has a graphical interface, it might be interesting to tell
       
    63 Mercurial about it. It will prevent the program from being mistakenly
       
    64 used in a terminal-only environment (such as an SSH terminal session),
       
    65 and will make :hg:`extdiff --per-file` open multiple file diffs at once
       
    66 instead of one by one (if you still want to open file diffs one by one,
       
    67 you can use the --confirm option).
       
    68 
       
    69 Declaring that a tool has a graphical interface can be done with the
       
    70 ``gui`` flag next to where ``diffargs`` are specified:
       
    71 
       
    72 ::
       
    73 
       
    74   [diff-tools]
       
    75   kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
       
    76   kdiff3.gui = true
       
    77 
    62 You can use -I/-X and list of file or directory names like normal
    78 You can use -I/-X and list of file or directory names like normal
    63 :hg:`diff` command. The extdiff extension makes snapshots of only
    79 :hg:`diff` command. The extdiff extension makes snapshots of only
    64 needed files, so running the external diff program will actually be
    80 needed files, so running the external diff program will actually be
    65 pretty fast (at least faster than having to compare the entire tree).
    81 pretty fast (at least faster than having to compare the entire tree).
    66 '''
    82 '''
    69 
    85 
    70 import os
    86 import os
    71 import re
    87 import re
    72 import shutil
    88 import shutil
    73 import stat
    89 import stat
       
    90 import subprocess
    74 
    91 
    75 from mercurial.i18n import _
    92 from mercurial.i18n import _
    76 from mercurial.node import (
    93 from mercurial.node import (
    77     nullid,
    94     nullid,
    78     short,
    95     short,
   103 configitem('extdiff', br'opts\..*',
   120 configitem('extdiff', br'opts\..*',
   104     default='',
   121     default='',
   105     generic=True,
   122     generic=True,
   106 )
   123 )
   107 
   124 
       
   125 configitem('extdiff', br'gui\..*',
       
   126     generic=True,
       
   127 )
       
   128 
   108 configitem('diff-tools', br'.*\.diffargs$',
   129 configitem('diff-tools', br'.*\.diffargs$',
   109     default=None,
   130     default=None,
       
   131     generic=True,
       
   132 )
       
   133 
       
   134 configitem('diff-tools', br'.*\.gui$',
   110     generic=True,
   135     generic=True,
   111 )
   136 )
   112 
   137 
   113 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
   138 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
   114 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
   139 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
   174              br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
   199              br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
   175     if not do3way and not re.search(regex, cmdline):
   200     if not do3way and not re.search(regex, cmdline):
   176         cmdline += ' $parent1 $child'
   201         cmdline += ' $parent1 $child'
   177     return re.sub(regex, quote, cmdline)
   202     return re.sub(regex, quote, cmdline)
   178 
   203 
   179 def _runperfilediff(cmdline, repo_root, ui, do3way, confirm,
   204 def _systembackground(cmd, environ=None, cwd=None):
       
   205     ''' like 'procutil.system', but returns the Popen object directly
       
   206         so we don't have to wait on it.
       
   207     '''
       
   208     cmd = procutil.quotecommand(cmd)
       
   209     env = procutil.shellenviron(environ)
       
   210     proc = subprocess.Popen(procutil.tonativestr(cmd),
       
   211                             shell=True, close_fds=procutil.closefds,
       
   212                             env=procutil.tonativeenv(env),
       
   213                             cwd=pycompat.rapply(procutil.tonativestr, cwd))
       
   214     return proc
       
   215 
       
   216 def _runperfilediff(cmdline, repo_root, ui, guitool, do3way, confirm,
   180                     commonfiles, tmproot, dir1a, dir1b,
   217                     commonfiles, tmproot, dir1a, dir1b,
   181                     dir2root, dir2,
   218                     dir2root, dir2,
   182                     rev1a, rev1b, rev2):
   219                     rev1a, rev1b, rev2):
   183     # Note that we need to sort the list of files because it was
   220     # Note that we need to sort the list of files because it was
   184     # built in an "unstable" way and it's annoying to get files in a
   221     # built in an "unstable" way and it's annoying to get files in a
   185     # random order, especially when "confirm" mode is enabled.
   222     # random order, especially when "confirm" mode is enabled.
       
   223     waitprocs = []
   186     totalfiles = len(commonfiles)
   224     totalfiles = len(commonfiles)
   187     for idx, commonfile in enumerate(sorted(commonfiles)):
   225     for idx, commonfile in enumerate(sorted(commonfiles)):
   188         path1a = os.path.join(tmproot, dir1a, commonfile)
   226         path1a = os.path.join(tmproot, dir1a, commonfile)
   189         label1a = commonfile + rev1a
   227         label1a = commonfile + rev1a
   190         if not os.path.isfile(path1a):
   228         if not os.path.isfile(path1a):
   226         curcmdline = formatcmdline(
   264         curcmdline = formatcmdline(
   227             cmdline, repo_root, do3way=do3way,
   265             cmdline, repo_root, do3way=do3way,
   228             parent1=path1a, plabel1=label1a,
   266             parent1=path1a, plabel1=label1a,
   229             parent2=path1b, plabel2=label1b,
   267             parent2=path1b, plabel2=label1b,
   230             child=path2, clabel=label2)
   268             child=path2, clabel=label2)
   231         ui.debug('running %r in %s\n' % (pycompat.bytestr(curcmdline),
   269 
   232                                          tmproot))
   270         if confirm or not guitool:
   233 
   271             # Run the comparison program and wait for it to exit
   234         # Run the comparison program and wait for it to exit
   272             # before we show the next file.
   235         # before we show the next file.
   273             # This is because either we need to wait for confirmation
   236         ui.system(curcmdline, cwd=tmproot, blockedtag='extdiff')
   274             # from the user between each invocation, or because, as far
   237 
   275             # as we know, the tool doesn't have a GUI, in which case
   238 def dodiff(ui, repo, cmdline, pats, opts):
   276             # we can't run multiple CLI programs at the same time.
       
   277             ui.debug('running %r in %s\n' %
       
   278                      (pycompat.bytestr(curcmdline), tmproot))
       
   279             ui.system(curcmdline, cwd=tmproot, blockedtag='extdiff')
       
   280         else:
       
   281             # Run the comparison program but don't wait, as we're
       
   282             # going to rapid-fire each file diff and then wait on
       
   283             # the whole group.
       
   284             ui.debug('running %r in %s (backgrounded)\n' %
       
   285                      (pycompat.bytestr(curcmdline), tmproot))
       
   286             proc = _systembackground(curcmdline, cwd=tmproot)
       
   287             waitprocs.append(proc)
       
   288 
       
   289     if waitprocs:
       
   290         with ui.timeblockedsection('extdiff'):
       
   291             for proc in waitprocs:
       
   292                 proc.wait()
       
   293 
       
   294 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
   239     '''Do the actual diff:
   295     '''Do the actual diff:
   240 
   296 
   241     - copy to a temp structure if diffing 2 internal revisions
   297     - copy to a temp structure if diffing 2 internal revisions
   242     - copy to a temp structure if diffing working revision with
   298     - copy to a temp structure if diffing working revision with
   243       another one and more than 1 file is changed
   299       another one and more than 1 file is changed
   380                                              tmproot))
   436                                              tmproot))
   381             ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
   437             ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
   382         else:
   438         else:
   383             # Run the external tool once for each pair of files
   439             # Run the external tool once for each pair of files
   384             _runperfilediff(
   440             _runperfilediff(
   385                 cmdline, repo.root, ui, do3way=do3way, confirm=confirm,
   441                 cmdline, repo.root, ui, guitool=guitool,
       
   442                 do3way=do3way, confirm=confirm,
   386                 commonfiles=common, tmproot=tmproot, dir1a=dir1a, dir1b=dir1b,
   443                 commonfiles=common, tmproot=tmproot, dir1a=dir1a, dir1b=dir1b,
   387                 dir2root=dir2root, dir2=dir2,
   444                 dir2root=dir2root, dir2=dir2,
   388                 rev1a=rev1a, rev1b=rev1b, rev2=rev2)
   445                 rev1a=rev1a, rev1b=rev1b, rev2=rev2)
   389 
   446 
   390         for copy_fn, working_fn, st in fnsandstat:
   447         for copy_fn, working_fn, st in fnsandstat:
   444     that revision is compared to the working directory, and, when no
   501     that revision is compared to the working directory, and, when no
   445     revisions are specified, the working directory files are compared
   502     revisions are specified, the working directory files are compared
   446     to its parent.
   503     to its parent.
   447 
   504 
   448     The --per-file option runs the external program repeatedly on each
   505     The --per-file option runs the external program repeatedly on each
   449     file to diff, instead of once on two directories.
   506     file to diff, instead of once on two directories. By default,
       
   507     this happens one by one, where the next file diff is open in the
       
   508     external program only once the previous external program (for the
       
   509     previous file diff) has exited. If the external program has a
       
   510     graphical interface, it can open all the file diffs at once instead
       
   511     of one by one. See :hg:`help -e extdiff` for information about how
       
   512     to tell Mercurial that a given program has a graphical interface.
   450 
   513 
   451     The --confirm option will prompt the user before each invocation of
   514     The --confirm option will prompt the user before each invocation of
   452     the external program. It is ignored if --per-file isn't specified.
   515     the external program. It is ignored if --per-file isn't specified.
   453     '''
   516     '''
   454     opts = pycompat.byteskwargs(opts)
   517     opts = pycompat.byteskwargs(opts)
   473     that revision is compared to the working directory, and, when no
   536     that revision is compared to the working directory, and, when no
   474     revisions are specified, the working directory files are compared
   537     revisions are specified, the working directory files are compared
   475     to its parent.
   538     to its parent.
   476     """
   539     """
   477 
   540 
   478     def __init__(self, path, cmdline):
   541     def __init__(self, path, cmdline, isgui):
   479         # We can't pass non-ASCII through docstrings (and path is
   542         # We can't pass non-ASCII through docstrings (and path is
   480         # in an unknown encoding anyway), but avoid double separators on
   543         # in an unknown encoding anyway), but avoid double separators on
   481         # Windows
   544         # Windows
   482         docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
   545         docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
   483         self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
   546         self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
   484         self._cmdline = cmdline
   547         self._cmdline = cmdline
       
   548         self._isgui = isgui
   485 
   549 
   486     def __call__(self, ui, repo, *pats, **opts):
   550     def __call__(self, ui, repo, *pats, **opts):
   487         opts = pycompat.byteskwargs(opts)
   551         opts = pycompat.byteskwargs(opts)
   488         options = ' '.join(map(procutil.shellquote, opts['option']))
   552         options = ' '.join(map(procutil.shellquote, opts['option']))
   489         if options:
   553         if options:
   490             options = ' ' + options
   554             options = ' ' + options
   491         return dodiff(ui, repo, self._cmdline + options, pats, opts)
   555         return dodiff(ui, repo, self._cmdline + options, pats, opts,
       
   556                       guitool=self._isgui)
   492 
   557 
   493 def uisetup(ui):
   558 def uisetup(ui):
   494     for cmd, path in ui.configitems('extdiff'):
   559     for cmd, path in ui.configitems('extdiff'):
   495         path = util.expandpath(path)
   560         path = util.expandpath(path)
   496         if cmd.startswith('cmd.'):
   561         if cmd.startswith('cmd.'):
   501                     path = filemerge.findexternaltool(ui, cmd) or cmd
   566                     path = filemerge.findexternaltool(ui, cmd) or cmd
   502             diffopts = ui.config('extdiff', 'opts.' + cmd)
   567             diffopts = ui.config('extdiff', 'opts.' + cmd)
   503             cmdline = procutil.shellquote(path)
   568             cmdline = procutil.shellquote(path)
   504             if diffopts:
   569             if diffopts:
   505                 cmdline += ' ' + diffopts
   570                 cmdline += ' ' + diffopts
   506         elif cmd.startswith('opts.'):
   571             isgui = ui.configbool('extdiff', 'gui.' + cmd)
       
   572         elif cmd.startswith('opts.') or cmd.startswith('gui.'):
   507             continue
   573             continue
   508         else:
   574         else:
   509             if path:
   575             if path:
   510                 # case "cmd = path opts"
   576                 # case "cmd = path opts"
   511                 cmdline = path
   577                 cmdline = path
   515                 path = procutil.findexe(cmd)
   581                 path = procutil.findexe(cmd)
   516                 if path is None:
   582                 if path is None:
   517                     path = filemerge.findexternaltool(ui, cmd) or cmd
   583                     path = filemerge.findexternaltool(ui, cmd) or cmd
   518                 cmdline = procutil.shellquote(path)
   584                 cmdline = procutil.shellquote(path)
   519                 diffopts = False
   585                 diffopts = False
       
   586             isgui = ui.configbool('extdiff', 'gui.' + cmd)
   520         # look for diff arguments in [diff-tools] then [merge-tools]
   587         # look for diff arguments in [diff-tools] then [merge-tools]
   521         if not diffopts:
   588         if not diffopts:
   522             args = ui.config('diff-tools', cmd+'.diffargs') or \
   589             key = cmd + '.diffargs'
   523                    ui.config('merge-tools', cmd+'.diffargs')
   590             for section in ('diff-tools', 'merge-tools'):
   524             if args:
   591                 args = ui.config(section, key)
   525                 cmdline += ' ' + args
   592                 if args:
       
   593                     cmdline += ' ' + args
       
   594                     if isgui is None:
       
   595                         isgui = ui.configbool(section, cmd + '.gui') or False
       
   596                     break
   526         command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
   597         command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
   527                 helpcategory=command.CATEGORY_FILE_CONTENTS,
   598                 helpcategory=command.CATEGORY_FILE_CONTENTS,
   528                 inferrepo=True)(savedcmd(path, cmdline))
   599                 inferrepo=True)(savedcmd(path, cmdline, isgui))
   529 
   600 
   530 # tell hggettext to extract docstrings from these functions:
   601 # tell hggettext to extract docstrings from these functions:
   531 i18nfunctions = [savedcmd]
   602 i18nfunctions = [savedcmd]