extdiff.py 23.7 KB
Newer Older
1
2
3
4
# extdiff.py - external diff program support for mercurial
#
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
#
5
# This software may be used and distributed according to the terms of the
Matt Mackall's avatar
Matt Mackall committed
6
# GNU General Public License version 2 or any later version.
Brendan Cully's avatar
Brendan Cully committed
7

8
'''command to allow external programs to compare revisions
9

10
The extdiff Mercurial extension allows you to use external programs
11
12
to compare revisions, or revision with working directory. The external
diff programs are called with a configurable set of options and two
Brendan Cully's avatar
Brendan Cully committed
13
14
15
non-option arguments: paths to directories containing snapshots of
files to compare.

16
17
18
19
20
If there is more than one file being compared and the "child" revision
is the working directory, any modifications made in the external diff
program will be copied back to the working directory from the temporary
directory.

21
The extdiff extension also allows you to configure new diff commands, so
22
you do not need to type :hg:`extdiff -p kdiff3` always. ::
23

Brendan Cully's avatar
Brendan Cully committed
24
25
26
27
28
29
  [extdiff]
  # add new command that runs GNU diff(1) in 'context diff' mode
  cdiff = gdiff -Nprc5
  ## or the old way:
  #cmd.cdiff = gdiff
  #opts.cdiff = -Nprc5
30

31
32
33
  # add new command called meld, runs meld (no need to name twice).  If
  # the meld executable is not available, the meld tool in [merge-tools]
  # will be used, if available
Brendan Cully's avatar
Brendan Cully committed
34
35
36
  meld =

  # add new command called vimdiff, runs gvimdiff with DirDiff plugin
37
38
  # (see http://www.vim.org/scripts/script.php?script_id=102) Non
  # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
Brendan Cully's avatar
Brendan Cully committed
39
  # your .vimrc
40
41
  vimdiff = gvim -f "+next" \\
            "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
Brendan Cully's avatar
Brendan Cully committed
42

43
Tool arguments can include variables that are expanded at runtime::
44
45
46
47

  $parent1, $plabel1 - filename, descriptive label of first parent
  $child,   $clabel  - filename, descriptive label of child revision
  $parent2, $plabel2 - filename, descriptive label of second parent
48
  $root              - repository root
49
50
51
52
53
  $parent is an alias for $parent1.

The extdiff extension will look in your [diff-tools] and [merge-tools]
sections for diff tool arguments, when none are specified in [extdiff].

54
55
::

56
  [extdiff]
57
  kdiff3 =
58
59
60
61

  [diff-tools]
  kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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

78
79
80
81
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 faster than having to compare the entire tree).
Brendan Cully's avatar
Brendan Cully committed
82
'''
83

84
85
86
87
88
from __future__ import absolute_import

import os
import re
import shutil
89
import stat
90
import subprocess
91

Matt Mackall's avatar
Matt Mackall committed
92
from mercurial.i18n import _
93
94
95
96
97
98
99
from mercurial.node import (
    nullid,
    short,
)
from mercurial import (
    archival,
    cmdutil,
100
    encoding,
101
102
    error,
    filemerge,
103
    formatter,
104
    pycompat,
105
    registrar,
106
107
108
    scmutil,
    util,
)
109
from mercurial.utils import (
110
    procutil,
111
112
    stringutil,
)
Brad Schick's avatar
Brad Schick committed
113

114
cmdtable = {}
115
command = registrar.command(cmdtable)
116
117
118
119

configtable = {}
configitem = registrar.configitem(configtable)

120
configitem(
121
    b'extdiff', br'opts\..*', default=b'', generic=True,
122
123
)

124
configitem(
125
    b'extdiff', br'gui\..*', generic=True,
126
127
)

128
configitem(
129
    b'diff-tools', br'.*\.diffargs$', default=None, generic=True,
130
131
)

132
configitem(
133
    b'diff-tools', br'.*\.gui$', generic=True,
134
135
)

136
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
137
138
139
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
# be specifying the version(s) of Mercurial they are tested with, or
# leave the attribute unspecified.
140
testedwith = b'ships-with-hg-core'
141

142

143
def snapshot(ui, repo, files, node, tmproot, listsubrepos):
144
145
146
    '''snapshot files as of some revision
    if not using snapshot, -I/-X does not work and recursive diff
    in tools like kdiff3 and meld displays too many files.'''
Brad Schick's avatar
Brad Schick committed
147
    dirname = os.path.basename(repo.root)
148
149
    if dirname == b"":
        dirname = b"root"
150
    if node is not None:
151
        dirname = b'%s.%s' % (dirname, short(node))
