Read about our upcoming Code of Conduct on this issue

fileview.py 24.2 KB
Newer Older
Adrian Buehlmann's avatar
Adrian Buehlmann committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""
Qt4 high level widgets for hg repo changelogs and filelogs
"""
19

20
import os
Adrian Buehlmann's avatar
Adrian Buehlmann committed
21
import difflib
David Wilhelm's avatar
David Wilhelm committed
22
import re
Adrian Buehlmann's avatar
Adrian Buehlmann committed
23

Steve Borho's avatar
Steve Borho committed
24
from mercurial import hg, error, match, patch, util
25
from mercurial import ui as uimod, mdiff
Steve Borho's avatar
Steve Borho committed
26

27
28
29
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import Qsci
Adrian Buehlmann's avatar
Adrian Buehlmann committed
30

31
from tortoisehg.util import hglib, patchctx
32
from tortoisehg.hgqt.i18n import _
33
from tortoisehg.hgqt import annotate, qscilib, qtlib, blockmatcher, lexers
34
from tortoisehg.hgqt import visdiff, wctxactions
Adrian Buehlmann's avatar
Adrian Buehlmann committed
35
36
37

qsci = Qsci.QsciScintilla

38
class HgFileView(QFrame):
39
40
    """file diff and content viewer"""

41
    linkActivated = pyqtSignal(QString)
42
    fileDisplayed = pyqtSignal(QString, QString)
43
    showMessage = pyqtSignal(QString)
44
    revisionSelected = pyqtSignal(int)
45

46
47
48
49
50
51
52
    searchRequested = pyqtSignal(unicode)
    """Emitted (pattern) when user request to search content"""

    grepRequested = pyqtSignal(unicode, dict)
    """Emitted (pattern, opts) when user request to search changelog"""

    def __init__(self, repo, parent):
53
54
        QFrame.__init__(self, parent)
        framelayout = QVBoxLayout(self)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
55
56
57
        framelayout.setContentsMargins(0,0,0,0)
        framelayout.setSpacing(0)

58
        l = QHBoxLayout()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
59
60
        l.setContentsMargins(0,0,0,0)
        l.setSpacing(0)
Steve Borho's avatar
Steve Borho committed
61

62
        self.topLayout = QVBoxLayout()
63

64
65
66
67
        self.labelhbox = hbox = QHBoxLayout()
        hbox.setContentsMargins(0,0,0,0)
        hbox.setSpacing(2)
        self.topLayout.addLayout(hbox)
68
69
70
71
72

        self.diffToolbar = QToolBar(_('Diff Toolbar'))
        self.diffToolbar.setIconSize(QSize(16,16))
        hbox.addWidget(self.diffToolbar)

73
        self.filenamelabel = w = QLabel()
74
        w.setWordWrap(True)
75
76
77
        f = w.textInteractionFlags()
        w.setTextInteractionFlags(f | Qt.TextSelectableByMouse)
        w.linkActivated.connect(self.linkActivated)
78
        hbox.addWidget(w, 1)
79

80
81
82
83
84
        self.extralabel = w = QLabel()
        w.setWordWrap(True)
        w.linkActivated.connect(self.linkActivated)
        self.topLayout.addWidget(w)
        w.hide()
85

Adrian Buehlmann's avatar
Adrian Buehlmann committed
86
87
88
        framelayout.addLayout(self.topLayout)
        framelayout.addLayout(l, 1)

Steve Borho's avatar
Steve Borho committed
89
90
91
92
        hbox = QHBoxLayout()
        hbox.setContentsMargins(0, 0, 0, 0)
        hbox.setSpacing(0)
        l.addLayout(hbox)
93
94

        self.blk = blockmatcher.BlockList(self)
95
        self.sci = annotate.AnnotateView(repo, self)
Steve Borho's avatar
Steve Borho committed
96
        hbox.addWidget(self.blk)
97
        hbox.addWidget(self.sci, 1)
98

99
100
101
102
103
        for name in ('searchRequested', 'editSelected', 'grepRequested'):
            getattr(self.sci, name).connect(getattr(self, name))
        self.sci.revisionHint.connect(self.showMessage)
        self.sci.sourceChanged.connect(self.sourceChanged)
        self.sci.setAnnotationEnabled(False)
104

105
        self.blk.linkScrollBar(self.sci.verticalScrollBar())
106
        self.blk.setVisible(False)
107

Adrian Buehlmann's avatar
Adrian Buehlmann committed
108
109
        self.sci.setFrameStyle(0)
        self.sci.setReadOnly(True)
110
        self.sci.setUtf8(True)
111
        self.sci.installEventFilter(qscilib.KeyPressInterceptor(self))
112
113
        self.sci.setContextMenuPolicy(Qt.CustomContextMenu)
        self.sci.customContextMenuRequested.connect(self.menuRequested)
114
115
        self.sci.setCaretLineVisible(False)

116
117
118
119
120
121
122
123
124
125
126
127
        # define markers for colorize zones of diff
        self.markerplus = self.sci.markerDefine(qsci.Background)
        self.markerminus = self.sci.markerDefine(qsci.Background)
        self.markertriangle = self.sci.markerDefine(qsci.Background)
        self.sci.setMarkerBackgroundColor(QColor('#B0FFA0'), self.markerplus)
        self.sci.setMarkerBackgroundColor(QColor('#A0A0FF'), self.markerminus)
        self.sci.setMarkerBackgroundColor(QColor('#FFA0A0'), self.markertriangle)

        # hide margin 0 (markers)
        self.sci.setMarginType(0, qsci.SymbolMargin)
        self.sci.setMarginWidth(0, 0)

128
129
130
131
132
133
        self.searchbar = qscilib.SearchToolBar(hidable=True)
        self.searchbar.hide()
        self.searchbar.searchRequested.connect(self.find)
        self.searchbar.conditionChanged.connect(self.highlightText)
        self.layout().addWidget(self.searchbar)

Adrian Buehlmann's avatar
Adrian Buehlmann committed
134
135
        self._ctx = None
        self._filename = None
136
        self._status = None
137
138
        self._mode = None
        self._lostMode = None
139
        self._lastSearch = u'', False
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154

        self.actionDiffMode = QAction('Diff', self)
        self.actionDiffMode.setCheckable(True)
        self.actionFileMode = QAction('File', self)
        self.actionFileMode.setCheckable(True)
        self.actionAnnMode = QAction('Ann', self)
        self.actionAnnMode.setCheckable(True)

        self.modeToggleGroup = QActionGroup(self)
        self.modeToggleGroup.addAction(self.actionDiffMode)
        self.modeToggleGroup.addAction(self.actionFileMode)
        self.modeToggleGroup.addAction(self.actionAnnMode)
        self.modeToggleGroup.triggered.connect(self.setMode)

        # Next/Prev diff (in full file mode)
155
156
        self.actionNextDiff = QAction(qtlib.geticon('down'),
                                      'Next diff (alt+down)', self)
157
158
        self.actionNextDiff.setShortcut('Alt+Down')
        self.actionNextDiff.triggered.connect(self.nextDiff)
159
160
        self.actionPrevDiff = QAction(qtlib.geticon('up'),
                                      'Previous diff (alt+up)', self)
161
162
163
164
165
        self.actionPrevDiff.setShortcut('Alt+Up')
        self.actionPrevDiff.triggered.connect(self.prevDiff)

        self.forceMode('diff')

166
167
168
169
        self.actionFind = self.searchbar.toggleViewAction()
        self.actionFind.setIcon(qtlib.geticon('edit-find'))
        self.actionFind.setShortcut(QKeySequence.Find)

170
171
172
173
174
175
176
        tb = self.diffToolbar
        tb.addAction(self.actionDiffMode)
        tb.addAction(self.actionFileMode)
        tb.addAction(self.actionAnnMode)
        tb.addSeparator()
        tb.addAction(self.actionNextDiff)
        tb.addAction(self.actionPrevDiff)
177
178
        tb.addSeparator()
        tb.addAction(self.actionFind)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
179

180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
        self.actionNextLine = QAction('Next line', self)
        self.actionNextLine.setShortcut(Qt.SHIFT + Qt.Key_Down)
        self.actionNextLine.triggered.connect(self.nextLine)
        self.addAction(self.actionNextLine)
        self.actionPrevLine = QAction('Prev line', self)
        self.actionPrevLine.setShortcut(Qt.SHIFT + Qt.Key_Up)
        self.actionPrevLine.triggered.connect(self.prevLine)
        self.addAction(self.actionPrevLine)
        self.actionNextCol = QAction('Next column', self)
        self.actionNextCol.setShortcut(Qt.SHIFT + Qt.Key_Right)
        self.actionNextCol.triggered.connect(self.nextCol)
        self.addAction(self.actionNextCol)
        self.actionPrevCol = QAction('Prev column', self)
        self.actionPrevCol.setShortcut(Qt.SHIFT + Qt.Key_Left)
        self.actionPrevCol.triggered.connect(self.prevCol)
        self.addAction(self.actionPrevCol)

197
        self.timer = QTimer()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
198
        self.timer.setSingleShot(False)
199
        self.timer.timeout.connect(self.idle_fill_files)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
200

201
202
203
204
205
206
207
208
209
210
    def menuRequested(self, point):
        point = self.sci.mapToGlobal(point)
        return self.sci.createStandardContextMenu().exec_(point)

    def loadSettings(self, qs, prefix):
        self.sci.loadSettings(qs, prefix)

    def saveSettings(self, qs, prefix):
        self.sci.saveSettings(qs, prefix)

211
212
213
214
    @pyqtSlot(QAction)
    def setMode(self, action):
        'One of the mode toolbar buttons has been toggled'
        mode = {'Diff':'diff', 'File':'file', 'Ann':'ann'}[str(action.text())]
215
        self.actionNextDiff.setEnabled(mode == 'file')
216
        self.actionPrevDiff.setEnabled(False)
217
        self.blk.setVisible(mode == 'file')
218
        self.sci.setAnnotationEnabled(mode == 'ann')
Adrian Buehlmann's avatar
Adrian Buehlmann committed
219
220
        if mode != self._mode:
            self._mode = mode
221
222
            if not self._lostMode:
                self.displayFile()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
223

224
225
226
    def forceMode(self, mode):
        'Force into file or diff mode, based on content constaints'
        assert mode in ('diff', 'file')
227
228
        if self._lostMode is None:
            self._lostMode = self._mode
229
230
231
232
233
234
235
236
237
238
239
        self._mode = mode
        if mode == 'diff':
            self.actionDiffMode.setChecked(True)
        else:
            self.actionFileMode.setChecked(True)
        self.actionDiffMode.setEnabled(False)
        self.actionFileMode.setEnabled(False)
        self.actionAnnMode.setEnabled(False)
        self.actionNextDiff.setEnabled(False)
        self.actionPrevDiff.setEnabled(False)
        self.blk.setVisible(mode == 'file')
240
        self.sci.setAnnotationEnabled(False)
241

Adrian Buehlmann's avatar
Adrian Buehlmann committed
242
243
244
    def setContext(self, ctx):
        self._ctx = ctx
        self._p_rev = None
245
        self.sci.setTabWidth(ctx._repo.tabwidth)
246
        self.actionAnnMode.setVisible(ctx.rev() != None)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
247
248
249
250

    def displayDiff(self, rev):
        if rev != self._p_rev:
            self.displayFile(rev=rev)
251

252
253
    def clearDisplay(self):
        self.sci.clear()
254
        self.blk.clear()
255
256
257
        # Setting the label to ' ' rather than clear() keeps the label
        # from disappearing during refresh, and tool layouts bouncing
        self.filenamelabel.setText(' ')
258
        self.extralabel.hide()
259
        self._diffs = []
260

261
    def displayFile(self, filename=None, rev=None, status=None):
Adrian Buehlmann's avatar
Adrian Buehlmann committed
262
        if filename is None:
263
            filename, status = self._filename, self._status
Adrian Buehlmann's avatar
Adrian Buehlmann committed
264
        else:
265
            self._filename, self._status = filename, status
266

Adrian Buehlmann's avatar
Adrian Buehlmann committed
267
268
        if rev is not None:
            self._p_rev = rev
269
270

        self.clearDisplay()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
271
        if filename is None:
272
            self.forceMode('file')
Adrian Buehlmann's avatar
Adrian Buehlmann committed
273
            return
274

275
276
277
278
        ctx = self._ctx
        repo = ctx._repo
        if self._p_rev is not None:
            ctx2 = repo[self._p_rev]
Adrian Buehlmann's avatar
Adrian Buehlmann committed
279
        else:
280
            ctx2 = None
281

282
        fd = FileData(ctx, ctx2, filename, status)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
283

284
285
        if fd.elabel:
            self.extralabel.setText(fd.elabel)
286
            self.extralabel.show()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
287
        else:
288
            self.extralabel.hide()
289
        self.filenamelabel.setText(fd.flabel)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
290

291
292
        if not fd.isValid():
            self.sci.setText(fd.error)
293
            self.forceMode('file')
294
            return
295

296
297
298
299
        if fd.diff and not fd.contents:
            self.forceMode('diff')
        elif fd.contents and not fd.diff:
            self.forceMode('file')
300
301
        elif not fd.contents and not fd.diff:
            self.forceMode('file')
302
303
304
305
        else:
            self.actionDiffMode.setEnabled(True)
            self.actionFileMode.setEnabled(True)
            self.actionAnnMode.setEnabled(True)
306
307
308
309
310
311
312
313
            if self._lostMode:
                if self._lostMode == 'diff':
                    self.actionDiffMode.trigger()
                elif self._lostMode == 'file':
                    self.actionFileMode.trigger()
                elif self._lostMode == 'ann':
                    self.actionAnnMode.trigger()
                self._lostMode = None
314

315
316
        if self._mode == 'diff':
            self.sci.setMarginWidth(1, 0)
Steve Borho's avatar
Steve Borho committed
317
            lexer = lexers.get_diff_lexer(self)
318
            self.sci.setLexer(lexer)
319
320
321
322
323
324
            # trim first three lines, for example:
            # diff -r f6bfc41af6d7 -r c1b18806486d tortoisehg/hgqt/thgrepo.py
            # --- a/tortoisehg/hgqt/thgrepo.py
            # +++ b/tortoisehg/hgqt/thgrepo.py
            noheader = fd.diff.split('\n', 3)[3]
            self.sci.setText(hglib.tounicode(noheader))
325
326
        elif fd.contents is None:
            return
327
328
        elif self._mode == 'ann':
            self.sci.setSource(filename, ctx.rev())
329
        else:
Steve Borho's avatar
Steve Borho committed
330
            lexer = lexers.get_lexer(filename, fd.contents, self)
331
            self.sci.setLexer(lexer)
332
            self.sci.setText(fd.contents)
333
            self.sci._updatemarginwidth()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
334

335
        self.highlightText(*self._lastSearch)
336
        uf = hglib.tounicode(self._filename)
Steve Borho's avatar
Steve Borho committed
337
        self.fileDisplayed.emit(uf, fd.contents or QString())
338

339
340
        if self._mode == 'file' and fd.contents and fd.olddata:
            # Update diff margin
Adrian Buehlmann's avatar
Adrian Buehlmann committed
341
342
343
            if self.timer.isActive():
                self.timer.stop()

344
345
            olddata = fd.olddata.splitlines()
            newdata = fd.contents.splitlines()
346
            self._diff = difflib.SequenceMatcher(None, olddata, newdata)
347
348
            self.blk.syncPageStep()
            self.timer.start()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
349
350

    def nextDiff(self):
351
352
353
354
355
356
357
358
359
360
361
        if self._mode == 'diff' or not self._diffs:
            self.actionNextDiff.setEnabled(False)
            self.actionPrevDiff.setEnabled(False)
            return
        row, column = self.sci.getCursorPosition()
        for i, (lo, hi) in enumerate(self._diffs):
            if lo > row:
                last = (i == (len(self._diffs)-1))
                self.sci.setCursorPosition(lo, 0)
                self.sci.verticalScrollBar().setValue(lo)
                break
362
        else:
363
            last = True
364
365
        self.actionNextDiff.setEnabled(not last)
        self.actionPrevDiff.setEnabled(True)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
366
367

    def prevDiff(self):
368
369
370
371
372
373
374
375
376
377
378
        if self._mode == 'diff' or not self._diffs:
            self.actionNextDiff.setEnabled(False)
            self.actionPrevDiff.setEnabled(False)
            return
        row, column = self.sci.getCursorPosition()
        for i, (lo, hi) in enumerate(reversed(self._diffs)):
            if hi < row:
                first = (i == (len(self._diffs)-1))
                self.sci.setCursorPosition(lo, 0)
                self.sci.verticalScrollBar().setValue(lo)
                break
379
        else:
380
            first = True
381
382
        self.actionNextDiff.setEnabled(True)
        self.actionPrevDiff.setEnabled(not first)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398

    def nextLine(self):
        x, y = self.sci.getCursorPosition()
        self.sci.setCursorPosition(x+1, y)

    def prevLine(self):
        x, y = self.sci.getCursorPosition()
        self.sci.setCursorPosition(x-1, y)

    def nextCol(self):
        x, y = self.sci.getCursorPosition()
        self.sci.setCursorPosition(x, y+1)

    def prevCol(self):
        x, y = self.sci.getCursorPosition()
        self.sci.setCursorPosition(x, y-1)
Steve Borho's avatar
Steve Borho committed
399

Adrian Buehlmann's avatar
Adrian Buehlmann committed
400
401
402
    def nDiffs(self):
        return len(self._diffs)

403
    @pyqtSlot(unicode, object)
404
405
    @pyqtSlot(unicode, object, int)
    def sourceChanged(self, path, rev, line=None):
406
        self.revisionSelected.emit(rev)
407

408
409
410
411
412
413
414
    @pyqtSlot(unicode, object, int)
    def editSelected(self, path, rev, line):
        """Open editor to show the specified file"""
        repo = self._ctx._repo
        path = hglib.fromunicode(path)
        base = visdiff.snapshot(repo, [path], repo[rev])[0]
        files = [os.path.join(base, path)]
415
416
        pattern = hglib.fromunicode(self._lastSearch[0])
        wctxactions.edit(self, repo.ui, repo, files, line, pattern)
417

418
419
    @pyqtSlot(unicode, bool, bool, bool)
    def find(self, exp, icase=True, wrap=False, forward=True):
420
        self.sci.find(exp, icase, wrap, forward)
421
422
423

    @pyqtSlot(unicode, bool)
    def highlightText(self, match, icase=False):
424
        self._lastSearch = match, icase
425
        self.sci.highlightText(match, icase)
426

Adrian Buehlmann's avatar
Adrian Buehlmann committed
427
428
429
430
431
432
433
434
435
436
437
    def verticalScrollBar(self):
        return self.sci.verticalScrollBar()

    def idle_fill_files(self):
        # we make a burst of diff-lines computed at once, but we
        # disable GUI updates for efficiency reasons, then only
        # refresh GUI at the end of the burst
        self.sci.setUpdatesEnabled(False)
        self.blk.setUpdatesEnabled(False)
        for n in range(30): # burst pool
            if self._diff is None or not self._diff.get_opcodes():
438
439
                self.actionNextDiff.setEnabled(bool(self._diffs))
                self.actionPrevDiff.setEnabled(False)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
440
441
442
443
444
445
446
447
448
449
450
451
                self._diff = None
                self.timer.stop()
                break

            tag, alo, ahi, blo, bhi = self._diff.get_opcodes().pop(0)
            if tag == 'replace':
                self._diffs.append([blo, bhi])
                self.blk.addBlock('x', blo, bhi)
                for i in range(blo, bhi):
                    self.sci.markerAdd(i, self.markertriangle)

            elif tag == 'delete':
452
453
                # You cannot effectively show deleted lines in a single
                # pane display.  They do not exist.
Adrian Buehlmann's avatar
Adrian Buehlmann committed
454
                pass
455
456
                # self._diffs.append([blo, bhi])
                # self.blk.addBlock('-', blo, bhi)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
457
                # for i in range(alo, ahi):
458
                #      self.sci.markerAdd(i, self.markerminus)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
459
460
461
462
463
464
465
466
467
468
469
470
471

            elif tag == 'insert':
                self._diffs.append([blo, bhi])
                self.blk.addBlock('+', blo, bhi)
                for i in range(blo, bhi):
                    self.sci.markerAdd(i, self.markerplus)

            elif tag == 'equal':
                pass

            else:
                raise ValueError, 'unknown tag %r' % (tag,)

472
        # ok, enable GUI refresh for code viewers and diff-block displayers
Adrian Buehlmann's avatar
Adrian Buehlmann committed
473
474
        self.sci.setUpdatesEnabled(True)
        self.blk.setUpdatesEnabled(True)
475
476


477
478
479
480
481
482
483
484
485
486
class FileData(object):
    def __init__(self, ctx, ctx2, wfile, status=None):
        self.contents = None
        self.error = None
        self.olddata = None
        self.diff = None
        self.flabel = u''
        self.elabel = u''
        self.readStatus(ctx, ctx2, wfile, status)

487
488
489
490
    def checkMaxDiff(self, ctx, wfile):
        p = _('File or diffs not displayed: ')
        try:
            fctx = ctx.filectx(wfile)
491
492
493
494
495
496
497
            if ctx.rev() is None:
                size = fctx.size()
            else:
                # fctx.size() can read all data into memory in rename cases so
                # we read the size directly from the filelog, this is deeper
                # under the API than I prefer to go, but seems necessary
                size = fctx._filelog.rawsize(fctx.filerev())
498
499
500
        except (EnvironmentError, error.LookupError), e:
            self.error = p + hglib.tounicode(str(e))
            return None
Steve Borho's avatar
Steve Borho committed
501
        if size > ctx._repo.maxdiff:
502
503
504
505
506
507
508
            self.error = p + _('File is larger than the specified max size.\n')
            return None
        try:
            data = fctx.data()
            if '\0' in data:
                self.error = p + _('File is binary.\n')
                return None
509
510
        except EnvironmentError, e:
            self.error = p + hglib.tounicode(str(e))
511
512
513
            return None
        return fctx, data

514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
    def isValid(self):
        return self.error is None

    def readStatus(self, ctx, ctx2, wfile, status):
        def getstatus(repo, n1, n2, wfile):
            m = match.exact(repo.root, repo.getcwd(), [wfile])
            modified, added, removed = repo.status(n1, n2, match=m)[:3]
            if wfile in modified:
                return 'M'
            if wfile in added:
                return 'A'
            if wfile in removed:
                return 'R'
            return None

        repo = ctx._repo
        self.flabel += u'<b>%s</b>' % hglib.tounicode(wfile)

532
        if isinstance(ctx, patchctx.patchctx):
533
534
535
536
537
538
539
540
541
542
            self.diff = ctx.thgmqpatchdata(wfile)
            flags = ctx.flags(wfile)
            if flags in ('x', '-'):
                lbl = _("exec mode has been <font color='red'>%s</font>")
                change = (flags == 'x') and _('set') or _('unset')
                self.elabel = lbl % change
            elif flags == 'l':
                self.flabel += _(' <i>(is a symlink)</i>')
            return

543
        absfile = repo.wjoin(wfile)
544
545
546
547
548
549
        if (wfile in ctx and 'l' in ctx.flags(wfile)) or \
           os.path.islink(absfile):
            if wfile in ctx:
                data = ctx[wfile].data()
            else:
                data = os.readlink(absfile)
550
551
552
553
            self.contents = hglib.tounicode(data)
            self.flabel += _(' <i>(is a symlink)</i>')
            return

554
555
556
557
558
559
560
        if status is None:
            status = getstatus(repo, ctx.p1().node(), ctx.node(), wfile)
        if ctx2 is None:
            ctx2 = ctx.p1()

        if status == 'S':
            try:
Steve Borho's avatar
Steve Borho committed
561
                from mercurial import subrepo, commands
562
563
564
565
566
567
568
                assert(ctx.rev() is None)
                out = []
                _ui = uimod.ui()
                sroot = repo.wjoin(wfile)
                srepo = hg.repository(_ui, path=sroot)
                srev = ctx.substate.get(wfile, subrepo.nullstate)[1]
                sactual = srepo['.'].hex()
569
                _ui.pushbuffer()
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
                commands.status(_ui, srepo)
                data = _ui.popbuffer()
                if data:
                    out.append(_('File Status:\n'))
                    out.append(data)
                    out.append('\n')
                if srev == '':
                    out.append(_('New subrepository\n\n'))
                elif srev != sactual:
                    out.append(_('Revision has changed from:\n\n'))
                    opts = {'date':None, 'user':None, 'rev':[srev]}
                    _ui.pushbuffer()
                    commands.log(_ui, srepo, **opts)
                    out.append(hglib.tounicode(_ui.popbuffer()))
                    out.append(_('To:\n'))
                    opts['rev'] = [sactual]
                    _ui.pushbuffer()
                    commands.log(_ui, srepo, **opts)
                    out.append(hglib.tounicode(_ui.popbuffer()))
                self.contents = u''.join(out)
Steve Borho's avatar
Steve Borho committed
590
591
592
                self.flabel += _(' <i>(is a dirty sub-repository)</i>')
                lbl = u' <a href="subrepo:%s">%s...</a>'
                self.flabel += lbl % (hglib.tounicode(sroot), _('open'))
593
594
            except (error.RepoError, util.Abort), e:
                self.error = _('Not a Mercurial subrepo, not previewable')
595
596
            return

597
598
599
        # TODO: elif check if a subdirectory (for manifest tool)

        if status in ('R', '!'):
600
601
602
603
604
605
            if wfile in ctx.p1():
                newdata = ctx.p1()[wfile].data()
                self.contents = hglib.tounicode(newdata)
                self.flabel += _(' <i>(was deleted)</i>')
            else:
                self.flabel += _(' <i>(was added, now missing)</i>')
606
            return
607
608

        if status in ('I', '?'):
609
610
611
612
613
614
615
616
617
618
619
            try:
                data = open(repo.wjoin(wfile), 'r').read()
                if '\0' in data:
                    self.error = 'binary file'
                else:
                    self.contents = hglib.tounicode(data)
                    self.flabel += _(' <i>(is unversioned)</i>')
            except EnvironmentError, e:
                self.error = hglib.tounicode(str(e))
            return

620
621
622
623
624
625
626
627
628
        if status in ('M', 'A'):
            res = self.checkMaxDiff(ctx, wfile)
            if res is None:
                return
            fctx, newdata = res
            self.contents = hglib.tounicode(newdata)
            change = None
            for pfctx in fctx.parents():
                if 'x' in fctx.flags() and 'x' not in pfctx.flags():
629
                    change = _('set')
630
                elif 'x' not in fctx.flags() and 'x' in pfctx.flags():
631
                    change = _('unset')
632
633
            if change:
                lbl = _("exec mode has been <font color='red'>%s</font>")
George Marrows's avatar
George Marrows committed
634
                self.elabel = lbl % change
635
636
637
638
639
640
641
642

        if status == 'A':
            renamed = fctx.renamed()
            if not renamed:
                self.flabel += _(' <i>(was added)</i>')
                return

            oldname, node = renamed
643
            fr = hglib.tounicode(oldname)
644
645
646
            self.flabel += _(' <i>(renamed from %s)</i>') % fr
            olddata = repo.filectx(oldname, fileid=node).data()
        elif status == 'M':
647
648
649
650
            if wfile not in ctx2:
                # merge situation where file was added in other branch
                self.flabel += _(' <i>(was added)</i>')
                return
651
652
653
654
655
656
            oldname = wfile
            olddata = ctx2[wfile].data()
        else:
            return

        self.olddata = olddata
657
658
659
660
661
662
663
        newdate = util.datestr(ctx.date())
        olddate = util.datestr(ctx2.date())
        revs = [str(ctx), str(ctx2)]
        diffopts = patch.diffopts(repo.ui, {})
        diffopts.git = False
        self.diff = mdiff.unidiff(olddata, olddate, newdata, newdate,
                                  oldname, wfile, revs, diffopts)