Read about our upcoming Code of Conduct on this issue

fileview.py 24.8 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
45
    revForDiffChanged = pyqtSignal(int)
    filled = pyqtSignal()
46

47
48
49
50
51
52
53
    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):
54
55
        QFrame.__init__(self, parent)
        framelayout = QVBoxLayout(self)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
56
57
58
        framelayout.setContentsMargins(0,0,0,0)
        framelayout.setSpacing(0)

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

63
        self.topLayout = QVBoxLayout()
64

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

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

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

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

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

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

        self.blk = blockmatcher.BlockList(self)
Steve Borho's avatar
Steve Borho committed
96
        hbox.addWidget(self.blk)
97
98
        self.blk.setVisible(False)

99
        self._stacked = QStackedWidget()
Steve Borho's avatar
Steve Borho committed
100
        hbox.addWidget(self._stacked, 1)
101

102
        self.sci = qscilib.Scintilla(self)
103
        self.blk.linkScrollBar(self.sci.verticalScrollBar())
104
105
        self._stacked.addWidget(self.sci)

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

114
115
116
117
118
119
120
121
122
123
124
125
        # 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)

126
127
128
129
        self._annotate = annotate.AnnotateView(repo, self)
        for name in ('searchRequested', 'editSelected', 'grepRequested'):
            getattr(self._annotate, name).connect(getattr(self, name))
        self._annotate.revisionHint.connect(self.showMessage)
130
        self._annotate.sourceChanged.connect(self.sourceChanged)
131
132
        self._annotate.setAnnotationEnabled(True)
        self._stacked.addWidget(self._annotate)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
133

134
135
136
137
138
139
        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
140
141
        self._ctx = None
        self._filename = None
142
        self._status = None
Adrian Buehlmann's avatar
Adrian Buehlmann committed
143
        self._find_text = None
144
145
        self._mode = None
        self._lostMode = None
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160

        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)
161
162
        self.actionNextDiff = QAction(qtlib.geticon('down'),
                                      'Next diff (alt+down)', self)
163
164
        self.actionNextDiff.setShortcut('Alt+Down')
        self.actionNextDiff.triggered.connect(self.nextDiff)
165
166
        self.actionPrevDiff = QAction(qtlib.geticon('up'),
                                      'Previous diff (alt+up)', self)
167
168
169
170
171
        self.actionPrevDiff.setShortcut('Alt+Up')
        self.actionPrevDiff.triggered.connect(self.prevDiff)

        self.forceMode('diff')

172
173
174
175
        self.actionFind = self.searchbar.toggleViewAction()
        self.actionFind.setIcon(qtlib.geticon('edit-find'))
        self.actionFind.setShortcut(QKeySequence.Find)

176
177
178
179
180
181
182
        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)
183
184
        tb.addSeparator()
        tb.addAction(self.actionFind)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
185

186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
        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)

203
        self.timer = QTimer()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
204
        self.timer.setSingleShot(False)
205
        self.timer.timeout.connect(self.idle_fill_files)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
206

207
208
209
210
211
212
213
214
215
216
    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)

217
218
219
220
    @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())]
221
        self.actionNextDiff.setEnabled(mode == 'file')
222
        self.actionPrevDiff.setEnabled(False)
223
        self.blk.setVisible(mode == 'file')
224
225
226
227
        if mode == 'ann':
            self._stacked.setCurrentWidget(self._annotate)
        else:
            self._stacked.setCurrentWidget(self.sci)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
228
229
        if mode != self._mode:
            self._mode = mode
230
231
            if not self._lostMode:
                self.displayFile()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
232

233
234
235
    def forceMode(self, mode):
        'Force into file or diff mode, based on content constaints'
        assert mode in ('diff', 'file')
236
237
        if self._lostMode is None:
            self._lostMode = self._mode
238
239
240
241
242
243
244
245
246
247
248
        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')
249
        self._stacked.setCurrentWidget(self.sci)
250

Adrian Buehlmann's avatar
Adrian Buehlmann committed
251
252
253
    def setContext(self, ctx):
        self._ctx = ctx
        self._p_rev = None
254
        self.sci.setTabWidth(ctx._repo.tabwidth)
255
        self.actionAnnMode.setVisible(ctx.rev() != None)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
256
257
258
259

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

261
262
    def clearDisplay(self):
        self.sci.clear()
263
        self.blk.clear()
264
265
266
        # Setting the label to ' ' rather than clear() keeps the label
        # from disappearing during refresh, and tool layouts bouncing
        self.filenamelabel.setText(' ')
