Mercurial > hg
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] |