visdiff.py 22.2 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
43
from mercurial.utils import (
    procutil,
)
44

45
46
47
from ..util import hglib
from ..util.i18n import _
from . import qtlib
48
49
50
51
52
53

# Match parent2 first, so 'parent1?' will match both parent1 and parent
_regex = '\$(parent2|parent1?|child|plabel1|plabel2|clabel|repo|phash1|phash2|chash)'

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

54
55
56
57
# 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

58
59
60
61
62
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
63
64
65
66

    global _diffCount
    _diffCount += 1

67
68
69
70
71
72
73
74
    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)
75
    label1a = '@%d:%s' % (ctx1a.rev(), ctx1a)
76
77
78
79
80

    # 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)
81
        label1b = '@%d:%s' % (ctx1b.rev(), ctx1b)
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
    else:
        dir1b = None
        fns_mtime1b = []
        label1b = ''

    # 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.
        label2 = ''
    else:
        dir2, fns_mtime2 = snapshot(repo, files2, ctx2)
99
        label2 = '@%d:%s' % (ctx2.rev(), ctx2)
100
101
102
103
104
105

    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
106
def snapshot(repo, files, ctx):
107
108
    '''snapshot files as of some revision'''
    dirname = os.path.basename(repo.root) or 'root'
109
    dirname += '.%d' % _diffCount
110
    if ctx.rev() is not None:
111
        dirname += '.%d' % ctx.rev()
Steve Borho's avatar
Steve Borho committed
112
    base = os.path.join(qtlib.gettempdir(), dirname)
113
    fns_and_mtime = []
114
    if not os.path.exists(base):
115
        os.makedirs(base)
116
117
118
119
120
121
    for fn in files:
        wfn = util.pconvert(fn)
        if not wfn in ctx:
            # File doesn't exist; could be a bogus modify
            continue
        dest = os.path.join(base, wfn)
122
123
124
        if os.path.exists(dest):
            # File has already been snapshot
            continue
125
        destdir = os.path.dirname(dest)
126
127
128
        try:
            if not os.path.isdir(destdir):
                os.makedirs(destdir)
129
130
            fctx = ctx[wfn]
            data = repo.wwritedata(wfn, fctx.data())
131
132
133
            f = open(dest, 'wb')
            f.write(data)
            f.close()
134
135
            if 'x' in fctx.flags():
                util.setflags(dest, False, True)
136
137
138
139
140
141
142
143
            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
144
145
146
147
148
    return base, fns_and_mtime

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

def filemerge(ui, fname, patchedfname):
    'Launch the preferred visual diff tool for two text files'
    detectedtools = hglib.difftools(ui)
    if not detectedtools:
179
        QMessageBox.warning(None,
180
181
182
183
184
185
186
187
188
189
190
191
                _('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)


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


def visualdiff(ui, repo, pats, opts):
211
    revs = opts.get('rev', [])
212
213
214
215
216
    change = opts.get('change')

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

239
    pats = scmutil.expandpats(pats)
240
241
    m = match.match(repo.root, '', pats, None, None, 'relpath')
    n2 = ctx2.node()
242
243
244
    mod_a, add_a, rem_a = pycompat.maplist(set,
                                           repo.status(ctx1a.node(),
                                                       n2, m)[:3])
245
    if ctx1b:
246
247
248
        mod_b, add_b, rem_b = pycompat.maplist(set,
                                               repo.status(ctx1b.node(),
                                                           n2, m)[:3])
249
        cpy = copies.mergecopies(repo, ctx1a, ctx1b, ctx1a.ancestor(ctx1b))[0]
250
    else:
251
        cpy = copies.pathcopies(ctx1a, ctx2)
252
253
254
255
        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:
256
        QMessageBox.information(None,
257
258
259
260
261
262
                _('No file changes'),
                _('There are no file changes to view'))
        return None

    detectedtools = hglib.difftools(repo.ui)
    if not detectedtools:
263
        QMessageBox.warning(None,
264
265
266
267
                _('No diff tool found'),
                _('No visual diff tools were detected'))
        return None

268
    preferred = besttool(repo.ui, detectedtools, opts.get('tool'))
269
270
271
272
273
274
275
276
277
278
279
280
281
282

    # Build tool list based on diff-patterns matches
    toollist = set()
    patterns = repo.ui.configitems('diff-patterns')
    patterns = [(p, t) for p,t in patterns if t in detectedtools]
    for path in MAR:
        for pat, tool in patterns:
            mf = match.match(repo.root, '', [pat])
            if mf(path):
                toollist.add(tool)
                break
        else:
            toollist.add(preferred)

283
    cto = list(cpy.keys())
284
    for path in MAR:
285
        if path in cto:
286
287
288
289
290
            hascopies = True
            break
    else:
        hascopies = False
    force = repo.ui.configbool('tortoisehg', 'forcevdiffwin')
291
    if len(toollist) > 1 or (hascopies and len(MAR) > 1) or force:
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
        usewin = True
    else:
        preferred = toollist.pop()
        dirdiff = repo.ui.configbool('merge-tools', preferred + '.dirdiff')
        dir3diff = repo.ui.configbool('merge-tools', preferred + '.dir3diff')
        usewin = repo.ui.configbool('merge-tools', preferred + '.usewin')
        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
326
    def dodiff():
327
328
        assert not (hascopies and len(MAR) > 1), \
                'dodiff cannot handle copies when diffing dirs'
329

330
331
332
333
334
335
        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
336
        dirs, labels, fns_and_mtimes = snapshotset(repo, ctxs, sa, sb, cpy,
337
338
339
340
341
342
                                                   copyworkingdir)
        dir1a, dir1b, dir2 = dirs
        label1a, label1b, label2 = labels
        fns_and_mtime = fns_and_mtimes[2]

        if len(MAR) > 1 and label2 == '':
343
344
345
            label2 = 'working files'

        def getfile(fname, dir, label):
Steve Borho's avatar
Steve Borho committed
346
            file = os.path.join(qtlib.gettempdir(), dir, fname)
347
348
            if os.path.isfile(file):
                return fname+label, file
Steve Borho's avatar
Steve Borho committed
349
            nullfile = os.path.join(qtlib.gettempdir(), 'empty')
350
351
            fp = open(nullfile, 'w')
            fp.close()
352
353
            return (hglib.fromunicode(_nonexistant, 'replace') + label,
                    nullfile)
354
355
356
357

        # 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:
358
359
            file2 = MAR.pop()
            file2local = util.localpath(file2)
360
361
362
363
364
            if file2 in cto:
                file1 = util.localpath(cpy[file2])
            else:
                file1 = file2
            label1a, dir1a = getfile(file1, dir1a, label1a)
365
            if do3way:
366
                label1b, dir1b = getfile(file1, dir1b, label1b)
367
            label2, dir2 = getfile(file2local, dir2, label2)
368
369
370
371
372
        if do3way:
            label1a += '[local]'
            label1b += '[other]'
            label2 += '[merged]'

373
        repoagent = repo._pyqtobj  # TODO
374
375
376
        replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b,
                       plabel1=label1a, plabel2=label1b,
                       phash1=str(ctx1a), phash2=str(ctx1b),
377
                       repo=hglib.fromunicode(repoagent.displayName()),
378
379
380
381
382
                       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:
383
            try:
384
                if os.lstat(copy_fn).st_mtime != mtime:
385
386
387
388
389
                    ui.debug('file changed while diffing. '
                            'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
                    util.copyfile(copy_fn, working_fn)
            except EnvironmentError:
                pass # Ignore I/O errors or missing files
390
391
392

    def dodiffwrapper():
        try:
Steve Borho's avatar
Steve Borho committed
393
            dodiff()
394
        finally:
Steve Borho's avatar
Steve Borho committed
395
            # cleanup happens atexit
396
            ui.note('cleaning up temp directory\n')
397
398
399
400
401
402
403
404
405
406

    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()

407
class FileSelectionDialog(QDialog):
408
409
410
    'Dialog for selecting visual diff candidates'
    def __init__(self, repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy):
        'Initialize the Dialog'
411
        QDialog.__init__(self)
412

413
        self.setWindowIcon(qtlib.geticon('visualdiff'))
414
415
416
417

        if ctx2.rev() is None:
            title = _('working changes')
        elif ctx1a == ctx2.parents()[0]:
418
            title = _('changeset %d:%s') % (ctx2.rev(), ctx2)
419
        else:
420
421
            title = _('revisions %d:%s to %d:%s') \
                % (ctx1a.rev(), ctx1a, ctx2.rev(), ctx2)
422
423
424
        title = _('Visual Diffs - ') + title
        if pats:
            title += _(' filtered')
425
        self.setWindowTitle(title)
426

427
        self.resize(650, 250)
428
429
        repoagent = repo._pyqtobj  # TODO
        self.reponame = hglib.fromunicode(repoagent.displayName())
430
431

        self.ctxs = (ctx1a, ctx1b, ctx2)
432
        self.filesets = (sa, sb)
433
        self.copies = cpy
434
435
        self.repo = repo
        self.curFile = None
436

437
        layout = QVBoxLayout()
438
439
        self.setLayout(layout)

440
441
        lbl = QLabel(_('Temporary files are removed when this dialog '
                       'is closed'))
442
443
        layout.addWidget(lbl)

444
        list = QListWidget()
445
446
        layout.addWidget(list)
        self.list = list
447
        list.itemActivated.connect(self.itemActivated)
448
449
450
451

        tools = hglib.difftools(repo.ui)
        preferred = besttool(repo.ui, tools)
        self.diffpath, self.diffopts, self.mergeopts = tools[preferred]
452
        self.tools = tools
453
        self.preferred = preferred
454
455

        if len(tools) > 1:
456
            hbox = QHBoxLayout()
457
458
            combo = QComboBox()
            lbl = QLabel(_('Select Tool:'))
459
460
461
462
            lbl.setBuddy(combo)
            hbox.addWidget(lbl)
            hbox.addWidget(combo, 1)
            layout.addLayout(hbox)
463
            for i, name in enumerate(tools.keys()):
464
465
466
467
468
                combo.addItem(name)
                if name == preferred:
                    defrow = i
            combo.setCurrentIndex(defrow)

469
            list.currentRowChanged.connect(self.updateToolSelection)
470
            combo.currentIndexChanged[str].connect(self.onToolSelected)
471
            self.toolCombo = combo
472

473
        BB = QDialogButtonBox
474
475
        bb = BB()
        layout.addWidget(bb)
476
477
478
479
480
481

        if ctx2.rev() is None:
            pass
            # Do not offer directory diffs when the working directory
            # is being referenced directly
        elif ctx1b:
482
483
484
485
486
487
            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)
488
        else:
489
490
            self.dbutton = bb.addButton(_('Directory diff'), BB.ActionRole)
            self.dbutton.pressed.connect(self.p1dirdiff)
491

492
        self.updateDiffButtons(preferred)
493

494
        QShortcut(QKeySequence('CTRL+D'), self.list, self.activateCurrent)
495
        QTimer.singleShot(0, self.fillmodel)
496

497
498
499
500
    @pyqtSlot()
    def fillmodel(self):
        repo = self.repo
        sa, sb = self.filesets
501
        self.dirs, self.revs = snapshotset(repo, self.ctxs, sa, sb, self.copies)[:2]
502
503
504
505
506
507
508
509
510
511

        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 ' '

512
        mod_a, add_a, rem_a = sa
513
        for f in sorted(mod_a | add_a | rem_a):
514
            status = get_status(f, mod_a, add_a, rem_a)
515
            row = '%s %s' % (status, hglib.tounicode(f))
516
            self.list.addItem(row)
517

518
    @pyqtSlot(str)
519
    def onToolSelected(self, tool):
520
        'user selected a tool from the tool combo'
521
522
523
524
        tool = hglib.fromunicode(tool)
        assert tool in self.tools
        self.diffpath, self.diffopts, self.mergeopts = self.tools[tool]
        self.updateDiffButtons(tool)
525

526
527
    @pyqtSlot(int)
    def updateToolSelection(self, row):
528
        'user selected a file, pick an appropriate tool from combo'
529
530
531
532
533
        if row == -1:
            return

        repo = self.repo
        patterns = repo.ui.configitems('diff-patterns')
534
        patterns = [(p, t) for p,t in patterns if t in self.tools]
535

536
537
538
539
540
        fname = self.list.item(row).text()[2:]
        fname = hglib.fromunicode(fname)
        if self.curFile == fname:
            return
        self.curFile = fname
541
542
543
544
545
546
        for pat, tool in patterns:
            mf = match.match(repo.root, '', [pat])
            if mf(fname):
                selected = tool
                break
        else:
547
            selected = self.preferred
548
        for i, name in enumerate(self.tools.keys()):
549
            if name == selected:
550
                self.toolCombo.setCurrentIndex(i)
551

552
553
554
555
556
557
    def activateCurrent(self):
        'CTRL+D has been pressed'
        row = self.list.currentRow()
        if row >= 0:
            self.launch(self.list.item(row).text()[2:])

558
559
560
    def itemActivated(self, item):
        'A QListWidgetItem has been activated'
        self.launch(item.text()[2:])
561

562
    def updateDiffButtons(self, tool):
563
        # hg>=4.4: configbool() may return None as the default is set to None
564
565
566
        if hasattr(self, 'p1button'):
            d2 = self.repo.ui.configbool('merge-tools', tool + '.dirdiff')
            d3 = self.repo.ui.configbool('merge-tools', tool + '.dir3diff')
567
568
569
            self.p1button.setEnabled(bool(d2))
            self.p2button.setEnabled(bool(d2))
            self.p3button.setEnabled(bool(d3))
570
571
        elif hasattr(self, 'dbutton'):
            d2 = self.repo.ui.configbool('merge-tools', tool + '.dirdiff')
572
            self.dbutton.setEnabled(bool(d2))
573

574
575
    def launch(self, fname):
        fname = hglib.fromunicode(fname)
576
577
578
579
580
581
582
583
584
585
586
587
588
589
        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:
Steve Borho's avatar
Steve Borho committed
590
                nullfile = os.path.join(qtlib.gettempdir(), 'empty')
591
592
                fp = open(nullfile, 'w')
                fp.close()
593
                return hglib.fromunicode(_nonexistant, 'replace'), nullfile
594
595
596
597
598

        local, file1a = getfile(ctx1a, dir1a, fname, source)
        if ctx1b:
            other, file1b = getfile(ctx1b, dir1b, fname, source)
        else:
Steve Borho's avatar
Steve Borho committed
599
            other, file1b = fname, None
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
        fname, file2 = getfile(ctx2, dir2, fname, None)

        label1a = local+rev1a
        label1b = other+rev1b
        label2 = fname+rev2
        if ctx1b:
            label1a += '[local]'
            label1b += '[other]'
            label2 += '[merged]'

        # 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)

619
    def p1dirdiff(self):
620
621
622
623
624
625
626
627
628
629
        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)

630
    def p2dirdiff(self):
631
632
633
634
635
636
637
638
639
640
        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)

641
    def threewaydirdiff(self):
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=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)