visdiff.py 22.7 KB
Newer Older
1
2
3
4
5
6
7
# visdiff.py - launch external visual diff tools
#
# Copyright 2009 Steve Borho <steve@borho.org>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

8
9
from __future__ import absolute_import

10
import os
11
import re
12
import stat
13
import subprocess
14
15
import threading

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from .qtcore import (
    QTimer,
    pyqtSlot,
)
from .qtgui import (
    QComboBox,
    QDialog,
    QDialogButtonBox,
    QHBoxLayout,
    QKeySequence,
    QLabel,
    QListWidget,
    QMessageBox,
    QShortcut,
    QVBoxLayout,
)
32

33
34
35
36
from mercurial import (
    copies,
    error,
    match,
37
    pycompat,
38
39
40
    scmutil,
    util,
)
41
42
from mercurial.utils import (
    procutil,
43
    stringutil,
44
)
45

46
47
48
from ..util import hglib
from ..util.i18n import _
from . import qtlib
49
50

# Match parent2 first, so 'parent1?' will match both parent1 and parent
kiilerix's avatar
kiilerix committed
51
_regex = b'\$(parent2|parent1?|child|plabel1|plabel2|clabel|repo|phash1|phash2|chash)'
52
53
54

_nonexistant = _('[non-existant]')

55
56
57
58
# This global counter is incremented for each visual diff done in a session
# It ensures that the names for snapshots created do not collide.
_diffCount = 0

59
60
61
62
63
def snapshotset(repo, ctxs, sa, sb, copies, copyworkingdir = False):
    '''snapshot files from parent-child set of revisions'''
    ctx1a, ctx1b, ctx2 = ctxs
    mod_a, add_a, rem_a = sa
    mod_b, add_b, rem_b = sb
64
65
66
67

    global _diffCount
    _diffCount += 1

68
69
70
71
72
73
74
75
    if copies:
        sources = set(copies.values())
    else:
        sources = set()

    # Always make a copy of ctx1a
    files1a = sources | mod_a | rem_a | ((mod_b | add_b) - add_a)
    dir1a, fns_mtime1a = snapshot(repo, files1a, ctx1a)
76
    label1a = b'@%d:%s' % (ctx1a.rev(), ctx1a)
77
78
79
80
81

    # Make a copy of ctx1b if relevant
    if ctx1b:
        files1b = sources | mod_b | rem_b | ((mod_a | add_a) - add_b)
        dir1b, fns_mtime1b = snapshot(repo, files1b, ctx1b)
82
        label1b = b'@%d:%s' % (ctx1b.rev(), ctx1b)
83
84
85
    else:
        dir1b = None
        fns_mtime1b = []
86
        label1b = b''
87
88
89
90
91
92
93
94
95
96

    # Either make a copy of ctx2, or use working dir directly if relevant.
    files2 = mod_a | add_a | mod_b | add_b
    if ctx2.rev() is None:
        if copyworkingdir:
            dir2, fns_mtime2 = snapshot(repo, files2, ctx2)
        else:
            dir2 = repo.root
            fns_mtime2 = []
        # If ctx2 is working copy, use empty label.
97
        label2 = b''
98
99
    else:
        dir2, fns_mtime2 = snapshot(repo, files2, ctx2)
100
        label2 = b'@%d:%s' % (ctx2.rev(), ctx2)
101
102
103
104
105
106

    dirs = [dir1a, dir1b, dir2]
    labels = [label1a, label1b, label2]
    fns_and_mtimes = [fns_mtime1a, fns_mtime1b, fns_mtime2]
    return dirs, labels, fns_and_mtimes

Steve Borho's avatar
Steve Borho committed
107
def snapshot(repo, files, ctx):
kiilerix's avatar
kiilerix committed
108
109
110
    '''snapshot repo files as of some revision, returning a tuple with the
    created temporary snapshot dir and tuples of file info if using working
    copy.'''
