visdiff.py 25.9 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
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
                       clabel=label2, child=dir2, chash=str(ctx2))
441
        launchtool(diffcmd, args, replace, True)  # type: Dict[Text, Union[bytes, Text]]
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
455

    def dodiffwrapper():
        try:
Steve Borho's avatar
Steve Borho committed
456
            dodiff()
457
        finally:
Steve Borho's avatar
Steve Borho committed
458
            # cleanup happens atexit
459
            ui.note(b'cleaning up temp directory\n')
460

461
    if mainapp:
462
463
464
465
466
467
468
469
        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()

470
class FileSelectionDialog(QDialog):
471
472
    'Dialog for selecting visual diff candidates'
    def __init__(self, repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy):
473
        # type: (localrepo.localrepository, Sequence[bytes], HgContext, List[Set[bytes]], Optional[HgContext], List[Set[bytes]], HgContext, Dict[bytes, bytes]) -> None
474
        'Initialize the Dialog'
475
        QDialog.__init__(self)
476

477
        self.setWindowIcon(qtlib.geticon('visualdiff'))
478
479
480
481

        if ctx2.rev() is None:
            title = _('working changes')
        elif ctx1a == ctx2.parents()[0]:
482
            title = _('changeset %d:%s') % (ctx2.rev(), ctx2)
483
        else:
484
485
            title = _('revisions %d:%s to %d:%s') \
                % (ctx1a.rev(), ctx1a, ctx2.rev(), ctx2)
486
487
488
        title = _('Visual Diffs - ') + title
        if pats:
            title += _(' filtered')
489
        self.setWindowTitle(title)
490

491
        self.resize(650, 250)
492
493
        repoagent = repo._pyqtobj  # TODO
        self.reponame = hglib.fromunicode(repoagent.displayName())
494
495

        self.ctxs = (ctx1a, ctx1b, ctx2)
496
        self.filesets = (sa, sb)
497
        self.copies = cpy
498
        self.repo = repo
499
        self.curFile = None  # type: Optional[bytes]
500

501
        layout = QVBoxLayout()
502
503
        self.setLayout(layout)

504
505
        lbl = QLabel(_('Temporary files are removed when this dialog '
                       'is closed'))
506
507
        layout.addWidget(lbl)

508
        list = QListWidget()
509
510
        layout.addWidget(list)
        self.list = list
511
        list.itemActivated.connect(self.itemActivated)
512
513
514
515

        tools = hglib.difftools(repo.ui)
        preferred = besttool(repo.ui, tools)
        self.diffpath, self.diffopts, self.mergeopts = tools[preferred]
516
        self.tools = tools
517
        self.preferred = preferred
518
519

        if len(tools) > 1:
520
            hbox = QHBoxLayout()
521
522
            combo = QComboBox()
            lbl = QLabel(_('Select Tool:'))
523
524
525
526
            lbl.setBuddy(combo)
            hbox.addWidget(lbl)
            hbox.addWidget(combo, 1)
            layout.addLayout(hbox)
527
            for i, name in enumerate(tools.keys()):
528
                combo.addItem(hglib.tounicode(name))
529
530
531
532
                if name == preferred:
                    defrow = i
            combo.setCurrentIndex(defrow)

533
            list.currentRowChanged.connect(self.updateToolSelection)
534
            combo.currentIndexChanged[str].connect(self.onToolSelected)
535
            self.toolCombo = combo
536

537
        BB = QDialogButtonBox
538
539
        bb = BB()
        layout.addWidget(bb)
540
541
542
543
544
545

        if ctx2.rev() is None:
            pass
            # Do not offer directory diffs when the working directory
            # is being referenced directly
        elif ctx1b:
546
547
548
549
550
551
            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)
552
        else:
553
554
            self.dbutton = bb.addButton(_('Directory diff'), BB.ActionRole)
            self.dbutton.pressed.connect(self.p1dirdiff)
555

556
        self.updateDiffButtons(preferred)
557

558
        QShortcut(QKeySequence('CTRL+D'), self.list, self.activateCurrent)
559
        QTimer.singleShot(0, self.fillmodel)
560

561
562
    @pyqtSlot()
    def fillmodel(self):
563
        # type: () -> None
564
565
        repo = self.repo
        sa, sb = self.filesets
566
        self.dirs, self.revs = snapshotset(repo, self.ctxs, sa, sb, self.copies)[:2]
567
568

        def get_status(file, mod, add, rem):
569
            # type: (bytes, Set[bytes], Set[bytes], Set[bytes]) -> Text
570
571
572
573
574
575
576
577
            if file in mod:
                return 'M'
            if file in add:
                return 'A'
            if file in rem:
                return 'R'
            return ' '