267
        self.extralabel.hide()
268
269
        self.sci.setMarginLineNumbers(1, False)
        self.sci.setMarginWidth(1, '')
270
        self._diffs = []
271

272
    def displayFile(self, filename=None, rev=None, status=None):
Adrian Buehlmann's avatar
Adrian Buehlmann committed
273
        if filename is None:
274
            filename, status = self._filename, self._status
Adrian Buehlmann's avatar
Adrian Buehlmann committed
275
        else:
276
            self._filename, self._status = filename, status
277

Adrian Buehlmann's avatar
Adrian Buehlmann committed
278
279
        if rev is not None:
            self._p_rev = rev
280
            self.revForDiffChanged.emit(rev)
281
282

        self.clearDisplay()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
283
        if filename is None:
284
            self.forceMode('file')
Adrian Buehlmann's avatar
Adrian Buehlmann committed
285
            return
286

287
288
289
290
        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
291
        else:
292
            ctx2 = None
293

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

296
297
        if fd.elabel:
            self.extralabel.setText(fd.elabel)
298
            self.extralabel.show()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
299
        else:
300
            self.extralabel.hide()
301
        self.filenamelabel.setText(fd.flabel)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
302

303
304
        if not fd.isValid():
            self.sci.setText(fd.error)
305
            self.forceMode('file')
306
            return
307

308
309
310
311
312
313
314
315
        if fd.diff and not fd.contents:
            self.forceMode('diff')
        elif fd.contents and not fd.diff:
            self.forceMode('file')
        else:
            self.actionDiffMode.setEnabled(True)
            self.actionFileMode.setEnabled(True)
            self.actionAnnMode.setEnabled(True)
316
317
318
319
320
321
322
323
            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
324

325
326
327
        if self._mode == 'ann':
            self._annotate.setSource(filename, ctx.rev())
        elif self._mode == 'diff':
Steve Borho's avatar
Steve Borho committed
328
            lexer = lexers.get_diff_lexer(self)
329
            self.sci.setLexer(lexer)
330
331
332
333
334
335
            # 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))
336
337
        elif fd.contents is None:
            return
338
        else:
Steve Borho's avatar
Steve Borho committed
339
            lexer = lexers.get_lexer(filename, fd.contents, self)
340
            self.sci.setLexer(lexer)
341
            nlines = fd.contents.count('\n')
342
343
            # margin 1 is used for line numbers
            self.sci.setMarginLineNumbers(1, True)
344
            self.sci.setMarginWidth(1, str(nlines)+'0')
345
            self.sci.setText(fd.contents)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
346

347
        uf = hglib.tounicode(self._filename)
Steve Borho's avatar
Steve Borho committed
348
        self.fileDisplayed.emit(uf, fd.contents or QString())
349

350
351
        if self._mode == 'file' and fd.contents and fd.olddata:
            # Update diff margin
Adrian Buehlmann's avatar
Adrian Buehlmann committed
352
353
354
            if self.timer.isActive():
                self.timer.stop()

355
356
            olddata = fd.olddata.splitlines()
            newdata = fd.contents.splitlines()
357
            self._diff = difflib.SequenceMatcher(None, olddata, newdata)
358
359
            self.blk.syncPageStep()
            self.timer.start()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
360
361

    def nextDiff(self):
362
363
364
365
366
367
368
369
370
371
372
        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
373
        else:
374
            last = True
375
376
        self.actionNextDiff.setEnabled(not last)
        self.actionPrevDiff.setEnabled(True)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
377
378

    def prevDiff(self):
379
380
381
382
383
384
385
386
387
388
389
        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
390
        else:
391
            first = True
392
393
        self.actionNextDiff.setEnabled(True)
        self.actionPrevDiff.setEnabled(not first)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409

    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
410

Adrian Buehlmann's avatar
Adrian Buehlmann committed
411
412
413
    def nDiffs(self):
        return len(self._diffs)

414
    @pyqtSlot(unicode, object)
415
416
417
418
    @pyqtSlot(unicode, object, int)
    def sourceChanged(self, path, rev, line=None):
        self.revForDiffChanged.emit(rev)

419
420
421
422
423
424
425
426
427
    @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)]
        wctxactions.edit(self, repo.ui, repo, files, line, self._find_text)

428
429
    @pyqtSlot(unicode, bool, bool, bool)
    def find(self, exp, icase=True, wrap=False, forward=True):
430
        self._find_text = hglib.fromunicode(exp)