111
112
    dirname = os.path.basename(repo.root) or b'root'
    dirname += b'.%d' % _diffCount
113
    if ctx.rev() is not None:
114
        dirname += b'.%d' % ctx.rev()
Steve Borho's avatar
Steve Borho committed
115
    base = os.path.join(qtlib.gettempdir(), dirname)
116
    fns_and_mtime = []
117
    if not os.path.exists(base):
118
        os.makedirs(base)
119
    for fn in files:
kiilerix's avatar
kiilerix committed
120
        assert isinstance(fn, bytes), repr(fn)
121
        wfn = util.pconvert(fn)
122
        if wfn not in ctx:
123
124
125
            # File doesn't exist; could be a bogus modify
            continue
        dest = os.path.join(base, wfn)
126
127
128
        if os.path.exists(dest):
            # File has already been snapshot
            continue
129
        destdir = os.path.dirname(dest)
130
131
132
        try:
            if not os.path.isdir(destdir):
                os.makedirs(destdir)
133
134
            fctx = ctx[wfn]
            data = repo.wwritedata(wfn, fctx.data())
135
136
            with open(dest, 'wb') as f:
                f.write(data)
kiilerix's avatar
kiilerix committed
137
            if b'x' in fctx.flags():
138
                util.setflags(dest, False, True)
139
140
141
142
143
144
145
146
            if ctx.rev() is None:
                fns_and_mtime.append((dest, repo.wjoin(fn), 
                                    os.lstat(dest).st_mtime))
            else:
                # Make file read/only, to indicate it's static (archival) nature
                os.chmod(dest, stat.S_IREAD)
        except EnvironmentError:
            pass
147
148
149
150
    return base, fns_and_mtime

def launchtool(cmd, opts, replace, block):
    def quote(match):
kiilerix's avatar
kiilerix committed
151
        key = pycompat.sysstr(match.group()[1:])
152
        return procutil.shellquote(replace[key])
153
    if isinstance(cmd, pycompat.unicode):
154
155
156
        cmd = hglib.fromunicode(cmd)
    lopts = []
    for opt in opts:
157
        if isinstance(opt, pycompat.unicode):
158
159
160
            lopts.append(hglib.fromunicode(opt))
        else:
            lopts.append(opt)
kiilerix's avatar
kiilerix committed
161
    args = b' '.join(lopts)
162
    args = re.sub(_regex, quote, args)
kiilerix's avatar
kiilerix committed
163
    cmdline = procutil.shellquote(cmd) + b' ' + args
164
    cmdline = procutil.quotecommand(cmdline)
165
    try:
166
        proc = subprocess.Popen(procutil.tonativestr(cmdline), shell=True,
167
                                creationflags=qtlib.openflags,
168
169
170
171
172
                                stderr=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stdin=subprocess.PIPE)
        if block:
            proc.communicate()
173
    except (OSError, EnvironmentError) as e:
174
        QMessageBox.warning(None,
175
                _('Tool launch failure'),
176
                _('%s : %s') % (hglib.tounicode(cmd), hglib.tounicode(str(e))))
177
178
179
180
181

def filemerge(ui, fname, patchedfname):
    'Launch the preferred visual diff tool for two text files'
    detectedtools = hglib.difftools(ui)
    if not detectedtools:
182
        QMessageBox.warning(None,
183
184
185
186
187
188
189
190
191
192
193
194
                _('No diff tool found'),
                _('No visual diff tools were detected'))
        return None
    preferred = besttool(ui, detectedtools)
    diffcmd, diffopts, mergeopts = detectedtools[preferred]
    replace = dict(parent=fname, parent1=fname,
                   plabel1=fname + _('[working copy]'),
                   repo='', phash1='', phash2='', chash='',
                   child=patchedfname, clabel=_('[original]'))
    launchtool(diffcmd, diffopts, replace, True)


195
def besttool(ui, tools, force=None):
196
    'Select preferred or highest priority tool from dictionary'
