1# extdiff.py - external diff program support for mercurial 2# 3# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8'''command to allow external programs to compare revisions 9 10The extdiff Mercurial extension allows you to use external programs 11to compare revisions, or revision with working directory. The external 12diff programs are called with a configurable set of options and two 13non-option arguments: paths to directories containing snapshots of 14files to compare. 15 16If there is more than one file being compared and the "child" revision 17is the working directory, any modifications made in the external diff 18program will be copied back to the working directory from the temporary 19directory. 20 21The extdiff extension also allows you to configure new diff commands, so 22you do not need to type :hg:`extdiff -p kdiff3` always. :: 23 24 [extdiff] 25 # add new command that runs GNU diff(1) in 'context diff' mode 26 cdiff = gdiff -Nprc5 27 ## or the old way: 28 #cmd.cdiff = gdiff 29 #opts.cdiff = -Nprc5 30 31 # add new command called meld, runs meld (no need to name twice). If 32 # the meld executable is not available, the meld tool in [merge-tools] 33 # will be used, if available 34 meld = 35 36 # add new command called vimdiff, runs gvimdiff with DirDiff plugin 37 # (see http://www.vim.org/scripts/script.php?script_id=102) Non 38 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in 39 # your .vimrc 40 vimdiff = gvim -f "+next" \\ 41 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))" 42 43Tool arguments can include variables that are expanded at runtime:: 44 45 $parent1, $plabel1 - filename, descriptive label of first parent 46 $child, $clabel - filename, descriptive label of child revision 47 $parent2, $plabel2 - filename, descriptive label of second parent 48 $root - repository root 49 $parent is an alias for $parent1. 50 51The extdiff extension will look in your [diff-tools] and [merge-tools] 52sections for diff tool arguments, when none are specified in [extdiff]. 53 54:: 55 56 [extdiff] 57 kdiff3 = 58 59 [diff-tools] 60 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child 61 62If a program has a graphical interface, it might be interesting to tell 63Mercurial about it. It will prevent the program from being mistakenly 64used in a terminal-only environment (such as an SSH terminal session), 65and will make :hg:`extdiff --per-file` open multiple file diffs at once 66instead of one by one (if you still want to open file diffs one by one, 67you can use the --confirm option). 68 69Declaring 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 78You can use -I/-X and list of file or directory names like normal 79:hg:`diff` command. The extdiff extension makes snapshots of only 80needed files, so running the external diff program will actually be 81pretty fast (at least faster than having to compare the entire tree). 82''' 83 84from __future__ import absolute_import 85 86import os 87import re 88import shutil 89import stat 90import subprocess 91 92from mercurial.i18n import _ 93from mercurial.node import ( 94 nullrev, 95 short, 96) 97from mercurial import ( 98 archival, 99 cmdutil, 100 encoding, 101 error, 102 filemerge, 103 formatter, 104 logcmdutil, 105 pycompat, 106 registrar, 107 scmutil, 108 util, 109) 110from mercurial.utils import ( 111 procutil, 112 stringutil, 113) 114 115cmdtable = {} 116command = registrar.command(cmdtable) 117 118configtable = {} 119configitem = registrar.configitem(configtable) 120 121configitem( 122 b'extdiff', 123 br'opts\..*', 124 default=b'', 125 generic=True, 126) 127 128configitem( 129 b'extdiff', 130 br'gui\..*', 131 generic=True, 132) 133 134configitem( 135 b'diff-tools', 136 br'.*\.diffargs$', 137 default=None, 138 generic=True, 139) 140 141configitem( 142 b'diff-tools', 143 br'.*\.gui$', 144 generic=True, 145) 146 147# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for 148# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should 149# be specifying the version(s) of Mercurial they are tested with, or 150# leave the attribute unspecified. 151testedwith = b'ships-with-hg-core' 152 153 154def snapshot(ui, repo, files, node, tmproot, listsubrepos): 155 """snapshot files as of some revision 156 if not using snapshot, -I/-X does not work and recursive diff 157 in tools like kdiff3 and meld displays too many files.""" 158 dirname = os.path.basename(repo.root) 159 if dirname == b"": 160 dirname = b"root" 161 if node is not None: 162 dirname = b'%s.%s' % (dirname, short(node)) 163 base = os.path.join(tmproot, dirname) 164 os.mkdir(base) 165 fnsandstat = [] 166 167 if node is not None: 168 ui.note( 169 _(b'making snapshot of %d files from rev %s\n') 170 % (len(files), short(node)) 171 ) 172 else: 173 ui.note( 174 _(b'making snapshot of %d files from working directory\n') 175 % (len(files)) 176 ) 177 178 if files: 179 repo.ui.setconfig(b"ui", b"archivemeta", False) 180 181 archival.archive( 182 repo, 183 base, 184 node, 185 b'files', 186 match=scmutil.matchfiles(repo, files), 187 subrepos=listsubrepos, 188 ) 189 190 for fn in sorted(files): 191 wfn = util.pconvert(fn) 192 ui.note(b' %s\n' % wfn) 193 194 if node is None: 195 dest = os.path.join(base, wfn) 196 197 fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest))) 198 return dirname, fnsandstat 199 200 201def formatcmdline( 202 cmdline, 203 repo_root, 204 do3way, 205 parent1, 206 plabel1, 207 parent2, 208 plabel2, 209 child, 210 clabel, 211): 212 # Function to quote file/dir names in the argument string. 213 # When not operating in 3-way mode, an empty string is 214 # returned for parent2 215 replace = { 216 b'parent': parent1, 217 b'parent1': parent1, 218 b'parent2': parent2, 219 b'plabel1': plabel1, 220 b'plabel2': plabel2, 221 b'child': child, 222 b'clabel': clabel, 223 b'root': repo_root, 224 } 225 226 def quote(match): 227 pre = match.group(2) 228 key = match.group(3) 229 if not do3way and key == b'parent2': 230 return pre 231 return pre + procutil.shellquote(replace[key]) 232 233 # Match parent2 first, so 'parent1?' will match both parent1 and parent 234 regex = ( 235 br'''(['"]?)([^\s'"$]*)''' 236 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1' 237 ) 238 if not do3way and not re.search(regex, cmdline): 239 cmdline += b' $parent1 $child' 240 return re.sub(regex, quote, cmdline) 241 242 243def _systembackground(cmd, environ=None, cwd=None): 244 """like 'procutil.system', but returns the Popen object directly 245 so we don't have to wait on it. 246 """ 247 env = procutil.shellenviron(environ) 248 proc = subprocess.Popen( 249 procutil.tonativestr(cmd), 250 shell=True, 251 close_fds=procutil.closefds, 252 env=procutil.tonativeenv(env), 253 cwd=pycompat.rapply(procutil.tonativestr, cwd), 254 ) 255 return proc 256 257 258def _runperfilediff( 259 cmdline, 260 repo_root, 261 ui, 262 guitool, 263 do3way, 264 confirm, 265 commonfiles, 266 tmproot, 267 dir1a, 268 dir1b, 269 dir2, 270 rev1a, 271 rev1b, 272 rev2, 273): 274 # Note that we need to sort the list of files because it was 275 # built in an "unstable" way and it's annoying to get files in a 276 # random order, especially when "confirm" mode is enabled. 277 waitprocs = [] 278 totalfiles = len(commonfiles) 279 for idx, commonfile in enumerate(sorted(commonfiles)): 280 path1a = os.path.join(dir1a, commonfile) 281 label1a = commonfile + rev1a 282 if not os.path.isfile(path1a): 283 path1a = pycompat.osdevnull 284 285 path1b = b'' 286 label1b = b'' 287 if do3way: 288 path1b = os.path.join(dir1b, commonfile) 289 label1b = commonfile + rev1b 290 if not os.path.isfile(path1b): 291 path1b = pycompat.osdevnull 292 293 path2 = os.path.join(dir2, commonfile) 294 label2 = commonfile + rev2 295 296 if confirm: 297 # Prompt before showing this diff 298 difffiles = _(b'diff %s (%d of %d)') % ( 299 commonfile, 300 idx + 1, 301 totalfiles, 302 ) 303 responses = _( 304 b'[Yns?]' 305 b'$$ &Yes, show diff' 306 b'$$ &No, skip this diff' 307 b'$$ &Skip remaining diffs' 308 b'$$ &? (display help)' 309 ) 310 r = ui.promptchoice(b'%s %s' % (difffiles, responses)) 311 if r == 3: # ? 312 while r == 3: 313 for c, t in ui.extractchoices(responses)[1]: 314 ui.write(b'%s - %s\n' % (c, encoding.lower(t))) 315 r = ui.promptchoice(b'%s %s' % (difffiles, responses)) 316 if r == 0: # yes 317 pass 318 elif r == 1: # no 319 continue 320 elif r == 2: # skip 321 break 322 323 curcmdline = formatcmdline( 324 cmdline, 325 repo_root, 326 do3way=do3way, 327 parent1=path1a, 328 plabel1=label1a, 329 parent2=path1b, 330 plabel2=label1b, 331 child=path2, 332 clabel=label2, 333 ) 334 335 if confirm or not guitool: 336 # Run the comparison program and wait for it to exit 337 # before we show the next file. 338 # This is because either we need to wait for confirmation 339 # from the user between each invocation, or because, as far 340 # as we know, the tool doesn't have a GUI, in which case 341 # we can't run multiple CLI programs at the same time. 342 ui.debug( 343 b'running %r in %s\n' % (pycompat.bytestr(curcmdline), tmproot) 344 ) 345 ui.system(curcmdline, cwd=tmproot, blockedtag=b'extdiff') 346 else: 347 # Run the comparison program but don't wait, as we're 348 # going to rapid-fire each file diff and then wait on 349 # the whole group. 350 ui.debug( 351 b'running %r in %s (backgrounded)\n' 352 % (pycompat.bytestr(curcmdline), tmproot) 353 ) 354 proc = _systembackground(curcmdline, cwd=tmproot) 355 waitprocs.append(proc) 356 357 if waitprocs: 358 with ui.timeblockedsection(b'extdiff'): 359 for proc in waitprocs: 360 proc.wait() 361 362 363def diffpatch(ui, repo, node1, node2, tmproot, matcher, cmdline): 364 template = b'hg-%h.patch' 365 # write patches to temporary files 366 with formatter.nullformatter(ui, b'extdiff', {}) as fm: 367 cmdutil.export( 368 repo, 369 [repo[node1].rev(), repo[node2].rev()], 370 fm, 371 fntemplate=repo.vfs.reljoin(tmproot, template), 372 match=matcher, 373 ) 374 label1 = cmdutil.makefilename(repo[node1], template) 375 label2 = cmdutil.makefilename(repo[node2], template) 376 file1 = repo.vfs.reljoin(tmproot, label1) 377 file2 = repo.vfs.reljoin(tmproot, label2) 378 cmdline = formatcmdline( 379 cmdline, 380 repo.root, 381 # no 3way while comparing patches 382 do3way=False, 383 parent1=file1, 384 plabel1=label1, 385 # while comparing patches, there is no second parent 386 parent2=None, 387 plabel2=None, 388 child=file2, 389 clabel=label2, 390 ) 391 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)) 392 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff') 393 return 1 394 395 396def diffrevs( 397 ui, 398 repo, 399 ctx1a, 400 ctx1b, 401 ctx2, 402 matcher, 403 tmproot, 404 cmdline, 405 do3way, 406 guitool, 407 opts, 408): 409 410 subrepos = opts.get(b'subrepos') 411 412 # calculate list of files changed between both revs 413 st = ctx1a.status(ctx2, matcher, listsubrepos=subrepos) 414 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed) 415 if do3way: 416 stb = ctx1b.status(ctx2, matcher, listsubrepos=subrepos) 417 mod_b, add_b, rem_b = ( 418 set(stb.modified), 419 set(stb.added), 420 set(stb.removed), 421 ) 422 else: 423 mod_b, add_b, rem_b = set(), set(), set() 424 modadd = mod_a | add_a | mod_b | add_b 425 common = modadd | rem_a | rem_b 426 if not common: 427 return 0 428 429 # Always make a copy of ctx1a (and ctx1b, if applicable) 430 # dir1a should contain files which are: 431 # * modified or removed from ctx1a to ctx2 432 # * modified or added from ctx1b to ctx2 433 # (except file added from ctx1a to ctx2 as they were not present in 434 # ctx1a) 435 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a) 436 dir1a = snapshot(ui, repo, dir1a_files, ctx1a.node(), tmproot, subrepos)[0] 437 rev1a = b'' if ctx1a.rev() is None else b'@%d' % ctx1a.rev() 438 if do3way: 439 # file calculation criteria same as dir1a 440 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b) 441 dir1b = snapshot( 442 ui, repo, dir1b_files, ctx1b.node(), tmproot, subrepos 443 )[0] 444 rev1b = b'@%d' % ctx1b.rev() 445 else: 446 dir1b = None 447 rev1b = b'' 448 449 fnsandstat = [] 450 451 # If ctx2 is not the wc or there is >1 change, copy it 452 dir2root = b'' 453 rev2 = b'' 454 if ctx2.node() is not None: 455 dir2 = snapshot(ui, repo, modadd, ctx2.node(), tmproot, subrepos)[0] 456 rev2 = b'@%d' % ctx2.rev() 457 elif len(common) > 1: 458 # we only actually need to get the files to copy back to 459 # the working dir in this case (because the other cases 460 # are: diffing 2 revisions or single file -- in which case 461 # the file is already directly passed to the diff tool). 462 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot, subrepos) 463 else: 464 # This lets the diff tool open the changed file directly 465 dir2 = b'' 466 dir2root = repo.root 467 468 label1a = rev1a 469 label1b = rev1b 470 label2 = rev2 471 472 if not opts.get(b'per_file'): 473 # If only one change, diff the files instead of the directories 474 # Handle bogus modifies correctly by checking if the files exist 475 if len(common) == 1: 476 common_file = util.localpath(common.pop()) 477 dir1a = os.path.join(tmproot, dir1a, common_file) 478 label1a = common_file + rev1a 479 if not os.path.isfile(dir1a): 480 dir1a = pycompat.osdevnull 481 if do3way: 482 dir1b = os.path.join(tmproot, dir1b, common_file) 483 label1b = common_file + rev1b 484 if not os.path.isfile(dir1b): 485 dir1b = pycompat.osdevnull 486 dir2 = os.path.join(dir2root, dir2, common_file) 487 label2 = common_file + rev2 488 489 # Run the external tool on the 2 temp directories or the patches 490 cmdline = formatcmdline( 491 cmdline, 492 repo.root, 493 do3way=do3way, 494 parent1=dir1a, 495 plabel1=label1a, 496 parent2=dir1b, 497 plabel2=label1b, 498 child=dir2, 499 clabel=label2, 500 ) 501 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)) 502 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff') 503 else: 504 # Run the external tool once for each pair of files 505 _runperfilediff( 506 cmdline, 507 repo.root, 508 ui, 509 guitool=guitool, 510 do3way=do3way, 511 confirm=opts.get(b'confirm'), 512 commonfiles=common, 513 tmproot=tmproot, 514 dir1a=os.path.join(tmproot, dir1a), 515 dir1b=os.path.join(tmproot, dir1b) if do3way else None, 516 dir2=os.path.join(dir2root, dir2), 517 rev1a=rev1a, 518 rev1b=rev1b, 519 rev2=rev2, 520 ) 521 522 for copy_fn, working_fn, st in fnsandstat: 523 cpstat = os.lstat(copy_fn) 524 # Some tools copy the file and attributes, so mtime may not detect 525 # all changes. A size check will detect more cases, but not all. 526 # The only certain way to detect every case is to diff all files, 527 # which could be expensive. 528 # copyfile() carries over the permission, so the mode check could 529 # be in an 'elif' branch, but for the case where the file has 530 # changed without affecting mtime or size. 531 if ( 532 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME] 533 or cpstat.st_size != st.st_size 534 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100) 535 ): 536 ui.debug( 537 b'file changed while diffing. ' 538 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn) 539 ) 540 util.copyfile(copy_fn, working_fn) 541 542 return 1 543 544 545def dodiff(ui, repo, cmdline, pats, opts, guitool=False): 546 """Do the actual diff: 547 548 - copy to a temp structure if diffing 2 internal revisions 549 - copy to a temp structure if diffing working revision with 550 another one and more than 1 file is changed 551 - just invoke the diff for a single file in the working dir 552 """ 553 554 cmdutil.check_at_most_one_arg(opts, b'rev', b'change') 555 revs = opts.get(b'rev') 556 from_rev = opts.get(b'from') 557 to_rev = opts.get(b'to') 558 change = opts.get(b'change') 559 do3way = b'$parent2' in cmdline 560 561 if change: 562 ctx2 = logcmdutil.revsingle(repo, change, None) 563 ctx1a, ctx1b = ctx2.p1(), ctx2.p2() 564 elif from_rev or to_rev: 565 repo = scmutil.unhidehashlikerevs( 566 repo, [from_rev] + [to_rev], b'nowarn' 567 ) 568 ctx1a = logcmdutil.revsingle(repo, from_rev, None) 569 ctx1b = repo[nullrev] 570 ctx2 = logcmdutil.revsingle(repo, to_rev, None) 571 else: 572 ctx1a, ctx2 = logcmdutil.revpair(repo, revs) 573 if not revs: 574 ctx1b = repo[None].p2() 575 else: 576 ctx1b = repo[nullrev] 577 578 # Disable 3-way merge if there is only one parent 579 if do3way: 580 if ctx1b.rev() == nullrev: 581 do3way = False 582 583 matcher = scmutil.match(ctx2, pats, opts) 584 585 if opts.get(b'patch'): 586 if opts.get(b'subrepos'): 587 raise error.Abort(_(b'--patch cannot be used with --subrepos')) 588 if opts.get(b'per_file'): 589 raise error.Abort(_(b'--patch cannot be used with --per-file')) 590 if ctx2.node() is None: 591 raise error.Abort(_(b'--patch requires two revisions')) 592 593 tmproot = pycompat.mkdtemp(prefix=b'extdiff.') 594 try: 595 if opts.get(b'patch'): 596 return diffpatch( 597 ui, repo, ctx1a.node(), ctx2.node(), tmproot, matcher, cmdline 598 ) 599 600 return diffrevs( 601 ui, 602 repo, 603 ctx1a, 604 ctx1b, 605 ctx2, 606 matcher, 607 tmproot, 608 cmdline, 609 do3way, 610 guitool, 611 opts, 612 ) 613 614 finally: 615 ui.note(_(b'cleaning up temp directory\n')) 616 shutil.rmtree(tmproot) 617 618 619extdiffopts = ( 620 [ 621 ( 622 b'o', 623 b'option', 624 [], 625 _(b'pass option to comparison program'), 626 _(b'OPT'), 627 ), 628 (b'r', b'rev', [], _(b'revision (DEPRECATED)'), _(b'REV')), 629 (b'', b'from', b'', _(b'revision to diff from'), _(b'REV1')), 630 (b'', b'to', b'', _(b'revision to diff to'), _(b'REV2')), 631 (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')), 632 ( 633 b'', 634 b'per-file', 635 False, 636 _(b'compare each file instead of revision snapshots'), 637 ), 638 ( 639 b'', 640 b'confirm', 641 False, 642 _(b'prompt user before each external program invocation'), 643 ), 644 (b'', b'patch', None, _(b'compare patches for two revisions')), 645 ] 646 + cmdutil.walkopts 647 + cmdutil.subrepoopts 648) 649 650 651@command( 652 b'extdiff', 653 [ 654 (b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')), 655 ] 656 + extdiffopts, 657 _(b'hg extdiff [OPT]... [FILE]...'), 658 helpcategory=command.CATEGORY_FILE_CONTENTS, 659 inferrepo=True, 660) 661def extdiff(ui, repo, *pats, **opts): 662 """use external program to diff repository (or selected files) 663 664 Show differences between revisions for the specified files, using 665 an external program. The default program used is diff, with 666 default options "-Npru". 667 668 To select a different program, use the -p/--program option. The 669 program will be passed the names of two directories to compare, 670 unless the --per-file option is specified (see below). To pass 671 additional options to the program, use -o/--option. These will be 672 passed before the names of the directories or files to compare. 673 674 The --from, --to, and --change options work the same way they do for 675 :hg:`diff`. 676 677 The --per-file option runs the external program repeatedly on each 678 file to diff, instead of once on two directories. By default, 679 this happens one by one, where the next file diff is open in the 680 external program only once the previous external program (for the 681 previous file diff) has exited. If the external program has a 682 graphical interface, it can open all the file diffs at once instead 683 of one by one. See :hg:`help -e extdiff` for information about how 684 to tell Mercurial that a given program has a graphical interface. 685 686 The --confirm option will prompt the user before each invocation of 687 the external program. It is ignored if --per-file isn't specified. 688 """ 689 opts = pycompat.byteskwargs(opts) 690 program = opts.get(b'program') 691 option = opts.get(b'option') 692 if not program: 693 program = b'diff' 694 option = option or [b'-Npru'] 695 cmdline = b' '.join(map(procutil.shellquote, [program] + option)) 696 return dodiff(ui, repo, cmdline, pats, opts) 697 698 699class savedcmd(object): 700 """use external program to diff repository (or selected files) 701 702 Show differences between revisions for the specified files, using 703 the following program:: 704 705 %(path)s 706 707 When two revision arguments are given, then changes are shown 708 between those revisions. If only one revision is specified then 709 that revision is compared to the working directory, and, when no 710 revisions are specified, the working directory files are compared 711 to its parent. 712 """ 713 714 def __init__(self, path, cmdline, isgui): 715 # We can't pass non-ASCII through docstrings (and path is 716 # in an unknown encoding anyway), but avoid double separators on 717 # Windows 718 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\') 719 self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))} 720 self._cmdline = cmdline 721 self._isgui = isgui 722 723 def __call__(self, ui, repo, *pats, **opts): 724 opts = pycompat.byteskwargs(opts) 725 options = b' '.join(map(procutil.shellquote, opts[b'option'])) 726 if options: 727 options = b' ' + options 728 return dodiff( 729 ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui 730 ) 731 732 733def _gettooldetails(ui, cmd, path): 734 """ 735 returns following things for a 736 ``` 737 [extdiff] 738 <cmd> = <path> 739 ``` 740 entry: 741 742 cmd: command/tool name 743 path: path to the tool 744 cmdline: the command which should be run 745 isgui: whether the tool uses GUI or not 746 747 Reads all external tools related configs, whether it be extdiff section, 748 diff-tools or merge-tools section, or its specified in an old format or 749 the latest format. 750 """ 751 path = util.expandpath(path) 752 if cmd.startswith(b'cmd.'): 753 cmd = cmd[4:] 754 if not path: 755 path = procutil.findexe(cmd) 756 if path is None: 757 path = filemerge.findexternaltool(ui, cmd) or cmd 758 diffopts = ui.config(b'extdiff', b'opts.' + cmd) 759 cmdline = procutil.shellquote(path) 760 if diffopts: 761 cmdline += b' ' + diffopts 762 isgui = ui.configbool(b'extdiff', b'gui.' + cmd) 763 else: 764 if path: 765 # case "cmd = path opts" 766 cmdline = path 767 diffopts = len(pycompat.shlexsplit(cmdline)) > 1 768 else: 769 # case "cmd =" 770 path = procutil.findexe(cmd) 771 if path is None: 772 path = filemerge.findexternaltool(ui, cmd) or cmd 773 cmdline = procutil.shellquote(path) 774 diffopts = False 775 isgui = ui.configbool(b'extdiff', b'gui.' + cmd) 776 # look for diff arguments in [diff-tools] then [merge-tools] 777 if not diffopts: 778 key = cmd + b'.diffargs' 779 for section in (b'diff-tools', b'merge-tools'): 780 args = ui.config(section, key) 781 if args: 782 cmdline += b' ' + args 783 if isgui is None: 784 isgui = ui.configbool(section, cmd + b'.gui') or False 785 break 786 return cmd, path, cmdline, isgui 787 788 789def uisetup(ui): 790 for cmd, path in ui.configitems(b'extdiff'): 791 if cmd.startswith(b'opts.') or cmd.startswith(b'gui.'): 792 continue 793 cmd, path, cmdline, isgui = _gettooldetails(ui, cmd, path) 794 command( 795 cmd, 796 extdiffopts[:], 797 _(b'hg %s [OPTION]... [FILE]...') % cmd, 798 helpcategory=command.CATEGORY_FILE_CONTENTS, 799 inferrepo=True, 800 )(savedcmd(path, cmdline, isgui)) 801 802 803# tell hggettext to extract docstrings from these functions: 804i18nfunctions = [savedcmd] 805