PageRenderTime 68ms CodeModel.GetById 18ms app.highlight 43ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/fileview.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 663 lines | 614 code | 26 blank | 23 comment | 6 complexity | c2ad430b1a844051014acc122a84e55c MD5 | raw file
  1# Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE).
  2# http://www.logilab.fr/ -- mailto:contact@logilab.fr
  3#
  4# This program is free software; you can redistribute it and/or modify it under
  5# the terms of the GNU General Public License as published by the Free Software
  6# Foundation; either version 2 of the License, or (at your option) any later
  7# version.
  8#
  9# This program is distributed in the hope that it will be useful, but WITHOUT
 10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 12#
 13# You should have received a copy of the GNU General Public License along with
 14# this program; if not, write to the Free Software Foundation, Inc.,
 15# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 16"""
 17Qt4 high level widgets for hg repo changelogs and filelogs
 18"""
 19
 20import os
 21import difflib
 22import re
 23
 24from mercurial import hg, error, match, patch, util
 25from mercurial import ui as uimod, mdiff
 26
 27from PyQt4.QtCore import *
 28from PyQt4.QtGui import *
 29from PyQt4 import Qsci
 30
 31from tortoisehg.util import hglib, patchctx
 32from tortoisehg.hgqt.i18n import _
 33from tortoisehg.hgqt import annotate, qscilib, qtlib, blockmatcher, lexers
 34from tortoisehg.hgqt import visdiff, wctxactions
 35
 36qsci = Qsci.QsciScintilla
 37
 38class HgFileView(QFrame):
 39    """file diff and content viewer"""
 40
 41    linkActivated = pyqtSignal(QString)
 42    fileDisplayed = pyqtSignal(QString, QString)
 43    showMessage = pyqtSignal(QString)
 44    revisionSelected = pyqtSignal(int)
 45
 46    searchRequested = pyqtSignal(unicode)
 47    """Emitted (pattern) when user request to search content"""
 48
 49    grepRequested = pyqtSignal(unicode, dict)
 50    """Emitted (pattern, opts) when user request to search changelog"""
 51
 52    def __init__(self, repo, parent):
 53        QFrame.__init__(self, parent)
 54        framelayout = QVBoxLayout(self)
 55        framelayout.setContentsMargins(0,0,0,0)
 56        framelayout.setSpacing(0)
 57
 58        l = QHBoxLayout()
 59        l.setContentsMargins(0,0,0,0)
 60        l.setSpacing(0)
 61
 62        self.topLayout = QVBoxLayout()
 63
 64        self.labelhbox = hbox = QHBoxLayout()
 65        hbox.setContentsMargins(0,0,0,0)
 66        hbox.setSpacing(2)
 67        self.topLayout.addLayout(hbox)
 68
 69        self.diffToolbar = QToolBar(_('Diff Toolbar'))
 70        self.diffToolbar.setIconSize(QSize(16,16))
 71        hbox.addWidget(self.diffToolbar)
 72
 73        self.filenamelabel = w = QLabel()
 74        w.setWordWrap(True)
 75        f = w.textInteractionFlags()
 76        w.setTextInteractionFlags(f | Qt.TextSelectableByMouse)
 77        w.linkActivated.connect(self.linkActivated)
 78        hbox.addWidget(w, 1)
 79
 80        self.extralabel = w = QLabel()
 81        w.setWordWrap(True)
 82        w.linkActivated.connect(self.linkActivated)
 83        self.topLayout.addWidget(w)
 84        w.hide()
 85
 86        framelayout.addLayout(self.topLayout)
 87        framelayout.addLayout(l, 1)
 88
 89        hbox = QHBoxLayout()
 90        hbox.setContentsMargins(0, 0, 0, 0)
 91        hbox.setSpacing(0)
 92        l.addLayout(hbox)
 93
 94        self.blk = blockmatcher.BlockList(self)
 95        self.sci = annotate.AnnotateView(repo, self)
 96        hbox.addWidget(self.blk)
 97        hbox.addWidget(self.sci, 1)
 98
 99        for name in ('searchRequested', 'editSelected', 'grepRequested'):
100            getattr(self.sci, name).connect(getattr(self, name))
101        self.sci.revisionHint.connect(self.showMessage)
102        self.sci.sourceChanged.connect(self.sourceChanged)
103        self.sci.setAnnotationEnabled(False)
104
105        self.blk.linkScrollBar(self.sci.verticalScrollBar())
106        self.blk.setVisible(False)
107
108        self.sci.setFrameStyle(0)
109        self.sci.setReadOnly(True)
110        self.sci.setUtf8(True)
111        self.sci.installEventFilter(qscilib.KeyPressInterceptor(self))
112        self.sci.setContextMenuPolicy(Qt.CustomContextMenu)
113        self.sci.customContextMenuRequested.connect(self.menuRequested)
114        self.sci.setCaretLineVisible(False)
115
116        # define markers for colorize zones of diff
117        self.markerplus = self.sci.markerDefine(qsci.Background)
118        self.markerminus = self.sci.markerDefine(qsci.Background)
119        self.markertriangle = self.sci.markerDefine(qsci.Background)
120        self.sci.setMarkerBackgroundColor(QColor('#B0FFA0'), self.markerplus)
121        self.sci.setMarkerBackgroundColor(QColor('#A0A0FF'), self.markerminus)
122        self.sci.setMarkerBackgroundColor(QColor('#FFA0A0'), self.markertriangle)
123
124        # hide margin 0 (markers)
125        self.sci.setMarginType(0, qsci.SymbolMargin)
126        self.sci.setMarginWidth(0, 0)
127
128        self.searchbar = qscilib.SearchToolBar(hidable=True)
129        self.searchbar.hide()
130        self.searchbar.searchRequested.connect(self.find)
131        self.searchbar.conditionChanged.connect(self.highlightText)
132        self.layout().addWidget(self.searchbar)
133
134        self._ctx = None
135        self._filename = None
136        self._status = None
137        self._mode = None
138        self._lostMode = None
139        self._lastSearch = u'', False
140
141        self.actionDiffMode = QAction('Diff', self)
142        self.actionDiffMode.setCheckable(True)
143        self.actionFileMode = QAction('File', self)
144        self.actionFileMode.setCheckable(True)
145        self.actionAnnMode = QAction('Ann', self)
146        self.actionAnnMode.setCheckable(True)
147
148        self.modeToggleGroup = QActionGroup(self)
149        self.modeToggleGroup.addAction(self.actionDiffMode)
150        self.modeToggleGroup.addAction(self.actionFileMode)
151        self.modeToggleGroup.addAction(self.actionAnnMode)
152        self.modeToggleGroup.triggered.connect(self.setMode)
153
154        # Next/Prev diff (in full file mode)
155        self.actionNextDiff = QAction(qtlib.geticon('down'),
156                                      'Next diff (alt+down)', self)
157        self.actionNextDiff.setShortcut('Alt+Down')
158        self.actionNextDiff.triggered.connect(self.nextDiff)
159        self.actionPrevDiff = QAction(qtlib.geticon('up'),
160                                      'Previous diff (alt+up)', self)
161        self.actionPrevDiff.setShortcut('Alt+Up')
162        self.actionPrevDiff.triggered.connect(self.prevDiff)
163
164        self.forceMode('diff')
165
166        self.actionFind = self.searchbar.toggleViewAction()
167        self.actionFind.setIcon(qtlib.geticon('edit-find'))
168        self.actionFind.setShortcut(QKeySequence.Find)
169
170        tb = self.diffToolbar
171        tb.addAction(self.actionDiffMode)
172        tb.addAction(self.actionFileMode)
173        tb.addAction(self.actionAnnMode)
174        tb.addSeparator()
175        tb.addAction(self.actionNextDiff)
176        tb.addAction(self.actionPrevDiff)
177        tb.addSeparator()
178        tb.addAction(self.actionFind)
179
180        self.actionNextLine = QAction('Next line', self)
181        self.actionNextLine.setShortcut(Qt.SHIFT + Qt.Key_Down)
182        self.actionNextLine.triggered.connect(self.nextLine)
183        self.addAction(self.actionNextLine)
184        self.actionPrevLine = QAction('Prev line', self)
185        self.actionPrevLine.setShortcut(Qt.SHIFT + Qt.Key_Up)
186        self.actionPrevLine.triggered.connect(self.prevLine)
187        self.addAction(self.actionPrevLine)
188        self.actionNextCol = QAction('Next column', self)
189        self.actionNextCol.setShortcut(Qt.SHIFT + Qt.Key_Right)
190        self.actionNextCol.triggered.connect(self.nextCol)
191        self.addAction(self.actionNextCol)
192        self.actionPrevCol = QAction('Prev column', self)
193        self.actionPrevCol.setShortcut(Qt.SHIFT + Qt.Key_Left)
194        self.actionPrevCol.triggered.connect(self.prevCol)
195        self.addAction(self.actionPrevCol)
196
197        self.timer = QTimer()
198        self.timer.setSingleShot(False)
199        self.timer.timeout.connect(self.idle_fill_files)
200
201    def menuRequested(self, point):
202        point = self.sci.mapToGlobal(point)
203        return self.sci.createStandardContextMenu().exec_(point)
204
205    def loadSettings(self, qs, prefix):
206        self.sci.loadSettings(qs, prefix)
207
208    def saveSettings(self, qs, prefix):
209        self.sci.saveSettings(qs, prefix)
210
211    @pyqtSlot(QAction)
212    def setMode(self, action):
213        'One of the mode toolbar buttons has been toggled'
214        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')
219        if mode != self._mode:
220            self._mode = mode
221            if not self._lostMode:
222                self.displayFile()
223
224    def forceMode(self, mode):
225        'Force into file or diff mode, based on content constaints'
226        assert mode in ('diff', 'file')
227        if self._lostMode is None:
228            self._lostMode = self._mode
229        self._mode = mode
230        if mode == 'diff':
231            self.actionDiffMode.setChecked(True)
232        else:
233            self.actionFileMode.setChecked(True)
234        self.actionDiffMode.setEnabled(False)
235        self.actionFileMode.setEnabled(False)
236        self.actionAnnMode.setEnabled(False)
237        self.actionNextDiff.setEnabled(False)
238        self.actionPrevDiff.setEnabled(False)
239        self.blk.setVisible(mode == 'file')
240        self.sci.setAnnotationEnabled(False)
241
242    def setContext(self, ctx):
243        self._ctx = ctx
244        self._p_rev = None
245        self.sci.setTabWidth(ctx._repo.tabwidth)
246        self.actionAnnMode.setVisible(ctx.rev() != None)
247
248    def displayDiff(self, rev):
249        if rev != self._p_rev:
250            self.displayFile(rev=rev)
251
252    def clearDisplay(self):
253        self.sci.clear()
254        self.blk.clear()
255        # Setting the label to ' ' rather than clear() keeps the label
256        # from disappearing during refresh, and tool layouts bouncing
257        self.filenamelabel.setText(' ')
258        self.extralabel.hide()
259        self._diffs = []
260
261    def displayFile(self, filename=None, rev=None, status=None):
262        if filename is None:
263            filename, status = self._filename, self._status
264        else:
265            self._filename, self._status = filename, status
266
267        if rev is not None:
268            self._p_rev = rev
269
270        self.clearDisplay()
271        if filename is None:
272            self.forceMode('file')
273            return
274
275        ctx = self._ctx
276        repo = ctx._repo
277        if self._p_rev is not None:
278            ctx2 = repo[self._p_rev]
279        else:
280            ctx2 = None
281
282        fd = FileData(ctx, ctx2, filename, status)
283
284        if fd.elabel:
285            self.extralabel.setText(fd.elabel)
286            self.extralabel.show()
287        else:
288            self.extralabel.hide()
289        self.filenamelabel.setText(fd.flabel)
290
291        if not fd.isValid():
292            self.sci.setText(fd.error)
293            self.forceMode('file')
294            return
295
296        if fd.diff and not fd.contents:
297            self.forceMode('diff')
298        elif fd.contents and not fd.diff:
299            self.forceMode('file')
300        elif not fd.contents and not fd.diff:
301            self.forceMode('file')
302        else:
303            self.actionDiffMode.setEnabled(True)
304            self.actionFileMode.setEnabled(True)
305            self.actionAnnMode.setEnabled(True)
306            if self._lostMode:
307                if self._lostMode == 'diff':
308                    self.actionDiffMode.trigger()
309                elif self._lostMode == 'file':
310                    self.actionFileMode.trigger()
311                elif self._lostMode == 'ann':
312                    self.actionAnnMode.trigger()
313                self._lostMode = None
314
315        if self._mode == 'diff':
316            self.sci.setMarginWidth(1, 0)
317            lexer = lexers.get_diff_lexer(self)
318            self.sci.setLexer(lexer)
319            # trim first three lines, for example:
320            # diff -r f6bfc41af6d7 -r c1b18806486d tortoisehg/hgqt/thgrepo.py
321            # --- a/tortoisehg/hgqt/thgrepo.py
322            # +++ b/tortoisehg/hgqt/thgrepo.py
323            noheader = fd.diff.split('\n', 3)[3]
324            self.sci.setText(hglib.tounicode(noheader))
325        elif fd.contents is None:
326            return
327        elif self._mode == 'ann':
328            self.sci.setSource(filename, ctx.rev())
329        else:
330            lexer = lexers.get_lexer(filename, fd.contents, self)
331            self.sci.setLexer(lexer)
332            self.sci.setText(fd.contents)
333            self.sci._updatemarginwidth()
334
335        self.highlightText(*self._lastSearch)
336        uf = hglib.tounicode(self._filename)
337        self.fileDisplayed.emit(uf, fd.contents or QString())
338
339        if self._mode == 'file' and fd.contents and fd.olddata:
340            # Update diff margin
341            if self.timer.isActive():
342                self.timer.stop()
343
344            olddata = fd.olddata.splitlines()
345            newdata = fd.contents.splitlines()
346            self._diff = difflib.SequenceMatcher(None, olddata, newdata)
347            self.blk.syncPageStep()
348            self.timer.start()
349
350    def nextDiff(self):
351        if self._mode == 'diff' or not self._diffs:
352            self.actionNextDiff.setEnabled(False)
353            self.actionPrevDiff.setEnabled(False)
354            return
355        row, column = self.sci.getCursorPosition()
356        for i, (lo, hi) in enumerate(self._diffs):
357            if lo > row:
358                last = (i == (len(self._diffs)-1))
359                self.sci.setCursorPosition(lo, 0)
360                self.sci.verticalScrollBar().setValue(lo)
361                break
362        else:
363            last = True
364        self.actionNextDiff.setEnabled(not last)
365        self.actionPrevDiff.setEnabled(True)
366
367    def prevDiff(self):
368        if self._mode == 'diff' or not self._diffs:
369            self.actionNextDiff.setEnabled(False)
370            self.actionPrevDiff.setEnabled(False)
371            return
372        row, column = self.sci.getCursorPosition()
373        for i, (lo, hi) in enumerate(reversed(self._diffs)):
374            if hi < row:
375                first = (i == (len(self._diffs)-1))
376                self.sci.setCursorPosition(lo, 0)
377                self.sci.verticalScrollBar().setValue(lo)
378                break
379        else:
380            first = True
381        self.actionNextDiff.setEnabled(True)
382        self.actionPrevDiff.setEnabled(not first)
383
384    def nextLine(self):
385        x, y = self.sci.getCursorPosition()
386        self.sci.setCursorPosition(x+1, y)
387
388    def prevLine(self):
389        x, y = self.sci.getCursorPosition()
390        self.sci.setCursorPosition(x-1, y)
391
392    def nextCol(self):
393        x, y = self.sci.getCursorPosition()
394        self.sci.setCursorPosition(x, y+1)
395
396    def prevCol(self):
397        x, y = self.sci.getCursorPosition()
398        self.sci.setCursorPosition(x, y-1)
399
400    def nDiffs(self):
401        return len(self._diffs)
402
403    @pyqtSlot(unicode, object)
404    @pyqtSlot(unicode, object, int)
405    def sourceChanged(self, path, rev, line=None):
406        self.revisionSelected.emit(rev)
407
408    @pyqtSlot(unicode, object, int)
409    def editSelected(self, path, rev, line):
410        """Open editor to show the specified file"""
411        repo = self._ctx._repo
412        path = hglib.fromunicode(path)
413        base = visdiff.snapshot(repo, [path], repo[rev])[0]
414        files = [os.path.join(base, path)]
415        pattern = hglib.fromunicode(self._lastSearch[0])
416        wctxactions.edit(self, repo.ui, repo, files, line, pattern)
417
418    @pyqtSlot(unicode, bool, bool, bool)
419    def find(self, exp, icase=True, wrap=False, forward=True):
420        self.sci.find(exp, icase, wrap, forward)
421
422    @pyqtSlot(unicode, bool)
423    def highlightText(self, match, icase=False):
424        self._lastSearch = match, icase
425        self.sci.highlightText(match, icase)
426
427    def verticalScrollBar(self):
428        return self.sci.verticalScrollBar()
429
430    def idle_fill_files(self):
431        # we make a burst of diff-lines computed at once, but we
432        # disable GUI updates for efficiency reasons, then only
433        # refresh GUI at the end of the burst
434        self.sci.setUpdatesEnabled(False)
435        self.blk.setUpdatesEnabled(False)
436        for n in range(30): # burst pool
437            if self._diff is None or not self._diff.get_opcodes():
438                self.actionNextDiff.setEnabled(bool(self._diffs))
439                self.actionPrevDiff.setEnabled(False)
440                self._diff = None
441                self.timer.stop()
442                break
443
444            tag, alo, ahi, blo, bhi = self._diff.get_opcodes().pop(0)
445            if tag == 'replace':
446                self._diffs.append([blo, bhi])
447                self.blk.addBlock('x', blo, bhi)
448                for i in range(blo, bhi):
449                    self.sci.markerAdd(i, self.markertriangle)
450
451            elif tag == 'delete':
452                # You cannot effectively show deleted lines in a single
453                # pane display.  They do not exist.
454                pass
455                # self._diffs.append([blo, bhi])
456                # self.blk.addBlock('-', blo, bhi)
457                # for i in range(alo, ahi):
458                #      self.sci.markerAdd(i, self.markerminus)
459
460            elif tag == 'insert':
461                self._diffs.append([blo, bhi])
462                self.blk.addBlock('+', blo, bhi)
463                for i in range(blo, bhi):
464                    self.sci.markerAdd(i, self.markerplus)
465
466            elif tag == 'equal':
467                pass
468
469            else:
470                raise ValueError, 'unknown tag %r' % (tag,)
471
472        # ok, enable GUI refresh for code viewers and diff-block displayers
473        self.sci.setUpdatesEnabled(True)
474        self.blk.setUpdatesEnabled(True)
475
476
477class FileData(object):
478    def __init__(self, ctx, ctx2, wfile, status=None):
479        self.contents = None
480        self.error = None
481        self.olddata = None
482        self.diff = None
483        self.flabel = u''
484        self.elabel = u''
485        self.readStatus(ctx, ctx2, wfile, status)
486
487    def checkMaxDiff(self, ctx, wfile):
488        p = _('File or diffs not displayed: ')
489        try:
490            fctx = ctx.filectx(wfile)
491            if ctx.rev() is None:
492                size = fctx.size()
493            else:
494                # fctx.size() can read all data into memory in rename cases so
495                # we read the size directly from the filelog, this is deeper
496                # under the API than I prefer to go, but seems necessary
497                size = fctx._filelog.rawsize(fctx.filerev())
498        except (EnvironmentError, error.LookupError), e:
499            self.error = p + hglib.tounicode(str(e))
500            return None
501        if size > ctx._repo.maxdiff:
502            self.error = p + _('File is larger than the specified max size.\n')
503            return None
504        try:
505            data = fctx.data()
506            if '\0' in data:
507                self.error = p + _('File is binary.\n')
508                return None
509        except EnvironmentError, e:
510            self.error = p + hglib.tounicode(str(e))
511            return None
512        return fctx, data
513
514    def isValid(self):
515        return self.error is None
516
517    def readStatus(self, ctx, ctx2, wfile, status):
518        def getstatus(repo, n1, n2, wfile):
519            m = match.exact(repo.root, repo.getcwd(), [wfile])
520            modified, added, removed = repo.status(n1, n2, match=m)[:3]
521            if wfile in modified:
522                return 'M'
523            if wfile in added:
524                return 'A'
525            if wfile in removed:
526                return 'R'
527            return None
528
529        repo = ctx._repo
530        self.flabel += u'<b>%s</b>' % hglib.tounicode(wfile)
531
532        if isinstance(ctx, patchctx.patchctx):
533            self.diff = ctx.thgmqpatchdata(wfile)
534            flags = ctx.flags(wfile)
535            if flags in ('x', '-'):
536                lbl = _("exec mode has been <font color='red'>%s</font>")
537                change = (flags == 'x') and _('set') or _('unset')
538                self.elabel = lbl % change
539            elif flags == 'l':
540                self.flabel += _(' <i>(is a symlink)</i>')
541            return
542
543        absfile = repo.wjoin(wfile)
544        if (wfile in ctx and 'l' in ctx.flags(wfile)) or \
545           os.path.islink(absfile):
546            if wfile in ctx:
547                data = ctx[wfile].data()
548            else:
549                data = os.readlink(absfile)
550            self.contents = hglib.tounicode(data)
551            self.flabel += _(' <i>(is a symlink)</i>')
552            return
553
554        if status is None:
555            status = getstatus(repo, ctx.p1().node(), ctx.node(), wfile)
556        if ctx2 is None:
557            ctx2 = ctx.p1()
558
559        if status == 'S':
560            try:
561                from mercurial import subrepo, commands
562                assert(ctx.rev() is None)
563                out = []
564                _ui = uimod.ui()
565                sroot = repo.wjoin(wfile)
566                srepo = hg.repository(_ui, path=sroot)
567                srev = ctx.substate.get(wfile, subrepo.nullstate)[1]
568                sactual = srepo['.'].hex()
569                _ui.pushbuffer()
570                commands.status(_ui, srepo)
571                data = _ui.popbuffer()
572                if data:
573                    out.append(_('File Status:\n'))
574                    out.append(data)
575                    out.append('\n')
576                if srev == '':
577                    out.append(_('New subrepository\n\n'))
578                elif srev != sactual:
579                    out.append(_('Revision has changed from:\n\n'))
580                    opts = {'date':None, 'user':None, 'rev':[srev]}
581                    _ui.pushbuffer()
582                    commands.log(_ui, srepo, **opts)
583                    out.append(hglib.tounicode(_ui.popbuffer()))
584                    out.append(_('To:\n'))
585                    opts['rev'] = [sactual]
586                    _ui.pushbuffer()
587                    commands.log(_ui, srepo, **opts)
588                    out.append(hglib.tounicode(_ui.popbuffer()))
589                self.contents = u''.join(out)
590                self.flabel += _(' <i>(is a dirty sub-repository)</i>')
591                lbl = u' <a href="subrepo:%s">%s...</a>'
592                self.flabel += lbl % (hglib.tounicode(sroot), _('open'))
593            except (error.RepoError, util.Abort), e:
594                self.error = _('Not a Mercurial subrepo, not previewable')
595            return
596
597        # TODO: elif check if a subdirectory (for manifest tool)
598
599        if status in ('R', '!'):
600            if wfile in ctx.p1():
601                newdata = ctx.p1()[wfile].data()
602                self.contents = hglib.tounicode(newdata)
603                self.flabel += _(' <i>(was deleted)</i>')
604            else:
605                self.flabel += _(' <i>(was added, now missing)</i>')
606            return
607
608        if status in ('I', '?'):
609            try:
610                data = open(repo.wjoin(wfile), 'r').read()
611                if '\0' in data:
612                    self.error = 'binary file'
613                else:
614                    self.contents = hglib.tounicode(data)
615                    self.flabel += _(' <i>(is unversioned)</i>')
616            except EnvironmentError, e:
617                self.error = hglib.tounicode(str(e))
618            return
619
620        if status in ('M', 'A'):
621            res = self.checkMaxDiff(ctx, wfile)
622            if res is None:
623                return
624            fctx, newdata = res
625            self.contents = hglib.tounicode(newdata)
626            change = None
627            for pfctx in fctx.parents():
628                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            if change:
633                lbl = _("exec mode has been <font color='red'>%s</font>")
634                self.elabel = lbl % change
635
636        if status == 'A':
637            renamed = fctx.renamed()
638            if not renamed:
639                self.flabel += _(' <i>(was added)</i>')
640                return
641
642            oldname, node = renamed
643            fr = hglib.tounicode(oldname)
644            self.flabel += _(' <i>(renamed from %s)</i>') % fr
645            olddata = repo.filectx(oldname, fileid=node).data()
646        elif status == 'M':
647            if wfile not in ctx2:
648                # merge situation where file was added in other branch
649                self.flabel += _(' <i>(was added)</i>')
650                return
651            oldname = wfile
652            olddata = ctx2[wfile].data()
653        else:
654            return
655
656        self.olddata = olddata
657        newdate = util.datestr(ctx.date())
658        olddate = util.datestr(ctx2.date())
659        revs = [str(ctx), str(ctx2)]
660        diffopts = patch.diffopts(repo.ui, {})
661        diffopts.git = False
662        self.diff = mdiff.unidiff(olddata, olddate, newdata, newdate,
663                                  oldname, wfile, revs, diffopts)