Brad Schick's avatar
Brad Schick committed
152
153
    base = os.path.join(tmproot, dirname)
    os.mkdir(base)
154
    fnsandstat = []
155

156
    if node is not None:
157
        ui.note(
158
            _(b'making snapshot of %d files from rev %s\n')
159
160
            % (len(files), short(node))
        )
161
    else:
162
        ui.note(
163
            _(b'making snapshot of %d files from working directory\n')
164
165
            % (len(files))
        )
166
167

    if files:
168
        repo.ui.setconfig(b"ui", b"archivemeta", False)
169

170
171
172
173
        archival.archive(
            repo,
            base,
            node,
174
            b'files',
175
176
177
            match=scmutil.matchfiles(repo, files),
            subrepos=listsubrepos,
        )
178
179
180

        for fn in sorted(files):
            wfn = util.pconvert(fn)
181
            ui.note(b'  %s\n' % wfn)
182
183
184
185

            if node is None:
                dest = os.path.join(base, wfn)

186
187
                fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest)))
    return dirname, fnsandstat
188

189
190
191
192
193
194
195
196
197
198
199
200

def formatcmdline(
    cmdline,
    repo_root,
    do3way,
    parent1,
    plabel1,
    parent2,
    plabel2,
    child,
    clabel,
):
201
202
203
    # Function to quote file/dir names in the argument string.
    # When not operating in 3-way mode, an empty string is
    # returned for parent2
204
    replace = {
205
206
207
208
209
210
211
212
        b'parent': parent1,
        b'parent1': parent1,
        b'parent2': parent2,
        b'plabel1': plabel1,
        b'plabel2': plabel2,
        b'child': child,
        b'clabel': clabel,
        b'root': repo_root,
213
214
    }

215
216
217
    def quote(match):
        pre = match.group(2)
        key = match.group(3)
218
        if not do3way and key == b'parent2':
219
220
221
222
            return pre
        return pre + procutil.shellquote(replace[key])

    # Match parent2 first, so 'parent1?' will match both parent1 and parent
223
224
225
226
    regex = (
        br'''(['"]?)([^\s'"$]*)'''
        br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1'
    )
227
    if not do3way and not re.search(regex, cmdline):
228
        cmdline += b' $parent1 $child'
229
230
    return re.sub(regex, quote, cmdline)

231