197
    preferred = force or ui.config(b'tortoisehg', b'vdiff') or \
198
                         ui.config(b'ui', b'merge')
199
200
201
202
    if preferred and preferred in tools:
        return preferred
    pris = []
    for t in tools.keys():
203
        try:
204
            p = ui.configint(b'merge-tools', t + b'.priority')
205
        except error.ConfigError as inst:
206
            ui.warn(b'visdiff: %s\n' % stringutil.forcebytestr(inst))
207
            p = 0
208
209
210
211
212
213
        pris.append((-p, t))
    tools = sorted(pris)
    return tools[0][1]


def visualdiff(ui, repo, pats, opts):
214
    revs = opts.get('rev', [])
215
216
217
218
219
    change = opts.get('change')

    try:
        ctx1b = None
        if change:
220
            # TODO: figure out what's the expect type
221
222
223
            if isinstance(change, pycompat.unicode):
                change = hglib.fromunicode(change)
            if isinstance(change, bytes):
224
225
226
                ctx2 = scmutil.revsymbol(repo, change)
            else:
                ctx2 = repo[change]
227
228
229
230
231
232
            p = ctx2.parents()
            if len(p) > 1:
                ctx1a, ctx1b = p
            else:
                ctx1a = p[0]
        else:
233
234
            n1, n2 = scmutil.revpair(repo, [hglib.fromunicode(rev)
                                            for rev in revs])
235
236
237
238
239
            ctx1a, ctx2 = repo[n1], repo[n2]
            p = ctx2.parents()
            if not revs and len(p) > 1:
                ctx1b = p[1]
    except (error.LookupError, error.RepoError):
240
        QMessageBox.warning(None,
241
242
243
244
                       _('Unable to find changeset'),
                       _('You likely need to refresh this application'))
        return None

245
    pats = scmutil.expandpats(pats)
246
    m = match.match(repo.root, b'', pats, None, None, b'relpath')
247
    n2 = ctx2.node()
248
249
250
251
252
253

    def _status(ctx):
        status = repo.status(ctx.node(), n2, m)
        return status.modified, status.added, status.removed

    mod_a, add_a, rem_a = pycompat.maplist(set, _status(ctx1a))
254
    if ctx1b:
255
        mod_b, add_b, rem_b = pycompat.maplist(set, _status(ctx1b))
256
        cpy = copies.mergecopies(repo, ctx1a, ctx1b, ctx1a.ancestor(ctx1b))[0]
257
258
        if util.safehasattr(copies, "branch_copies"):
            cpy = cpy.copy  # hg 5.4 >= 7f8bdee0034e
259
    else:
260
        cpy = copies.pathcopies(ctx1a, ctx2)
261
262
263
264
        mod_b, add_b, rem_b = set(), set(), set()
    MA = mod_a | add_a | mod_b | add_b
    MAR = MA | rem_a | rem_b
    if not MAR:
265
        QMessageBox.information(None,
266
267
268
269
270
271
                _('No file changes'),
                _('There are no file changes to view'))
        return None

    detectedtools = hglib.difftools(repo.ui)
    if not detectedtools:
272
        QMessageBox.warning(None,
273
274
275
276
                _('No diff tool found'),
                _('No visual diff tools were detected'))
        return None

277
    preferred = besttool(repo.ui, detectedtools, opts.get('tool'))
278
279
280

    # Build tool list based on diff-patterns matches
    toollist = set()
281
    patterns = repo.ui.configitems(b'diff-patterns')
282
283
284
    patterns = [(p, t) for p,t in patterns if t in detectedtools]
    for path in MAR:
        for pat, tool in patterns:
285
            mf = match.match(repo.root, b'', [pat])
286
287
288
289
290
291
            if mf(path):
                toollist.add(tool)
                break
        else:
            toollist.add(preferred)

292
    cto = list(cpy.keys())
