visdiff.py 25.8 KB
Newer Older
1
2
3
4
5
# 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
6
# GNU General Public License version 2 or any later version.
7

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
if hglib.TYPE_CHECKING:
    from typing import (
        Any,
        Dict,
        Iterable,
        List,
        Optional,
        Sequence,
        Set,
        Text,
        Tuple,
        Union,
    )
    from mercurial import (
        localrepo,
        ui as uimod,
    )
    from .qtgui import (
        QListWidgetItem,
    )
    from ..util.typelib import (
        DiffTools,
        HgContext,
    )

    # Destination name, source name, dest modification time
    FnsAndMtime = Tuple[bytes, bytes, float]


79
# Match parent2 first, so 'parent1?' will match both parent1 and parent
kiilerix's avatar
kiilerix committed
80
_regex = b'\$(parent2|parent1?|child|plabel1|plabel2|clabel|repo|phash1|phash2|chash)'
81
82
83

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

84
85
86
87
# 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

88
def snapshotset(repo, ctxs, sa, sb, copies, copyworkingdir = False):
89
    # type: (localrepo.localrepository, Sequence[HgContext], List[Set[bytes]], List[Set[bytes]], Dict[bytes, bytes], bool) -> Tuple[List[Optional[bytes]], List[bytes], List[List[FnsAndMtime]]]