232
233
234
235
236
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.
    '''
    env = procutil.shellenviron(environ)
237
238
239
240
241
242
243
    proc = subprocess.Popen(
        procutil.tonativestr(cmd),
        shell=True,
        close_fds=procutil.closefds,
        env=procutil.tonativeenv(env),
        cwd=pycompat.rapply(procutil.tonativestr, cwd),
    )
244
245
    return proc

246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263

def _runperfilediff(
    cmdline,
    repo_root,
    ui,
    guitool,
    do3way,
    confirm,
    commonfiles,
    tmproot,
    dir1a,
    dir1b,
    dir2root,
    dir2,
    rev1a,
    rev1b,
    rev2,
):
264
265
266
    # 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.
267
    waitprocs = []
268
269
270
271
272
    totalfiles = len(commonfiles)
    for idx, commonfile in enumerate(sorted(commonfiles)):
        path1a = os.path.join(tmproot, dir1a, commonfile)
        label1a = commonfile + rev1a
        if not os.path.isfile(path1a):
273
            path1a = pycompat.osdevnull
274

275
276
        path1b = b''
        label1b = b''
277
278
279
280
        if do3way:
            path1b = os.path.join(tmproot, dir1b, commonfile)
            label1b = commonfile + rev1b
            if not os.path.isfile(path1b):
281
                path1b = pycompat.osdevnull
282
283
284
285
286
287

        path2 = os.path.join(dir2root, dir2, commonfile)
        label2 = commonfile + rev2

        if confirm:
            # Prompt before showing this diff
288
            difffiles = _(b'diff %s (%d of %d)') % (
289
290
291
292
293
                commonfile,
                idx + 1,
                totalfiles,
            )
            responses = _(
294
295
296
297
298
                b'[Yns?]'
                b'$$ &Yes, show diff'
                b'$$ &No, skip this diff'
                b'$$ &Skip remaining diffs'
                b'$$ &? (display help)'
299
            )
300
            r = ui.promptchoice(b'%s %s' % (difffiles, responses))
301
            if r == 3:  # ?
302
303
                while r == 3:
                    for c, t in ui.extractchoices(responses)[1]:
304
305
                        ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
                    r = ui.promptchoice(b'%s %s' % (difffiles, responses))
306
            if r == 0:  # yes
307
                pass
308
            elif r == 1:  # no
309
                continue
310
            elif r == 2:  # skip
311
312
313
                break

        curcmdline = formatcmdline(
314
315
316
317
318
319
320
321
322
323
            cmdline,
            repo_root,
            do3way=do3way,
            parent1=path1a,
            plabel1=label1a,
            parent2=path1b,
            plabel2=label1b,
            child=path2,
            clabel=label2,
        )
324

325
326
327
328
329
330
331
        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.
332
            ui.debug(
333
                b'running %r in %s\n' % (pycompat.bytestr(curcmdline), tmproot)
334
            )
335
            ui.system(curcmdline, cwd=tmproot, blockedtag=b'extdiff')
336
337
338
339
        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.
340
            ui.debug(
341
                b'running %r in %s (backgrounded)\n'
342
343
                % (pycompat.bytestr(curcmdline), tmproot)
            )
344
345
            proc = _systembackground(curcmdline, cwd=tmproot)
            waitprocs.append(proc)
346

347
    if waitprocs:
348
        with ui.timeblockedsection(b'extdiff'):
349
350
351
            for proc in waitprocs:
                proc.wait()

352

353
def diffpatch(ui, repo, node1, node2, tmproot, matcher, cmdline):
354
    template = b'hg-%h.patch'
355
    # write patches to temporary files
356
357
358
    with formatter.nullformatter(ui, b'extdiff', {}) as fm:
        cmdutil.export(
            repo,
359
            [repo[node1].rev(), repo[node2].rev()],
360
361
362
363
            fm,
            fntemplate=repo.vfs.reljoin(tmproot, template),
            match=matcher,
        )
364
    label1 = cmdutil.makefilename(repo[node1], template)
365
    label2 = cmdutil.makefilename(repo[node2], template)
366
367
    file1 = repo.vfs.reljoin(tmproot, label1)
    file2 = repo.vfs.reljoin(tmproot, label2)
368
369
370
    cmdline = formatcmdline(
        cmdline,
        repo.root,
371
372
        # no 3way while comparing patches
        do3way=False,
373
374
375
376
377
378
        parent1=file1,
        plabel1=label1,
        # while comparing patches, there is no second parent
        parent2=None,
        plabel2=None,
        child=file2,
379
380
381
382
383
384
385
        clabel=label2,
    )
    ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
    ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
    return 1


386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
def diffrevs(
    ui,
    repo,
    node1a,
    node1b,
    node2,
    matcher,
    tmproot,
    cmdline,
    do3way,
    guitool,
    opts,
):

    subrepos = opts.get(b'subrepos')
    st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
    mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
    if do3way:
        stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
        mod_b, add_b, rem_b = (
            set(stb.modified),
            set(stb.added),
            set(stb.removed),
        )
    else:
        mod_b, add_b, rem_b = set(), set(), set()
    modadd = mod_a | add_a | mod_b | add_b
    common = modadd | rem_a | rem_b
    if not common:
        return 0
    # Always make a copy of node1a (and node1b, if applicable)
    dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
    dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[0]
    rev1a = b'@%d' % repo[node1a].rev()
    if do3way:
        dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
        dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot, subrepos)[0]
        rev1b = b'@%d' % repo[node1b].rev()
    else:
        dir1b = None
        rev1b = b''

    fnsandstat = []

    # If node2 in not the wc or there is >1 change, copy it
    dir2root = b''
    rev2 = b''
    if node2:
        dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
        rev2 = b'@%d' % repo[node2].rev()
    elif len(common) > 1:
        # we only actually need to get the files to copy back to
        # the working dir in this case (because the other cases
        # are: diffing 2 revisions or single file -- in which case
        # the file is already directly passed to the diff tool).
        dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot, subrepos)
    else:
        # This lets the diff tool open the changed file directly
        dir2 = b''
        dir2root = repo.root

    label1a = rev1a
    label1b = rev1b
    label2 = rev2

    # If only one change, diff the files instead of the directories
    # Handle bogus modifies correctly by checking if the files exist
    if len(common) == 1:
        common_file = util.localpath(common.pop())
        dir1a = os.path.join(tmproot, dir1a, common_file)
        label1a = common_file + rev1a
        if not os.path.isfile(dir1a):
            dir1a = pycompat.osdevnull
        if do3way:
            dir1b = os.path.join(tmproot, dir1b, common_file)
            label1b = common_file + rev1b
            if not os.path.isfile(dir1b):
                dir1b = pycompat.osdevnull
        dir2 = os.path.join(dir2root, dir2, common_file)
        label2 = common_file + rev2

    if not opts.get(b'per_file'):
        # Run the external tool on the 2 temp directories or the patches
        cmdline = formatcmdline(
            cmdline,
            repo.root,
            do3way=do3way,
            parent1=dir1a,
            plabel1=label1a,
            parent2=dir1b,
            plabel2=label1b,
            child=dir2,
            clabel=label2,
        )
        ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
        ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
    else:
        # Run the external tool once for each pair of files
        _runperfilediff(
            cmdline,
            repo.root,
            ui,
            guitool=guitool,
            do3way=do3way,
            confirm=opts.get(b'confirm'),
            commonfiles=common,
            tmproot=tmproot,
            dir1a=dir1a,
            dir1b=dir1b,
            dir2root=dir2root,
            dir2=dir2,
            rev1a=rev1a,
            rev1b=rev1b,
            rev2=rev2,
        )

    for copy_fn, working_fn, st in fnsandstat:
        cpstat = os.lstat(copy_fn)
        # Some tools copy the file and attributes, so mtime may not detect
        # all changes.  A size check will detect more cases, but not all.
        # The only certain way to detect every case is to diff all files,
        # which could be expensive.
        # copyfile() carries over the permission, so the mode check could
        # be in an 'elif' branch, but for the case where the file has
        # changed without affecting mtime or size.
        if (
            cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
            or cpstat.st_size != st.st_size
            or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
        ):
            ui.debug(
                b'file changed while diffing. '
                b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
            )
            util.copyfile(copy_fn, working_fn)

    return 1


525
def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
kiilerix's avatar
kiilerix committed
526
    '''Do the actual diff:
527
528
529
530
531
532

    - copy to a temp structure if diffing 2 internal revisions
    - copy to a temp structure if diffing working revision with
      another one and more than 1 file is changed
    - just invoke the diff for a single file in the working dir
    '''
533

534
    cmdutil.check_at_most_one_arg(opts, b'rev', b'change')
535
536
537
    revs = opts.get(b'rev')
    change = opts.get(b'change')
    do3way = b'$parent2' in cmdline
538

539
    if change:
540
541
        ctx2 = scmutil.revsingle(repo, change, None)
        ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
542
    else:
543
        ctx1a, ctx2 = scmutil.revpair(repo, revs)
544
        if not revs:
545
            ctx1b = repo[None].p2()
546
        else:
547
548
549
550
551
            ctx1b = repo[nullid]

    node1a = ctx1a.node()
    node1b = ctx1b.node()
    node2 = ctx2.node()
552
553
554
555
556

    # Disable 3-way merge if there is only one parent
    if do3way:
        if node1b == nullid:
            do3way = False
557

558
    matcher = scmutil.match(repo[node2], pats, opts)
559

560
    if opts.get(b'patch'):
561
        if opts.get(b'subrepos'):
562
            raise error.Abort(_(b'--patch cannot be used with --subrepos'))
563
        if opts.get(b'per_file'):
564
            raise error.Abort(_(b'--patch cannot be used with --per-file'))
565
        if node2 is None:
566
            raise error.Abort(_(b'--patch requires two revisions'))
567

568
    tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
569
    try:
570
        if opts.get(b'patch'):
571
            return diffpatch(ui, repo, node1a, node2, tmproot, matcher, cmdline)
572

573
574
575
576
577
578
579
580
581
582
583
584
585
        return diffrevs(
            ui,
            repo,
            node1a,
            node1b,
            node2,
            matcher,
            tmproot,
            cmdline,
            do3way,
            guitool,
            opts,
        )
586

587
    finally:
588
        ui.note(_(b'cleaning up temp directory\n'))
589
590
        shutil.rmtree(tmproot)

591

592
593
594
extdiffopts = (
    [
        (
595
596
597
598
599
600
601
602
603
604
605
            b'o',
            b'option',
            [],
            _(b'pass option to comparison program'),
            _(b'OPT'),
        ),
        (b'r', b'rev', [], _(b'revision'), _(b'REV')),
        (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')),
        (
            b'',
            b'per-file',
606
            False,
607
            _(b'compare each file instead of revision snapshots'),
608
609
        ),
        (
610
611
            b'',
            b'confirm',
612
            False,
613
            _(b'prompt user before each external program invocation'),
614
        ),
615
        (b'', b'patch', None, _(b'compare patches for two revisions')),
616
617
618
619
620
621
622
    ]
    + cmdutil.walkopts
    + cmdutil.subrepoopts
)


@command(
623
624
    b'extdiff',
    [(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),]
625
    + extdiffopts,
626
    _(b'hg extdiff [OPT]... [FILE]...'),
627
    helpcategory=command.CATEGORY_FILE_CONTENTS,
628
629
    inferrepo=True,
)
630
631
632
633
def extdiff(ui, repo, *pats, **opts):
    '''use external program to diff repository (or selected files)

    Show differences between revisions for the specified files, using