293
    for path in MAR:
294
        if path in cto:
295
296
297
298
            hascopies = True
            break
    else:
        hascopies = False
299
    force = repo.ui.configbool(b'tortoisehg', b'forcevdiffwin')
300
    if len(toollist) > 1 or (hascopies and len(MAR) > 1) or force:
301
302
303
        usewin = True
    else:
        preferred = toollist.pop()
304
305
306
        dirdiff = repo.ui.configbool(b'merge-tools', preferred + b'.dirdiff')
        dir3diff = repo.ui.configbool(b'merge-tools', preferred + b'.dir3diff')
        usewin = repo.ui.configbool(b'merge-tools', preferred + b'.usewin')
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
        if not usewin and len(MAR) > 1:
            if ctx1b is not None:
                usewin = not dir3diff
            else:
                usewin = not dirdiff
    if usewin:
        # Multiple required tools, or tool does not support directory diffs
        sa = [mod_a, add_a, rem_a]
        sb = [mod_b, add_b, rem_b]
        dlg = FileSelectionDialog(repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy)
        return dlg

    # We can directly use the selected tool, without a visual diff window
    diffcmd, diffopts, mergeopts = detectedtools[preferred]

    # Disable 3-way merge if there is only one parent or no tool support
    do3way = False
    if ctx1b:
        if mergeopts:
            do3way = True
            args = mergeopts
        else:
            args = diffopts
            if str(ctx1b.rev()) in revs:
                ctx1a = ctx1b
    else:
        args = diffopts

Steve Borho's avatar
Steve Borho committed
335
    def dodiff():
336
337
        assert not (hascopies and len(MAR) > 1), \
                'dodiff cannot handle copies when diffing dirs'
338

339
340
341
342
343
344
        sa = [mod_a, add_a, rem_a]
        sb = [mod_b, add_b, rem_b]
        ctxs = [ctx1a, ctx1b, ctx2]

        # If more than one file, diff on working dir copy.
        copyworkingdir = len(MAR) > 1
345
        dirs, labels, fns_and_mtimes = snapshotset(repo, ctxs, sa, sb, cpy,
346
347
348
349
350
                                                   copyworkingdir)
        dir1a, dir1b, dir2 = dirs
        label1a, label1b, label2 = labels
        fns_and_mtime = fns_and_mtimes[2]

351
        if len(MAR) > 1 and label2 == b'':
352
            label2 = b'working files'
353
354

        def getfile(fname, dir, label):
Steve Borho's avatar
Steve Borho committed
355
            file = os.path.join(qtlib.gettempdir(), dir, fname)
356
357
            if os.path.isfile(file):
                return fname+label, file
358
            nullfile = os.path.join(qtlib.gettempdir(), b'empty')
kiilerix's avatar
kiilerix committed
359
            fp = open(nullfile, 'wb')
360
            fp.close()
361
362
            return (hglib.fromunicode(_nonexistant, 'replace') + label,
                    nullfile)
363
364
365
366

        # If only one change, diff the files instead of the directories
        # Handle bogus modifies correctly by checking if the files exist
        if len(MAR) == 1:
367
368
            file2 = MAR.pop()
            file2local = util.localpath(file2)
369
370
371
372
373
            if file2 in cto:
                file1 = util.localpath(cpy[file2])
            else:
                file1 = file2
            label1a, dir1a = getfile(file1, dir1a, label1a)
374
            if do3way:
375
                label1b, dir1b = getfile(file1, dir1b, label1b)
376
            label2, dir2 = getfile(file2local, dir2, label2)
377
        if do3way:
378
379
380
            label1a += b'[local]'
            label1b += b'[other]'
            label2 += b'[merged]'
381

382
        repoagent = repo._pyqtobj  # TODO
383
384
385
        replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b,
                       plabel1=label1a, plabel2=label1b,
                       phash1=str(ctx1a), phash2=str(ctx1b),