431
432
433
434
        if self._mode == 'ann':
            self._annotate.find(exp, icase, wrap, forward)
        else:
            self.sci.find(exp, icase, wrap, forward)
435
436
437

    @pyqtSlot(unicode, bool)
    def highlightText(self, match, icase=False):
438
439
440
441
        if self._mode == 'ann':
            self._annotate.highlightText(match, icase)
        else:
            self.sci.highlightText(match, icase)
442

Adrian Buehlmann's avatar
Adrian Buehlmann committed
443
444
445
446
447
448
449
450
451
452
453
    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():
454
455
                self.actionNextDiff.setEnabled(bool(self._diffs))
                self.actionPrevDiff.setEnabled(False)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
456
457
                self._diff = None
                self.timer.stop()
458
                self.filled.emit()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
459
460
461
462
463
464
465
466
467
468
                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':
469
470
                # You cannot effectively show deleted lines in a single
                # pane display.  They do not exist.
Adrian Buehlmann's avatar
Adrian Buehlmann committed
471
                pass
472
473
                # self._diffs.append([blo, bhi])
                # self.blk.addBlock('-', blo, bhi)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
474
                # for i in range(alo, ahi):
475
                #      self.sci.markerAdd(i, self.markerminus)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
476
477
478
479
480
481
482
483
484
485
486
487
488

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

489
        # ok, enable GUI refresh for code viewers and diff-block displayers
Adrian Buehlmann's avatar
Adrian Buehlmann committed
490
491
        self.sci.setUpdatesEnabled(True)
        self.blk.setUpdatesEnabled(True)
492
493


494
495
496
497
498
499
500
501
502
503
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)

504
505
506
507
    def checkMaxDiff(self, ctx, wfile):
        p = _('File or diffs not displayed: ')
        try:
            fctx = ctx.filectx(wfile)
508
509
510
511
512
513
514
            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())
515
516
517
        except (EnvironmentError, error.LookupError), e:
            self.error = p + hglib.tounicode(str(e))
            return None
Steve Borho's avatar
Steve Borho committed
518
        if size > ctx._repo.maxdiff:
519
520
521
522
523
524
525
            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
526
527
        except EnvironmentError, e:
            self.error = p + hglib.tounicode(str(e))
528
529
530
            return None
        return fctx, data

531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
    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)

549
        if isinstance(ctx, patchctx.patchctx):
550
551
552
553
554
555
556
557
558
559
            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

560
        absfile = repo.wjoin(wfile)
561
562
563
564
565
566
        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)
567
568
569
570
            self.contents = hglib.tounicode(data)
            self.flabel += _(' <i>(is a symlink)</i>')
            return

571
572
573
574
575
576
577
        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
578
                from mercurial import subrepo, commands
579
580
581
582
583
584
585
                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()
586
                _ui.pushbuffer()
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
                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
607
608
609
                self.flabel += _(' <i>(is a dirty sub-repository)</i>')
                lbl = u' <a href="subrepo:%s">%s...</a>'
                self.flabel += lbl % (hglib.tounicode(sroot), _('open'))
610
611
            except (error.RepoError, util.Abort), e:
                self.error = _('Not a Mercurial subrepo, not previewable')
612
613
            return

614
615
616
        # TODO: elif check if a subdirectory (for manifest tool)

        if status in ('R', '!'):
617
618
619
620
621
622
            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>')
623
            return
624
625

        if status in ('I', '?'):
626
627
628
629
630
631
632
633
634
635
636
            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

637
638
639
640
641
642
643
644
645
        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():
646
                    change = _('set')
647
                elif 'x' not in fctx.flags() and 'x' in pfctx.flags():
648
                    change = _('unset')
649
650
            if change:
                lbl = _("exec mode has been <font color='red'>%s</font>")
George Marrows's avatar
George Marrows committed
651
                self.elabel = lbl % change
652
653
654
655
656
657
658
659

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

            oldname, node = renamed
660
            fr = hglib.tounicode(oldname)
661
662
663
            self.flabel += _(' <i>(renamed from %s)</i>') % fr
            olddata = repo.filectx(oldname, fileid=node).data()
        elif status == 'M':
664
665
666
667
            if wfile not in ctx2:
                # merge situation where file was added in other branch
                self.flabel += _(' <i>(was added)</i>')
                return
668
669
670
671
672
673
            oldname = wfile
            olddata = ctx2[wfile].data()
        else:
            return

        self.olddata = olddata
674
675
676
677
678
679
680
        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)