634
    an external program. The default program used is diff, with
Vadim Gelfer's avatar
Vadim Gelfer committed
635
636
    default options "-Npru".

637
    To select a different program, use the -p/--program option. The
638
639
640
641
    program will be passed the names of two directories to compare,
    unless the --per-file option is specified (see below). To pass
    additional options to the program, use -o/--option. These will be
    passed before the names of the directories or files to compare.
642

643
644
645
646
    When two revision arguments are given, then changes are shown
    between those revisions. If only one revision is specified then
    that revision is compared to the working directory, and, when no
    revisions are specified, the working directory files are compared
647
648
649
    to its parent.

    The --per-file option runs the external program repeatedly on each
650
651
652
653
654
655
656
    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.
657
658
659
660

    The --confirm option will prompt the user before each invocation of
    the external program. It is ignored if --per-file isn't specified.
    '''
661
    opts = pycompat.byteskwargs(opts)
662
663
    program = opts.get(b'program')
    option = opts.get(b'option')
664
    if not program:
665
666
667
        program = b'diff'
        option = option or [b'-Npru']
    cmdline = b' '.join(map(procutil.shellquote, [program] + option))
668
    return dodiff(ui, repo, cmdline, pats, opts)
669

670

671
class savedcmd(object):
672
    """use external program to diff repository (or selected files)
673
674

    Show differences between revisions for the specified files, using
675
676
677
    the following program::

        %(path)s
678
679
680
681
682
683
684
685

    When two revision arguments are given, then changes are shown
    between those revisions. If only one revision is specified then
    that revision is compared to the working directory, and, when no
    revisions are specified, the working directory files are compared
    to its parent.
    """

686
    def __init__(self, path, cmdline, isgui):
687
        # We can't pass non-ASCII through docstrings (and path is
688
689
690
        # in an unknown encoding anyway), but avoid double separators on
        # Windows
        docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
691
        self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))}
692
        self._cmdline = cmdline
693
        self._isgui = isgui
694
695

    def __call__(self, ui, repo, *pats, **opts):
696
        opts = pycompat.byteskwargs(opts)
697
        options = b' '.join(map(procutil.shellquote, opts[b'option']))
698
        if options:
699
            options = b' ' + options
700
701
702
703
        return dodiff(
            ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui
        )

704

705
def uisetup(ui):
706
    for cmd, path in ui.configitems(b'extdiff'):
707
        path = util.expandpath(path)
708
        if cmd.startswith(b'cmd.'):
Brendan Cully's avatar
Brendan Cully committed
709
            cmd = cmd[4:]
710
            if not path:
711
                path = procutil.findexe(cmd)
712
713
                if path is None:
                    path = filemerge.findexternaltool(ui, cmd) or cmd
714
            diffopts = ui.config(b'extdiff', b'opts.' + cmd)
715
            cmdline = procutil.shellquote(path)
716
            if diffopts:
717
718
719
                cmdline += b' ' + diffopts
            isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
        elif cmd.startswith(b'opts.') or cmd.startswith(b'gui.'):
Brendan Cully's avatar
Brendan Cully committed
720
721
722
            continue
        else:
            if path:
723
724
                # case "cmd = path opts"
                cmdline = path
725
                diffopts = len(pycompat.shlexsplit(cmdline)) > 1
Brendan Cully's avatar
Brendan Cully committed
726
            else:
727
                # case "cmd ="
728
                path = procutil.findexe(cmd)
729
730
                if path is None:
                    path = filemerge.findexternaltool(ui, cmd) or cmd
731
                cmdline = procutil.shellquote(path)
732
                diffopts = False
733
            isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
734
        # look for diff arguments in [diff-tools] then [merge-tools]
735
        if not diffopts:
736
737
            key = cmd + b'.diffargs'
            for section in (b'diff-tools', b'merge-tools'):
738
739
                args = ui.config(section, key)
                if args:
740
                    cmdline += b' ' + args
741
                    if isgui is None:
742
                        isgui = ui.configbool(section, cmd + b'.gui') or False
743
                    break
744
745
746
        command(
            cmd,
            extdiffopts[:],
747
            _(b'hg %s [OPTION]... [FILE]...') % cmd,
748
749
750
751
            helpcategory=command.CATEGORY_FILE_CONTENTS,
            inferrepo=True,
        )(savedcmd(path, cmdline, isgui))

752
753
754

# tell hggettext to extract docstrings from these functions:
i18nfunctions = [savedcmd]