Staging
v0.5.0
https://foss.heptapod.net/mercurial/hgview
Raw File
Tip revision: 1850a087c1b6cb026470c738b2c7f82c6fada06d authored by Mads Kiilerich on 04 April 2020, 02:27:44 UTC
qt5: renaming of references to hgviewlib/qt5, finishing up the Qt5 port
Tip revision: 1850a08
hgrepomodel.py
# Copyright (c) 2009-2012 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, see <http://www.gnu.org/licenses/>.
"""
Qt5 model for hg repo changelogs and filelogs
"""

import re
import os, os.path as osp

from mercurial.node import short as short_hex
from mercurial.error import LookupError

from hgviewlib.hggraph import Graph, ismerge, diff as revdiff, HgRepoListWalker
from hgviewlib.hggraph import filelog_grapher, getlog, gettags
from hgviewlib.config import HgConfig
from hgviewlib.util import tounicode, tostr, tohg, isbfile, xml_escape, allbranches
from hgviewlib.qt5 import icon as geticon
from hgviewlib.hgpatches import phases

from PyQt5.QtGui import QColor, QPixmap, QPainter, QPen, QFont
from PyQt5.QtCore import Qt, pyqtSignal, QAbstractItemModel, QAbstractTableModel, \
     QDateTime, QTimer, QModelIndex

# XXX make this better than a poor hard written list...
COLORS = [ "blue", "darkgreen", "red", "green", "darkblue", "purple",
           "cyan", Qt.darkYellow, "magenta", "darkred", "darkmagenta",
           "darkcyan", "gray", ]
COLORS = [str(QColor(x).name()) for x in COLORS]
#COLORS = [str(color) for color in QColor.colorNames()]

# We use two colors, One for even rows and one for odd rows
COLOR_BG_OBSOLETE = [QColor(255, 250, 250), QColor(243, 230, 230)]
COLOR_BG_TROUBLED = [QColor(255, 193,  71), QColor(255, 153,  51)]
COLOR_BG_HIGHLIGHT = [QColor(127, 199, 175),
                      QColor(127, 199, 175).lighter()]

BOOKMARK_CSS = "color: white; background-color: blue;"
TAG_CSS = "color: black; background-color: SandyBrown;"

def cvrt_date(date):
    """
    Convert a date given the hg way, ie. couple (date, tz), into a
    formatted string
    """
    if not date:
        return u''
    date, tzdelay = date
    return QDateTime.fromTime_t(int(date)).toString(Qt.LocaleDate)


# XXX maybe it's time to make these methods of the model...
# in following lambdas, ctx is a hg changectx
_columnmap = {'ID': lambda model, ctx, gnode: ctx.rev() is not None and str(ctx.rev()) or "",
              'Log': getlog,
              'Author': lambda model, ctx, gnode: tounicode(ctx.user()),
              'Date': lambda model, ctx, gnode: cvrt_date(ctx.date()),
              'Branch': lambda model, ctx, gnode: tounicode(ctx.branch()),
              'Filename': lambda model, ctx, gnode: tounicode(gnode.extra[0]),
              'Phase': lambda model, ctx, gnode: tounicode(ctx.phasestr()),
              }

_tooltips = {'ID': lambda model, ctx, gnode: ctx.rev() is not None and tounicode(ctx.hex()) or u"Working Directory",
             }

def auth_width(model, repo):
    auths = model._aliases.values()
    if not auths:
        return None
    return sorted(auths, cmp=lambda x,y: cmp(len(x), len(y)))[-1]

# in following lambdas, r is a hg repo
# it return the longuest entry of this column
_maxwidth = {'ID': lambda self, r: str(len(r.changelog)),
             'Date': lambda self, r: cvrt_date(r[0].date()),
             'Branch': lambda self, r: ([None] + sorted(allbranches(r), key=len))[-1],
             'Author': lambda self, r: 'author name',
             'Filename': lambda self, r: self.filename,
             'Phase': lambda self, r: sorted(phases.phasenames, key=len)[-1]
             }