90
91
92
93
    '''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
94
95
96
97

    global _diffCount
    _diffCount += 1

98
99
100
101
102
103
104
105
    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)
106
    label1a = b'@%d:%s' % (ctx1a.rev(), ctx1a)
107
108
109
110
111

    # 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)
112
        label1b = b'@%d:%s' % (ctx1b.rev(), ctx1b)
113
114
    else:
        dir1b = None
115
        fns_mtime1b = []  # type: List[FnsAndMtime]
116
        label1b = b''
117
118
119
120
121
122
123
124

    # 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
125
            fns_mtime2 = []  # type: List[FnsAndMtime]
126
        # If ctx2 is working copy, use empty label.
127
        label2 = b''
128
129
    else:
        dir2, fns_mtime2 = snapshot(repo, files2, ctx2)
130
        label2 = b'@%d:%s' % (ctx2.rev(), ctx2)
131
132
133
134
135
136

    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
137
def snapshot(repo, files, ctx):
138
    # type: (localrepo.localrepository, Iterable[bytes], HgContext) -> Tuple[bytes, List[FnsAndMtime]]
kiilerix's avatar
kiilerix committed
139
140
141
    '''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.'''
142
143
    dirname = os.path.basename(repo.root) or b'root'
    dirname += b'.%d' % _diffCount
144
    if ctx.rev() is not None:
145
        dirname += b'.%d' % ctx.rev()
Steve Borho's avatar
Steve Borho committed
146
    base = os.path.join(qtlib.gettempdir(), dirname)
147
    fns_and_mtime = []
148
    if not os.path.exists(base):
149
        os.makedirs(base)
150
    for fn in files:
kiilerix's avatar
kiilerix committed
151
        assert isinstance(fn, bytes), repr(fn)
152
        wfn = util.pconvert(fn)
153
        if wfn not in ctx:
154
155
156
            # File doesn't exist; could be a bogus modify
            continue
        dest = os.path.join(base, wfn)
157
158
159
        if os.path.exists(dest):
            # File has already been snapshot
            continue
160
        destdir = os.path.dirname(dest)
161
162
163
        try:
            if not os.path.isdir(destdir):
                os.makedirs(destdir)
164
165
            fctx = ctx[wfn]
            data = repo.wwritedata(wfn, fctx.data())
166
167
            with open(dest, 'wb') as f:
                f.write(data)
kiilerix's avatar
kiilerix committed
168
            if b'x' in fctx.flags():
169
                util.setflags(dest, False, True)
170
            if ctx.rev() is None:
171
                fns_and_mtime.append((dest, repo.wjoin(fn),
172
173
174
175
176
177
                                    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
178
179
180
    return base, fns_and_mtime

def launchtool(cmd, opts, replace, block):
181
182
    # type: (bytes, Sequence[bytes], Dict[Text, Union[bytes, Text]], bool) -> None
    # TODO: fix up the bytes vs str in the replacement mapping
183
    def quote(match):
kiilerix's avatar
kiilerix committed
184
        key = pycompat.sysstr(match.group()[1:])
185
        return procutil.shellquote(replace[key])
186
187

    args = b' '.join(opts)
188
    args = re.sub(_regex, quote, args)
kiilerix's avatar
kiilerix committed
189
    cmdline = procutil.shellquote(cmd) + b' ' + args
190
    try:
191
        proc = subprocess.Popen(procutil.tonativestr(cmdline), shell=True,
192
                                creationflags=qtlib.openflags,
193
194
195
196
197
                                stderr=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stdin=subprocess.PIPE)
        if block:
            proc.communicate()
198
    except (OSError, EnvironmentError) as e:
199
        QMessageBox.warning(None,
200
                _('Tool launch failure'),
201
                _('%s : %s') % (hglib.tounicode(cmd), hglib.tounicode(str(e))))
202
203

def filemerge(ui, fname, patchedfname):
204
    # type: (uimod.ui, Text, Text) -> None
205
206
207
    'Launch the preferred visual diff tool for two text files'
    detectedtools = hglib.difftools(ui)
    if not detectedtools:
208
        QMessageBox.warning(None,
209
210
211
212
213
214
215
216
217
218
219
220
                _('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)


221
def besttool(ui, tools, force=None):
222
    # type: (uimod.ui, DiffTools, Optional[bytes]) -> bytes
223
    'Select preferred or highest priority tool from dictionary'
224
    preferred = force or ui.config(b'tortoisehg', b'vdiff') or \
225
                         ui.config(b'ui', b'merge')
226
227
228
229
    if preferred and preferred in tools:
        return preferred
    pris = []
    for t in tools.keys():
230
        try:
231
            p = ui.configint(b'merge-tools', t + b'.priority')
232
        except error.ConfigError as inst:
233
            ui.warn(b'visdiff: %s\n' % stringutil.forcebytestr(inst))
234
            p = 0
235
        assert p is not None  # help pytype: default *.priority is 0
236
        pris.append((-p, t))
237
238

    return sorted(pris)[0][1]
239
240
241


def visualdiff(ui, repo, pats, opts):
242
    # type: (uimod.ui, localrepo.localrepository, Sequence[bytes], Dict[Text, Any]) -> Optional[FileSelectionDialog]
243
    revs = opts.get('rev', [])
244
245
246
247
248
    change = opts.get('change')

    try:
        ctx1b = None
        if change:
249
            # TODO: figure out what's the expect type
250
251
252
            if isinstance(change, pycompat.unicode):
                change = hglib.fromunicode(change)
            if isinstance(change, bytes):
253
                ctx2 = hglib.revsymbol(repo, change)
254
255
            else:
                ctx2 = repo[change]
256
257
258
259
260
261
            p = ctx2.parents()
            if len(p) > 1:
                ctx1a, ctx1b = p
            else:
                ctx1a = p[0]
        else:
262
263
            n1, n2 = scmutil.revpair(repo, [hglib.fromunicode(rev)
                                            for rev in revs])
264
265
266
267
268
            ctx1a, ctx2 = repo[n1], repo[n2]
            p = ctx2.parents()
            if not revs and len(p) > 1:
                ctx1b = p[1]
    except (error.LookupError, error.RepoError):
269
        QMessageBox.warning(None,
270
271
272
273
                       _('Unable to find changeset'),
                       _('You likely need to refresh this application'))
        return None

274
275
276
277
278
    return visual_diff(ui, repo, pats, ctx1a, ctx1b, ctx2, opts.get('tool'),
                       opts.get('mainapp'), revs)

def visual_diff(ui, repo, pats, ctx1a, ctx1b, ctx2, tool, mainapp=False,
                revs=None):
279
    # type: (uimod.ui, localrepo.localrepository, Sequence[bytes], HgContext, Optional[HgContext], HgContext, bytes, bool, Optional[Sequence[int]]) -> Optional[FileSelectionDialog]
280
281
282
283
284
285
286
287
288
289
290
    """Opens the visual diff tool on the given file patterns in the given
    contexts.  If a ``tool`` is provided, it is used, otherwise the diff tool
    launched is determined by the configuration.  For a 2-way diff, ``ctx1a`` is
    the context for the first revision, ``ctxb1`` is None, and ``ctx2`` is the
    context for the second revision.  For a 3-way diff, ``ctx2`` is the wdir
    context and ``ctx1a`` and ``ctx1b`` are the "local" and "other" contexts
    respectively.
    """
    # TODO: Figure out how to get rid of the `revs` argument
    if revs is None:
        revs = []
291
    pats = scmutil.expandpats(pats)
292
    m = match.match(repo.root, b'', pats, None, None, b'relpath', ctx=ctx2)
293
    n2 = ctx2.node()
294
295

    def _status(ctx):
296
        # type: (HgContext) -> Tuple[List[bytes], List[bytes], List[bytes]]
297
298
299
300
        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))
301
    if ctx1b:
302
        mod_b, add_b, rem_b = pycompat.maplist(set, _status(ctx1b))
303
        cpy = copies.mergecopies(repo, ctx1a, ctx1b, ctx1a.ancestor(ctx1b))[0].copy
304
    else:
305
        cpy = copies.pathcopies(ctx1a, ctx2)
306
        mod_b, add_b, rem_b = set(), set(), set()
307
308
309
310
311

    cpy = {
        dst: src for dst, src in cpy.items() if m(src) or m(dst)
    }

312
313
314
    MA = mod_a | add_a | mod_b | add_b
    MAR = MA | rem_a | rem_b
    if not MAR:
315
        QMessageBox.information(None,
316
317
318
319
320
321
                _('No file changes'),
                _('There are no file changes to view'))
        return None

    detectedtools = hglib.difftools(repo.ui)
    if not detectedtools:
322
        QMessageBox.warning(None,
323
324
325
326
                _('No diff tool found'),
                _('No visual diff tools were detected'))
        return None

327
    preferred = besttool(repo.ui, detectedtools, tool)
328
329
330

    # Build tool list based on diff-patterns matches
    toollist = set()
331
    patterns = repo.ui.configitems(b'diff-patterns')
332
333
334
    patterns = [(p, t) for p,t in patterns if t in detectedtools]
    for path in MAR:
        for pat, tool in patterns:
335
            mf = match.match(repo.root, b'', [pat])
336
337
338
339
340
341
            if mf(path):
                toollist.add(tool)
                break
        else:
            toollist.add(preferred)

342
    cto = list(cpy.keys())
343
    for path in MAR:
344
        if path in cto:
345
346
347
348
            hascopies = True
            break
    else:
        hascopies = False
349
    force = repo.ui.configbool(b'tortoisehg', b'forcevdiffwin')
350
    if len(toollist) > 1 or (hascopies and len(MAR) > 1) or force:
351
352
353
        usewin = True
    else:
        preferred = toollist.pop()
354
355
356
        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')
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
        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
385
    def dodiff():
386
387
        assert not (hascopies and len(MAR) > 1), \
                'dodiff cannot handle copies when diffing dirs'
388

389
390
391
392
393
394
        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
395
        dirs, labels, fns_and_mtimes = snapshotset(repo, ctxs, sa, sb, cpy,
396
397
398
399
400
                                                   copyworkingdir)
        dir1a, dir1b, dir2 = dirs
        label1a, label1b, label2 = labels
        fns_and_mtime = fns_and_mtimes[2]

401
        if len(MAR) > 1 and label2 == b'':
402
            label2 = b'working files'
403
404

        def getfile(fname, dir, label):
405
            # type: (bytes, bytes, bytes) -> Tuple[bytes, bytes]
Steve Borho's avatar
Steve Borho committed
406
            file = os.path.join(qtlib.gettempdir(), dir, fname)
407
408
            if os.path.isfile(file):
                return fname+label, file
409
            nullfile = os.path.join(qtlib.gettempdir(), b'empty')
kiilerix's avatar
kiilerix committed
410
            fp = open(nullfile, 'wb')
411
            fp.close()
412
413
            return (hglib.fromunicode(_nonexistant, 'replace') + label,
                    nullfile)
414
415
416
417

        # 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:
418
419
            file2 = MAR.pop()
            file2local = util.localpath(file2)
420
421
422
423
424
            if file2 in cto:
                file1 = util.localpath(cpy[file2])
            else:
                file1 = file2
            label1a, dir1a = getfile(file1, dir1a, label1a)
425
            if do3way:
426
                label1b, dir1b = getfile(file1, dir1b, label1b)
427
            label2, dir2 = getfile(file2local, dir2, label2)
428
        if do3way:
429
430
431
            label1a += b'[local]'
            label1b += b'[other]'
            label2 += b'[merged]'
432

433
        repoagent = repo._pyqtobj  # TODO
434
435

        # TODO: sort out bytes vs str
436
437
438
        replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b,
                       plabel1=label1a, plabel2=label1b,
                       phash1=str(ctx1a), phash2=str(ctx1b),
439
                       repo=hglib.fromunicode(repoagent.displayName()),
440
441
                       clabel=label2, child=dir2, chash=str(ctx2))  # type: Dict[Text, Union[bytes, Text]]
        launchtool(diffcmd, args, replace, True)
442
443
444

        # detect if changes were made to mirrored working files
        for copy_fn, working_fn, mtime in fns_and_mtime:
445
            try:
446
                if os.lstat(copy_fn).st_mtime != mtime:
447
448
449
                    ui.debug(b'file changed while diffing. '
                             b'Overwriting: %s (src: %s)\n'
                             % (working_fn, copy_fn))
450
451
452
                    util.copyfile(copy_fn, working_fn)
            except EnvironmentError:
                pass # Ignore I/O errors or missing files
453

454
    if mainapp:
455
        dodiff()
456
457
458
    else:
        # We are not the main application, so this must be done in a
        # background thread
459
        thread = threading.Thread(target=dodiff, name='visualdiff')
460
461
462
        thread.setDaemon(True)
        thread.start()

463
class FileSelectionDialog(QDialog):
464
465
    'Dialog for selecting visual diff candidates'
    def __init__(self, repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy):
466
        # type: (localrepo.localrepository, Sequence[bytes], HgContext, List[Set[bytes]], Optional[HgContext], List[Set[bytes]], HgContext, Dict[bytes, bytes]) -> None
467
        'Initialize the Dialog'
468
        QDialog.__init__(self)
469

470
        self.setWindowIcon(qtlib.geticon('visualdiff'))
471
472
473
474

        if ctx2.rev() is None:
            title = _('working changes')
        elif ctx1a == ctx2.parents()[0]:
475
            title = _('changeset %d:%s') % (ctx2.rev(), ctx2)
476
        else:
477
478
            title = _('revisions %d:%s to %d:%s') \
                % (ctx1a.rev(), ctx1a, ctx2.rev(), ctx2)
479
480
481
        title = _('Visual Diffs - ') + title
        if pats:
            title += _(' filtered')
482
        self.setWindowTitle(title)
483

484
        self.resize(650, 250)
485
486
        repoagent = repo._pyqtobj  # TODO
        self.reponame = hglib.fromunicode(repoagent.displayName())
487
488

        self.ctxs = (ctx1a, ctx1b, ctx2)
489
        self.filesets = (sa, sb)
490
        self.copies = cpy
491
        self.repo = repo
492
        self.curFile = None  # type: Optional[bytes]
493

494
        layout = QVBoxLayout()
495
496
        self.setLayout(layout)

497
498
        lbl = QLabel(_('Temporary files are removed when this dialog '
                       'is closed'))
499
500
        layout.addWidget(lbl)

501
        list = QListWidget()
502
503
        layout.addWidget(list)
        self.list = list
504
        list.itemActivated.connect(self.itemActivated)
505
506
507
508

        tools = hglib.difftools(repo.ui)
        preferred = besttool(repo.ui, tools)
        self.diffpath, self.diffopts, self.mergeopts = tools[preferred]
509
        self.tools = tools
510
        self.preferred = preferred
511
512

        if len(tools) > 1:
513
            hbox = QHBoxLayout()
514
515
            combo = QComboBox()
            lbl = QLabel(_('Select Tool:'))
516
517
518
519
            lbl.setBuddy(combo)
            hbox.addWidget(lbl)
            hbox.addWidget(combo, 1)
            layout.addLayout(hbox)
520
            for i, name in enumerate(tools.keys()):
521
                combo.addItem(hglib.tounicode(name))
522
523
524
525
                if name == preferred:
                    defrow = i
            combo.setCurrentIndex(defrow)

526
            list.currentRowChanged.connect(self.updateToolSelection)
527
            combo.currentIndexChanged[str].connect(self.onToolSelected)
528
            self.toolCombo = combo
529

530
        bb = QDialogButtonBox()
531
        layout.addWidget(bb)
532
533
534
535
536
537

        if ctx2.rev() is None:
            pass
            # Do not offer directory diffs when the working directory
            # is being referenced directly
        elif ctx1b:
538
            self.p1button = bb.addButton(_('Dir diff to p1'), QDialogButtonBox.ButtonRole.ActionRole)
539
            self.p1button.pressed.connect(self.p1dirdiff)
540
            self.p2button = bb.addButton(_('Dir diff to p2'), QDialogButtonBox.ButtonRole.ActionRole)
541
            self.p2button.pressed.connect(self.p2dirdiff)
542
            self.p3button = bb.addButton(_('3-way dir diff'), QDialogButtonBox.ButtonRole.ActionRole)
543
            self.p3button.pressed.connect(self.threewaydirdiff)
544
        else:
545
            self.dbutton = bb.addButton(_('Directory diff'), QDialogButtonBox.ButtonRole.ActionRole)
546
            self.dbutton.pressed.connect(self.p1dirdiff)
547

548
        self.updateDiffButtons(preferred)
549

550
        QShortcut(QKeySequence('CTRL+D'), self.list, self.activateCurrent)
551
        QTimer.singleShot(0, self.fillmodel)
552

553
554
    @pyqtSlot()
    def fillmodel(self):
555
        # type: () -> None
556
557
        repo = self.repo
        sa, sb = self.filesets
558
        self.dirs, self.revs = snapshotset(repo, self.ctxs, sa, sb, self.copies)[:2]
559
560

        def get_status(file, mod, add, rem):
561
            # type: (bytes, Set[bytes], Set[bytes], Set[bytes]) -> Text
562
563
564
565
566
567
568
569
            if file in mod:
                return 'M'
            if file in add:
                return 'A'
            if file in rem:
                return 'R'
            return ' '

570
        mod_a, add_a, rem_a = sa
571
        for f in sorted(mod_a | add_a | rem_a):
572
            status = get_status(f, mod_a, add_a, rem_a)
573
            row = '%s %s' % (status, hglib.tounicode(f))
574
            self.list.addItem(row)
575

576
    @pyqtSlot(str)
577
    def onToolSelected(self, tool):
578
        # type: (Text) -> None
579
        'user selected a tool from the tool combo'
580
        tool = hglib.fromunicode(tool)  # pytype: disable=annotation-type-mismatch
581
        assert tool in self.tools, tool
582
583
        self.diffpath, self.diffopts, self.mergeopts = self.tools[tool]
        self.updateDiffButtons(tool)
584

585
586
    @pyqtSlot(int)
    def updateToolSelection(self, row):
587
        # type: (int) -> None
588
        'user selected a file, pick an appropriate tool from combo'
589
590
591
592
        if row == -1:
            return

        repo = self.repo
593
        patterns = repo.ui.configitems(b'diff-patterns')
594
        patterns = [(p, t) for p,t in patterns if t in self.tools]
595

596
597
598
599
600
        fname = self.list.item(row).text()[2:]
        fname = hglib.fromunicode(fname)
        if self.curFile == fname:
            return
        self.curFile = fname
601
        for pat, tool in patterns:
602
            mf = match.match(repo.root, b'', [pat])
603
604
605
606
            if mf(fname):
                selected = tool
                break
        else:
607
            selected = self.preferred
608
        for i, name in enumerate(self.tools.keys()):
609
            if name == selected:
610
                self.toolCombo.setCurrentIndex(i)
611

612
    def activateCurrent(self):
613
        # type: () -> None
614
615
616
617
618
        'CTRL+D has been pressed'
        row = self.list.currentRow()
        if row >= 0:
            self.launch(self.list.item(row).text()[2:])

619
    def itemActivated(self, item):
620
        # type: (QListWidgetItem) -> None
621
622
        'A QListWidgetItem has been activated'
        self.launch(item.text()[2:])
623

624
    def updateDiffButtons(self, tool):
625
        # type: (bytes) -> None
626
        # hg>=4.4: configbool() may return None as the default is set to None
627
        if hasattr(self, 'p1button'):
628
629
            d2 = self.repo.ui.configbool(b'merge-tools', tool + b'.dirdiff')
            d3 = self.repo.ui.configbool(b'merge-tools', tool + b'.dir3diff')
630
631
632
            self.p1button.setEnabled(bool(d2))
            self.p2button.setEnabled(bool(d2))
            self.p3button.setEnabled(bool(d3))
633
        elif hasattr(self, 'dbutton'):
634
            d2 = self.repo.ui.configbool(b'merge-tools', tool + b'.dirdiff')
635
            self.dbutton.setEnabled(bool(d2))
636

637
    def launch(self, fname):
638
639
        # type: (Text) -> None
        fname = hglib.fromunicode(fname)  # pytype: disable=annotation-type-mismatch
640
641
642
643
644
        source = self.copies.get(fname, None)
        dir1a, dir1b, dir2 = self.dirs
        rev1a, rev1b, rev2 = self.revs
        ctx1a, ctx1b, ctx2 = self.ctxs

645
        # pytype: disable=redundant-function-type-comment
646
        def getfile(ctx, dir, fname, source):
647
            # type: (HgContext, bytes, bytes, Optional[bytes]) -> Tuple[bytes, bytes]
648
649
650
651
652
653
654
655
            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:
656
                nullfile = os.path.join(qtlib.gettempdir(), b'empty')
657
658
                fp = open(nullfile, 'w')
                fp.close()
659
                return hglib.fromunicode(_nonexistant, 'replace'), nullfile
660
        # pytype: enable=redundant-function-type-comment
661
662
663
664
665

        local, file1a = getfile(ctx1a, dir1a, fname, source)
        if ctx1b:
            other, file1b = getfile(ctx1b, dir1b, fname, source)
        else:
Steve Borho's avatar
Steve Borho committed
666
            other, file1b = fname, None
667
        fname, file2 = getfile(ctx2, dir2, fname, None)  # pytype: disable=annotation-type-mismatch
668
669
670
671
672

        label1a = local+rev1a
        label1b = other+rev1b
        label2 = fname+rev2
        if ctx1b:
673
674
675
            label1a += b'[local]'
            label1b += b'[other]'
            label2 += b'[merged]'
676
677
678
679
680
681

        # 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),
682
                       clabel=label2, child=file2)  # type: Dict[Text, Union[bytes, Text]]
683
684
685
        args = ctx1b and self.mergeopts or self.diffopts
        launchtool(self.diffpath, args, replace, False)

686
    def p1dirdiff(self):
687
        # type: () -> None
688
689
690
691
692
693
694
        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),
695
                       parent2='', plabel2='', clabel=rev2, child=dir2)  # type: Dict[Text, Union[bytes, Text]]
696
697
        launchtool(self.diffpath, self.diffopts, replace, False)

698
    def p2dirdiff(self):
699
        # type: () -> None
700
701
702
703
704
705
706
        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),
707
                       parent2='', plabel2='', clabel=rev2, child=dir2)  # type: Dict[Text, Union[bytes, Text]]
708
709
        launchtool(self.diffpath, self.diffopts, replace, False)

710
    def threewaydirdiff(self):
711
        # type: () -> None
712
713
714
715
716
717
718
        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),
719
                       parent2=dir1b, plabel2=rev1b, clabel=dir2, child=rev2)  # type: Dict[Text, Union[bytes, Text]]
720
        launchtool(self.diffpath, self.mergeopts, replace, False)