578
        mod_a, add_a, rem_a = sa
579
        for f in sorted(mod_a | add_a | rem_a):
580
            status = get_status(f, mod_a, add_a, rem_a)
581
            row = '%s %s' % (status, hglib.tounicode(f))
582
            self.list.addItem(row)
583

584
    @pyqtSlot(str)
585
    def onToolSelected(self, tool):
586
        # type: (Text) -> None
587
        'user selected a tool from the tool combo'
588
        tool = hglib.fromunicode(tool)  # pytype: disable=annotation-type-mismatch
589
        assert tool in self.tools, tool
590
591
        self.diffpath, self.diffopts, self.mergeopts = self.tools[tool]
        self.updateDiffButtons(tool)
592

593
594
    @pyqtSlot(int)
    def updateToolSelection(self, row):
595
        # type: (int) -> None
596
        'user selected a file, pick an appropriate tool from combo'
597
598
599
600
        if row == -1:
            return

        repo = self.repo
601
        patterns = repo.ui.configitems(b'diff-patterns')
602
        patterns = [(p, t) for p,t in patterns if t in self.tools]
603

604
605
606
607
608
        fname = self.list.item(row).text()[2:]
        fname = hglib.fromunicode(fname)
        if self.curFile == fname:
            return
        self.curFile = fname
609
        for pat, tool in patterns:
610
            mf = match.match(repo.root, b'', [pat])
611
612
613
614
            if mf(fname):
                selected = tool
                break
        else:
615
            selected = self.preferred
616
        for i, name in enumerate(self.tools.keys()):
617
            if name == selected:
618
                self.toolCombo.setCurrentIndex(i)
619

620
    def activateCurrent(self):
621
        # type: () -> None
622
623
624
625
626
        'CTRL+D has been pressed'
        row = self.list.currentRow()
        if row >= 0:
            self.launch(self.list.item(row).text()[2:])

627
    def itemActivated(self, item):
628
        # type: (QListWidgetItem) -> None
629
630
        'A QListWidgetItem has been activated'
        self.launch(item.text()[2:])
631

632
    def updateDiffButtons(self, tool):
633
        # type: (bytes) -> None
634
        # hg>=4.4: configbool() may return None as the default is set to None
635
        if hasattr(self, 'p1button'):
636
637
            d2 = self.repo.ui.configbool(b'merge-tools', tool + b'.dirdiff')
            d3 = self.repo.ui.configbool(b'merge-tools', tool + b'.dir3diff')
638
639
640
            self.p1button.setEnabled(bool(d2))
            self.p2button.setEnabled(bool(d2))
            self.p3button.setEnabled(bool(d3))
641
        elif hasattr(self, 'dbutton'):
642
            d2 = self.repo.ui.configbool(b'merge-tools', tool + b'.dirdiff')
643
            self.dbutton.setEnabled(bool(d2))
644

645
    def launch(self, fname):
646
647
        # type: (Text) -> None
        fname = hglib.fromunicode(fname)  # pytype: disable=annotation-type-mismatch
648
649
650
651
652
        source = self.copies.get(fname, None)
        dir1a, dir1b, dir2 = self.dirs
        rev1a, rev1b, rev2 = self.revs
        ctx1a, ctx1b, ctx2 = self.ctxs

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

        local, file1a = getfile(ctx1a, dir1a, fname, source)
        if ctx1b:
            other, file1b = getfile(ctx1b, dir1b, fname, source)
        else:
Steve Borho's avatar
Steve Borho committed
674
            other, file1b = fname, None
675
        fname, file2 = getfile(ctx2, dir2, fname, None)  # pytype: disable=annotation-type-mismatch
676
677
678
679
680

        label1a = local+rev1a
        label1b = other+rev1b
        label2 = fname+rev2
        if ctx1b:
681
682
683
            label1a += b'[local]'
            label1b += b'[other]'
            label2 += b'[merged]'
684
685
686
687
688
689

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

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

706
    def p2dirdiff(self):
707
        # type: () -> None
708
709
710
711
712
713
714
        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),
715
                       parent2='', plabel2='', clabel=rev2, child=dir2)  # type: Dict[Text, Union[bytes, Text]]
716
717
        launchtool(self.diffpath, self.diffopts, replace, False)

718
    def threewaydirdiff(self):
719
        # type: () -> None
720
721
722
723
724
725
726
        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),
727
                       parent2=dir1b, plabel2=rev1b, clabel=dir2, child=rev2)  # type: Dict[Text, Union[bytes, Text]]
728
        launchtool(self.diffpath, self.mergeopts, replace, False)