def datacached(meth):
    """
    decorator used to cache 'data' method of Qt models. It will *not*
    cache None return values (so costly non-null values
    can be computed and filled as a background process)
    """
    def data(self, index, role):
        if not index.isValid():
            return None
        row = index.row()
        col = index.column()
        if (row, col, role) in self._datacache:
            return self._datacache[(row, col, role)]
        result = meth(self, index, role)
        if result is not None:
            self._datacache[(row, col, role)] = result
        return result
    return data



class HgRepoListModel(QAbstractTableModel, HgRepoListWalker):
    """
    Model used for displaying the revisions of a Hg *local* repository
    """

    message_logged = pyqtSignal(str, int)
    filled = pyqtSignal()

    _allcolumns = ('ID', 'Branch', 'Log', 'Author', 'Date')
    _columns = ('ID', 'Branch', 'Log', 'Author', 'Date')
    _stretchs = {'Log': 1, }
    _getcolumns = "getChangelogColumns"

    def __init__(self, repo, branch='', fromhead=None, follow=False, parent=None, show_hidden=False, closed=False):
        """
        repo is a hg repo instance
        """
        self._fill_timer = None
        QAbstractTableModel.__init__(self, parent)
        HgRepoListWalker.__init__(self)
        self.setRepo(repo, branch=branch, fromhead=fromhead, follow=follow, closed=closed)
        self.highlights = []

    def setRepo(self, repo, branch='', fromhead=None, follow=False, closed=False):
        HgRepoListWalker.setRepo(self, repo, branch, fromhead, follow, closed=closed)
        self.layoutChanged.emit()
        QTimer.singleShot(0, lambda: self.filled.emit())
        self._fill_timer = self.startTimer(50)

    def highlight_rows(self, rows):
        """mark ``rows`` to be highlighted."""
        self.highlights[:] = rows
        self._datacache.clear()

    def timerEvent(self, event):
        if event.timerId() == self._fill_timer:
            self.message_logged.emit(
                      'filling (%s)' % (len(self.graph)),
                      -1)
            if self.graph.isfilled():
                self.killTimer(self._fill_timer)
                self._fill_timer = None
                self.updateRowCount()
                self.message_logged.emit('', -1)
            # we fill the graph data structures without telling
            # views until we are done - this gives
            # maximal GUI responsiveness
            elif not self.graph.build_nodes(nnodes=self.fill_step):
                self.killTimer(self._fill_timer)
                self._fill_timer = None
                self.updateRowCount()
                self.message_logged.emit('', -1)

    def updateRowCount(self):
        currentlen = self.rowcount
        newlen = len(self.graph)
        if newlen > self.rowcount:
            self.beginInsertRows(QModelIndex(), currentlen, newlen-1)
            self.rowcount = newlen
            self.endInsertRows()

    @staticmethod
    def get_color(n, ignore=()):
        """
        Return a color at index 'n' rotating in the available
        colors. 'ignore' is a list of colors not to be chosen.
        """
        ignore = [str(QColor(x).name()) for x in ignore]
        colors = [x for x in COLORS if x not in ignore]
        if not colors: # ghh, no more available colors...
            colors = COLORS
        return colors[n % len(colors)]

    def user_color(self, user):
        if user in self._aliases:
            user = self._aliases[user]
        if user in self._users:
            try:
                color = self._users[user]['color']
                color = QColor(color).name()
                self._user_colors[user] = color
            except:
                pass
        return HgRepoListWalker.user_color(self, user)

    def _display_log(self, ctx, gnode, row):
        """Display the log column content."""
        content = []
        # display bookmarks
        bookmarks = [tounicode(bookmark) for bookmark in ctx.bookmarks()]
        if bookmarks:
            content.extend(u'<span style="white-space: pre; %s"> %s </span>'
                           % (BOOKMARK_CSS, xml_escape(bkm)) for bkm in bookmarks)
        # display tags
        tags = [tag for tag in gettags(self, ctx).split(',') if tag]
        if tags:
            content.extend(u'<span style="white-space: pre; %s"> %s </span>'
                           % (TAG_CSS, xml_escape(tag)) for tag in tags)
        # display log
        style =  "color: grey;" if ctx.obsolete() else u""
        content.append(u'<span style="white-space: pre; %s"> %s </span>'
                       % (style, xml_escape(_columnmap['Log'](self, ctx, gnode))))
        return u' '.join(content)

    @datacached
    def data(self, index, role):
        if not index.isValid():
            return None
        row = index.row()
        self.ensureBuilt(row=row)
        column = self._columns[index.column()]
        gnode = self.graph[row]
        ctx = self.repo[gnode.rev]
        if role == Qt.DisplayRole:
            if column == 'Author': #author
                user = _columnmap[column](self, ctx, gnode)
                return self.user_name(user)
            elif column == 'Log':
                return self._display_log(ctx, gnode, row)
            return _columnmap[column](self, ctx, gnode)
        elif role == Qt.ToolTipRole:
            msg = ""
            if column == 'ID':
                msg += u"<b>Phase:</b> %s<br>\n" % _columnmap['Phase'](self, ctx, gnode)
            if gnode.rev in self.wd_revs:
                msg += u" <i>Working Directory position"
                states = u'modified added removed deleted'.split()
                status = self.wd_status[self.wd_revs.index(gnode.rev)]
                status = [state for st, state in zip(status, states) if st]
                if status:
                    msg += ' (%s)' % (', '.join(status))
                msg += u"</i><br>\n"
            msg += _tooltips.get(column, _columnmap[column])(self, ctx, gnode)
            return msg
        elif role == Qt.ForegroundRole:
            color = None
            if column == 'Author': #author
                user = tounicode(ctx.user()) if ctx.node() else u''
                color = QColor(self.user_color(user))
                if ctx.obsolete():
                    color = color.lighter()
            elif column == 'Branch': #branch
                color = QColor(self.namedbranch_color(tounicode(ctx.branch())))
                if ctx.obsolete():
                    color = color.lighter()
            elif ctx.obsolete():
                color = QColor('grey')
            if color is not None:
                return color

        elif role == Qt.BackgroundRole:
            row = index.row()
            if row in self.highlights:
                return COLOR_BG_HIGHLIGHT[row % 2]
            elif ctx.obsolete():
                return COLOR_BG_OBSOLETE[row % 2]
            elif ctx.instabilities():
                return COLOR_BG_TROUBLED[row % 2]

    def headerData(self, section, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._columns[section]
        return None

    def maxWidthValueForColumn(self, column):
        column = self._columns[column]
        if column in _maxwidth:
            return _maxwidth[column](self, self.repo)
        return None

    def clear(self):
        """empty the list"""
        self.graph = None
        self._datacache = {}
        self.notify_data_changed()

    def notify_data_changed(self):
        self.layoutChanged.emit()

    def indexFromRev(self, rev):
        self.ensureBuilt(rev=rev)
        row = self.rowFromRev(rev)
        if row is not None:
            return self.index(row, 0)

class FileRevModel(HgRepoListModel):
    """
    Model used to manage the list of revisions of a file, in file
    viewer of in diff-file viewer dialogs.
    """
    _allcolumns = ('ID', 'Branch', 'Log', 'Author', 'Date', 'Filename')
    _columns = ('ID', 'Branch', 'Log', 'Author', 'Date', 'Filename')
    _stretchs = {'Log': 1, }
    _getcolumns = "getFilelogColumns"

    def __init__(self, repo, filename=None, parent=None):
        """
        data is a HgHLRepo instance
        """
        HgRepoListModel.__init__(self, repo, parent=parent)
        self.setFilename(filename)

    def setRepo(self, repo, branch='', fromhead=None, follow=False, closed=False):
        self.repo = repo
        self._datacache = {}
        self.load_config()

    def setFilename(self, filename):
        self.filename = filename

        self._user_colors = {}
        self._branch_colors = {}

        self.rowcount = 0
        self._datacache = {}

        if self.filename:
            grapher = filelog_grapher(self.repo, self.filename)
            self.graph = Graph(self.repo, grapher, self.max_file_size)
            fl = self.repo.file(tohg(self.filename))
            self.heads = [fl.linkrev(fl.rev(x)) for x in fl.heads()]
            self.ensureBuilt(row=self.fill_step//2)
            QTimer.singleShot(0, lambda: self.filled.emit())
            self._fill_timer = self.startTimer(500)
        else:
            self.graph = None
            self.heads = []


replus = re.compile(r'^[+][^+].*', re.M)
reminus = re.compile(r'^[-][^-].*', re.M)

class HgFileListModel(QAbstractTableModel):
    """
    Model used for listing (modified) files of a given Hg revision
    """

    _description_desc = dict(path='', flag='', desc='Display revision description',
                             bfile=None, parent=None, fromside=None, infiles=False)

    def __init__(self, repo, parent=None):
        """
        data is a HgHLRepo instance
        """
        QAbstractTableModel.__init__(self, parent)
        self.repo = repo
        self._datacache = {}
        self.load_config()
        self.current_ctx = None
        self._files = []
        self._filesdict = {}
        self.diffwidth = 100
        self._fulllist = False
        self._fill_iter = None

    def toggleFullFileList(self):
        self._fulllist = not self._fulllist
        self.loadFiles()
        self.layoutChanged.emit()

    def load_config(self):
        cfg = HgConfig(self.repo.ui)
        self._flagcolor = {}
        self._flagcolor['='] = cfg.getFileModifiedColor()
        self._flagcolor['-'] = cfg.getFileRemovedColor()
        self._flagcolor['-'] = cfg.getFileDeletedColor()
        self._flagcolor['+'] = cfg.getFileAddedColor()
        self._flagcolor[''] = cfg.getFileDescriptionColor()
        self._displaydiff = cfg.getDisplayDiffStats()
        self._descriptionview = cfg.getFileDescriptionView()

    def setDiffWidth(self, w):
        if w != self.diffwidth:
            self.diffwidth = w
            self._datacache = {}
            self.dataChanged.emit(
                      self.index(1, 0),
                      self.index(1, self.rowCount()))

    def __len__(self):
        return len(self._files)

    def __contains__(self, filename):
        return filename in self._filesdict

    def rowCount(self, parent=None):
        return len(self)

    def columnCount(self, parent=None):
        return 1 + self._displaydiff

    def file(self, row):
        return self._files[row]['path']

    def fileflag(self, fn):
        if not fn:
            return '+'
        return self._filesdict[fn]['flag']

    def fileparentctx(self, fn, ctx=None):
        if ctx is None:
            return self._filesdict[fn]['parent']
        return ctx.parents()[0]

    def fileFromIndex(self, index):
        if not index.isValid() or index.row()>=len(self) or not self.current_ctx:
            return None
        row = index.row()
        file_info = self._files[row]
        return self._files[row]['path']

    def revFromIndex(self, index):
        if self._fulllist and ismerge(self.current_ctx):
            if not index.isValid() or index.row()>=len(self) or not self.current_ctx:
                return None
            row = index.row()
            if self._files[row]['fromside'] == 'right':
                return self.current_ctx.parents()[1].rev()
            return self.current_ctx.parents()[0].rev()
        return None

    def indexFromFile(self, filename):
        if filename in self._filesdict:
            row = self._files.index(self._filesdict[filename])
            return self.index(row, 0)
        return QModelIndex()

    def _filterFile(self, filename, ctxfiles):
        if self._fulllist:
            return True
        return filename in ctxfiles #self.current_ctx.files()

    def _buildDesc(self, parent, fromside):
        _files = []
        ctx = self.current_ctx
        ctxfiles = ctx.files()
        changes = list(self.repo.status(parent.node(), ctx.node()))[:3]
        modified, added, removed = changes
        for lst, flag in ((added, '+'), (modified, '='), (removed, '-')):
            for f in [x for x in lst if self._filterFile(x, ctxfiles)]:
                path = tounicode(f)
                desc = path
                bfile = isbfile(path)
                if bfile:
                    desc = desc.replace('.hgbfiles'+os.sep, '')
                _files.append({'path': path, 'flag': flag, 'desc': desc, 'bfile': bfile,
                               'parent': parent, 'fromside': fromside,
                               'infiles': f in ctxfiles})
                # renamed/copied files are handled by background
                # filling process since it can be a bit long
        return _files

    def loadFiles(self):
        self._fill_iter = None
        self._files = []
        self._datacache = {}
        self._files = self._buildDesc(self.current_ctx.parents()[0], 'left')
        if ismerge(self.current_ctx):
            _paths = [x['path'] for x in self._files]
            _files = self._buildDesc(self.current_ctx.parents()[1], 'right')
            self._files += [x for x in _files if x['path'] not in _paths]
        self._filesdict = dict([(f['path'], f) for f in self._files])
        if self._descriptionview == 'asfile':
            self._files.insert(0, self._description_desc)
        self.fillFileStats()

    def setSelectedRev(self, ctx):
        if ctx != self.current_ctx:
            self.current_ctx = ctx
            self._datacache = {}
            self.loadFiles()
            self.layoutChanged.emit()

    def fillFileStats(self):
        """
        Method called to start the background process of computing
        file stats, which are to be displayed in the 'Stats' column
        """
        self._fill_iter = self._fill()
        self._fill_one_step()

    def _fill_one_step(self):
        if self._fill_iter is None:
            return
        try:
            nextfill = next(self._fill_iter)
            if nextfill is not None:
                row, col = nextfill
                idx = self.index(row, col)
                self.dataChanged.emit(idx, idx)
            QTimer.singleShot(10, lambda: self._fill_one_step())

        except StopIteration:
            self._fill_iter = None

    def _fill(self):
        # the generator used to fill file stats as a background process
        files = enumerate(self._files)
        if self._descriptionview == 'asfile':
            next(files) # consume description entry
        for row, desc in files:
            filename = desc['path']
            if desc['flag'] == '=' and self._displaydiff:
                try:
                    diff = revdiff(self.repo, self.current_ctx, None, files=[filename])
                    tot = self.current_ctx.filectx(tohg(filename)).data().count(b'\n')
                    add = len(replus.findall(diff))
                    rem = len(reminus.findall(diff))
                except (LookupError, TypeError): # unknown revision and mq support
                    tot, add, rem = 0, 0, 0

                if tot == 0:
                    tot = max(add + rem, 1)
                desc['stats'] = (tot, add, rem)
                yield row, 1

            if desc['flag'] == '+':
                m = self.current_ctx.filectx(tohg(filename)).renamed()
                if m:
                    removed = list(
                        self.repo.status(desc['parent'].node(),
                                         self.current_ctx.node()))[2]
                    oldname, node = m
                    if oldname in removed:
                        # removed.remove(oldname) XXX
                        desc['renamedfrom'] = (tounicode(oldname), node)
                        desc['flag'] = '='
                        desc['desc'] += u'\n (was %s)' % tounicode(oldname)
                    else:
                        desc['copiedfrom'] = (tounicode(oldname), node)
                        desc['flag'] = '='
                        desc['desc'] += u'\n (copy of %s)' % tounicode(oldname)
                    yield row, 0
            yield None

    def data(self, index, role):
        if not index.isValid() or index.row()>len(self) or not self.current_ctx:
            return None
        row = index.row()
        column = index.column()
        current_file_desc = self._files[row]
        current_file = current_file_desc['path']
        stats = current_file_desc.get('stats')
        if column == 1:
            if stats is not None:
                if role == Qt.DecorationRole:
                    tot, add, rem = stats
                    w = self.diffwidth - 20
                    h = 20

                    np = int(w*add/tot)
                    nm = int(w*rem/tot)
                    nd = w-np-nm

                    pix = QPixmap(w+10, h)
                    pix.fill(QColor(0,0,0,0))
                    painter = QPainter(pix)

                    for x0,w0, color in ((0, nm, 'red'),
                                         (nm, np, 'green'),
                                         (nm+np, nd, 'gray')):
                        color = QColor(color)
                        painter.setBrush(color)
                        painter.setPen(color)
                        painter.drawRect(x0+5, 0, w0, h-3)
                    painter.setBrush(QColor(0,0,0,0))
                    pen = QPen(Qt.black)
                    pen.setWidth(0)
                    painter.setPen(pen)
                    painter.drawRect(5, 0, w+1, h-3)
                    painter.end()
                    return pix
                elif role == Qt.ToolTipRole:
                    tot, add, rem = stats
                    msg = "Diff stats:<br>"
                    msg += "&nbsp;<b>File:&nbsp;</b>%s lines<br>" % tot
                    msg += "&nbsp;<b>added lines:&nbsp;</b> %s<br>" % add
                    msg += "&nbsp;<b>removed lines:&nbsp;</b> %s" % rem
                    return msg

        elif column == 0:
            if role in (Qt.DisplayRole, Qt.ToolTipRole):
                return current_file_desc['desc']
            elif role == Qt.DecorationRole:
                if self._fulllist and ismerge(self.current_ctx):
                    icn = None
                    if current_file_desc['infiles']:
                        icn = geticon('leftright')
                    elif current_file_desc['fromside'] == 'left':
                        icn = geticon('left')
                    elif current_file_desc['fromside'] == 'right':
                        icn = geticon('right')
                    if icn:
                        return icn.pixmap(20,20)
            elif role == Qt.FontRole:
                if self._fulllist and current_file_desc['infiles']:
                    font = QFont()
                    font.setBold(True)
                    return font
            elif role == Qt.ForegroundRole:
                color = self._flagcolor.get(current_file_desc['flag'], 'black')
                if color is not None:
                    return QColor(color)
        return None

    def headerData(self, section, orientation, role):
        if ismerge(self.current_ctx):
            if self._fulllist:
                header = ('File (all)', 'Diff')
            else:
                header = ('File (merged only)', 'Diff')
        else:
            header = ('File', 'Diff')

        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return header[section]
        return None


class TreeItem(object):
    def __init__(self, data, parent=None):
        self.parentItem = parent
        self.itemData = data
        self.childItems = []

    def appendChild(self, item):
        self.childItems.append(item)
        return item
    addChild = appendChild

    def child(self, row):
        return self.childItems[row]

    def childCount(self):
        return len(self.childItems)

    def columnCount(self):
        return len(self.itemData)

    def data(self, column):
        return self.itemData[column]

    def parent(self):
        return self.parentItem

    def row(self):
        if self.parentItem:
            return self.parentItem.childItems.index(self)
        return 0

    def __getitem__(self, idx):
        return self.childItems[idx]

    def __len__(self):
        return len(self.childItems)

    def __iter__(self):
        for ch in self.childItems:
            yield ch


class ManifestModel(QAbstractItemModel):
    """
    Qt model to display a hg manifest, ie. the tree of files at a
    given revision. To be used with a QTreeView.
    """
    def __init__(self, repo, rev, parent=None):
        QAbstractItemModel.__init__(self, parent)

        self.repo = repo
        self.changectx = self.repo[rev]
        self.setupModelData()

    def data(self, index, role):
        if not index.isValid():
            return None

        if role != Qt.DisplayRole:
            return None

        item = index.internalPointer()
        return tounicode(item.data(index.column()))

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemIsEnabled
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def headerData(self, section, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.rootItem.data(section)
        return None

    def index(self, row, column, parent):
        if row < 0 or column < 0 or row >= self.rowCount(parent) or column >= self.columnCount(parent):
            return QModelIndex()

        if not parent.isValid():
            parentItem = self.rootItem
        else:
            parentItem = parent.internalPointer()
        childItem = parentItem.child(row)
        if childItem is not None:
            return self.createIndex(row, column, childItem)
        else:
            return QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()

        childItem = index.internalPointer()
        parentItem = childItem.parent()

        if parentItem == self.rootItem:
            return QModelIndex()

        return self.createIndex(parentItem.row(), 0, parentItem)

    def rowCount(self, parent):
        if parent.column() > 0:
            return 0

        if not parent.isValid():
            parentItem = self.rootItem
        else:
            parentItem = parent.internalPointer()
        return parentItem.childCount()

    def columnCount(self, parent):
        if parent.isValid():
            return parent.internalPointer().columnCount()
        else:
            return self.rootItem.columnCount()

    def setupModelData(self):
        if self.changectx.rev() is not None:
            rootData = ["rev %s:%s" % (self.changectx.rev(),
                                       tostr(short_hex(self.changectx.node())))]
        else:
            rootData = ['Working Directory']
        self.rootItem = TreeItem(rootData)

        for path in sorted(self.changectx.manifest()):
            path = tounicode(path).split(osp.sep)
            node = self.rootItem

            for p in path:
                for ch in node:
                    if ch.data(0) == p:
                        node = ch
                        break
                else:
                    node = node.addChild(TreeItem([p], node))

    def pathFromIndex(self, index):
        idxs = []
        while index.isValid():
            idxs.insert(0, index)
            index = self.parent(index)
        return osp.sep.join([index.internalPointer().data(0) for index in idxs])

back to top