386
                       repo=hglib.fromunicode(repoagent.displayName()),
387
388
389
390
391
                       clabel=label2, child=dir2, chash=str(ctx2))
        launchtool(diffcmd, args, replace, True)

        # detect if changes were made to mirrored working files
        for copy_fn, working_fn, mtime in fns_and_mtime:
392
            try:
393
                if os.lstat(copy_fn).st_mtime != mtime:
394
395
396
                    ui.debug(b'file changed while diffing. '
                             b'Overwriting: %s (src: %s)\n'
                             % (working_fn, copy_fn))
397
398
399
                    util.copyfile(copy_fn, working_fn)
            except EnvironmentError:
                pass # Ignore I/O errors or missing files
400
401
402

    def dodiffwrapper():
        try:
Steve Borho's avatar
Steve Borho committed
403
            dodiff()
404
        finally:
Steve Borho's avatar
Steve Borho committed
405
            # cleanup happens atexit
406
            ui.note(b'cleaning up temp directory\n')
407
408
409
410
411
412
413
414
415
416

    if opts.get('mainapp'):
        dodiffwrapper()
    else:
        # We are not the main application, so this must be done in a
        # background thread
        thread = threading.Thread(target=dodiffwrapper, name='visualdiff')
        thread.setDaemon(True)
        thread.start()

417
class FileSelectionDialog(QDialog):
418
419
420
    'Dialog for selecting visual diff candidates'
    def __init__(self, repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy):
        'Initialize the Dialog'
421
        QDialog.__init__(self)
422

423
        self.setWindowIcon(qtlib.geticon('visualdiff'))
424
425
426
427

        if ctx2.rev() is None:
            title = _('working changes')
        elif ctx1a == ctx2.parents()[0]:
428
            title = _('changeset %d:%s') % (ctx2.rev(), ctx2)
429
        else:
430
431
            title = _('revisions %d:%s to %d:%s') \
                % (ctx1a.rev(), ctx1a, ctx2.rev(), ctx2)
432
433
434
        title = _('Visual Diffs - ') + title
        if pats:
            title += _(' filtered')
435
        self.setWindowTitle(title)
436

437
        self.resize(650, 250)
438
439
        repoagent = repo._pyqtobj  # TODO
        self.reponame = hglib.fromunicode(repoagent.displayName())
440
441

        self.ctxs = (ctx1a, ctx1b, ctx2)
442
        self.filesets = (sa, sb)
443
        self.copies = cpy
444
445
        self.repo = repo
        self.curFile = None
446

447
        layout = QVBoxLayout()
448
449
        self.setLayout(layout)

450
451
        lbl = QLabel(_('Temporary files are removed when this dialog '
                       'is closed'))
452
453
        layout.addWidget(lbl)

454
        list = QListWidget()
455
456
        layout.addWidget(list)
        self.list = list
457
        list.itemActivated.connect(self.itemActivated)
458
459
460
461

        tools = hglib.difftools(repo.ui)
        preferred = besttool(repo.ui, tools)
        self.diffpath, self.diffopts, self.mergeopts = tools[preferred]
462
        self.tools = tools
463
        self.preferred = preferred
464
465

        if len(tools) > 1:
466
            hbox = QHBoxLayout()
467
468
            combo = QComboBox()
            lbl = QLabel(_('Select Tool:'))
469
470
471
472
            lbl.setBuddy(combo)
            hbox.addWidget(lbl)
            hbox.addWidget(combo, 1)
            layout.addLayout(hbox)
473
            for i, name in enumerate(tools.keys()):
474
                combo.addItem(hglib.tounicode(name))
475
476
477
478
                if name == preferred:
                    defrow = i
            combo.setCurrentIndex(defrow)

479
            list.currentRowChanged.connect(self.updateToolSelection)
480
            combo.currentIndexChanged[str].connect(self.onToolSelected)
481
            self.toolCombo = combo
