comparison hgext/extdiff.py @ 41584:a4cd77a425a3

extdiff: support tools that can be run simultaneously
author Ludovic Chabant <ludovic@chabant.com>
date Sat, 02 Feb 2019 21:58:49 -0800
parents fa471151d269
children 2372284d9457
comparison
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]