482

483
        BB = QDialogButtonBox
484
485
        bb = BB()
        layout.addWidget(bb)
486
487
488
489
490
491

        if ctx2.rev() is None:
            pass
            # Do not offer directory diffs when the working directory
            # is being referenced directly
        elif ctx1b:
492
493
494
495
496
497
            self.p1button = bb.addButton(_('Dir diff to p1'), BB.ActionRole)
            self.p1button.pressed.connect(self.p1dirdiff)
            self.p2button = bb.addButton(_('Dir diff to p2'), BB.ActionRole)
            self.p2button.pressed.connect(self.p2dirdiff)
            self.p3button = bb.addButton(_('3-way dir diff'), BB.ActionRole)
            self.p3button.pressed.connect(self.threewaydirdiff)
498
        else:
499
500
            self.dbutton = bb.addButton(_('Directory diff'), BB.ActionRole)
            self.dbutton.pressed.connect(self.p1dirdiff)
501

502
        self.updateDiffButtons(preferred)
503

504
        QShortcut(QKeySequence('CTRL+D'), self.list, self.activateCurrent)
505
        QTimer.singleShot(0, self.fillmodel)
506

507
508
509
510
    @pyqtSlot()
    def fillmodel(self):
        repo = self.repo
        sa, sb = self.filesets
511
        self.dirs, self.revs = snapshotset(repo, self.ctxs, sa, sb, self.copies)[:2]
512
513
514
515
516
517
518
519
520
521

        def get_status(file, mod, add, rem):
            if file in mod:
                return 'M'
            if file in add:
                return 'A'
            if file in rem:
                return 'R'
            return ' '

522
        mod_a, add_a, rem_a = sa
523
        for f in sorted(mod_a | add_a | rem_a):
524
            status = get_status(f, mod_a, add_a, rem_a)
525
            row = '%s %s' % (status, hglib.tounicode(f))
526
            self.list.addItem(row)
527

528
    @pyqtSlot(str)
529
    def onToolSelected(self, tool):
530
        'user selected a tool from the tool combo'
531
        tool = hglib.fromunicode(tool)
532
        assert tool in self.tools, tool
533
534
        self.diffpath, self.diffopts, self.mergeopts = self.tools[tool]
        self.updateDiffButtons(tool)
535

536
537
    @pyqtSlot(int)
    def updateToolSelection(self, row):
538
        'user selected a file, pick an appropriate tool from combo'
539
540
541
542
        if row == -1:
            return

        repo = self.repo
543
        patterns = repo.ui.configitems(b'diff-patterns')
544
        patterns = [(p, t) for p,t in patterns if t in self.tools]
545

546
547
548
549
550
        fname = self.list.item(row).text()[2:]
        fname = hglib.fromunicode(fname)
        if self.curFile == fname:
            return
        self.curFile = fname
551
        for pat, tool in patterns:
552
            mf = match.match(repo.root, b'', [pat])
553
554
555
556
            if mf(fname):
                selected = tool
                break
        else:
557
            selected = self.preferred
558
        for i, name in enumerate(self.tools.keys()):
559
            if name == selected:
560
                self.toolCombo.setCurrentIndex(i)
561

562
563
564
565
566
567
    def activateCurrent(self):
        'CTRL+D has been pressed'
        row = self.list.currentRow()
        if row >= 0:
            self.launch(self.list.item(row).text()[2:])

568
569
570
    def itemActivated(self, item):
        'A QListWidgetItem has been activated'
        self.launch(item.text()[2:])
571

572
    def updateDiffButtons(self, tool):
573
        # hg>=4.4: configbool() may return None as the default is set to None
574
        if hasattr(self, 'p1button'):
575
576
            d2 = self.repo.ui.configbool(b'merge-tools', tool + b'.dirdiff')
            d3 = self.repo.ui.configbool(b'merge-tools', tool + b'.dir3diff')
577
578
579
            self.p1button.setEnabled(bool(d2))
            self.p2button.setEnabled(bool(d2))
            self.p3button.setEnabled(bool(d3))
580
        elif hasattr(self, 'dbutton'):
581
            d2 = self.repo.ui.configbool(b'merge-tools', tool + b'.dirdiff')
582
            self.dbutton.setEnabled(bool(d2))
583

584
585
    def launch(self, fname):
        fname = hglib.fromunicode(fname)
586
587
588
589
590
591
592
593
594
595
596
597
598
599
        source = self.copies.get(fname, None)
        dir1a, dir1b, dir2 = self.dirs
        rev1a, rev1b, rev2 = self.revs
        ctx1a, ctx1b, ctx2 = self.ctxs

        def getfile(ctx, dir, fname, source):
            m = ctx.manifest()
            if fname in m:
                path = os.path.join(dir, util.localpath(fname))
                return fname, path
            elif source and source in m:
                path = os.path.join(dir, util.localpath(source))
                return source, path
            else:
600
                nullfile = os.path.join(qtlib.gettempdir(), b'empty')
601
602
                fp = open(nullfile, 'w')
                fp.close()
603
                return hglib.fromunicode(_nonexistant, 'replace'), nullfile
604
605
606
607
608

        local, file1a = getfile(ctx1a, dir1a, fname, source)
        if ctx1b:
            other, file1b = getfile(ctx1b, dir1b, fname, source)
        else:
Steve Borho's avatar
Steve Borho committed
609
            other, file1b = fname, None
610
611
612
613
614
615
        fname, file2 = getfile(ctx2, dir2, fname, None)

        label1a = local+rev1a
        label1b = other+rev1b
        label2 = fname+rev2
        if ctx1b:
616
617
618
            label1a += b'[local]'
            label1b += b'[other]'
            label2 += b'[merged]'
619
620
621
622
623
624
625
626
627
628

        # Function to quote file/dir names in the argument string
        replace = dict(parent=file1a, parent1=file1a, plabel1=label1a,
                       parent2=file1b, plabel2=label1b,
                       repo=self.reponame,
                       phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2),
                       clabel=label2, child=file2)
        args = ctx1b and self.mergeopts or self.diffopts
        launchtool(self.diffpath, args, replace, False)

629
    def p1dirdiff(self):
630
631
632
633
634
635
636
637
638
639
        dir1a, dir1b, dir2 = self.dirs
        rev1a, rev1b, rev2 = self.revs
        ctx1a, ctx1b, ctx2 = self.ctxs

        replace = dict(parent=dir1a, parent1=dir1a, plabel1=rev1a,
                       repo=self.reponame,
                       phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2),
                       parent2='', plabel2='', clabel=rev2, child=dir2)
        launchtool(self.diffpath, self.diffopts, replace, False)

640
    def p2dirdiff(self):
641
642
643
644
645
646
647
648
649
650
        dir1a, dir1b, dir2 = self.dirs
        rev1a, rev1b, rev2 = self.revs
        ctx1a, ctx1b, ctx2 = self.ctxs

        replace = dict(parent=dir1b, parent1=dir1b, plabel1=rev1b,
                       repo=self.reponame,
                       phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2),
                       parent2='', plabel2='', clabel=rev2, child=dir2)
        launchtool(self.diffpath, self.diffopts, replace, False)

651
    def threewaydirdiff(self):
652
653
654
655
656
657
658
659
660
        dir1a, dir1b, dir2 = self.dirs
        rev1a, rev1b, rev2 = self.revs
        ctx1a, ctx1b, ctx2 = self.ctxs

        replace = dict(parent=dir1a, parent1=dir1a, plabel1=rev1a,
                       repo=self.reponame,
                       phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2),
                       parent2=dir1b, plabel2=rev1b, clabel=dir2, child=rev2)
        launchtool(self.diffpath, self.mergeopts, replace, False)