This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 11:00 UTC+2 (a few minutes of down time)

manifestmodel.py 21.6 KB
Newer Older
1 2 3 4
# manifestmodel.py - Model for TortoiseHg manifest view
#
# Copyright (C) 2009-2010 LOGILAB S.A. <http://www.logilab.fr/>
# Copyright (C) 2010 Yuya Nishihara <yuya@tcha.org>
Adrian Buehlmann's avatar
Adrian Buehlmann committed
5 6 7 8 9 10
#
# 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.

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
from __future__ import absolute_import

import os
import re

from .qtcore import (
    QAbstractItemModel,
    QMimeData,
    QModelIndex,
    QUrl,
    Qt,
    pyqtSignal,
    pyqtSlot,
)
from .qtgui import (
    QCompleter,
    QFileIconProvider,
    QIcon,
)

31 32 33 34
from hgext.largefiles import (
    lfutil,
)

35 36 37
from mercurial import (
    error,
    match as matchmod,
38
    pycompat,
39
    scmutil,
40 41 42 43 44 45 46 47 48
)

from ..util import hglib
from . import (
    filedata,
    qtlib,
    status,
    visdiff,
)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
49

50 51
_subrepoType2IcoMap = {
    'hg': 'hg',
52
    'hgsubversion': 'thg-svn-subrepo',
53 54 55 56
    'git': 'thg-git-subrepo',
    'svn': 'thg-svn-subrepo',
    }

57 58 59 60 61
_subrepoStatus2IcoMap = {
    'A': 'thg-added-subrepo',
    'R': 'thg-removed-subrepo',
    }

62
class ManifestModel(QAbstractItemModel):
63
    """Status of files between two revisions or patch"""
64

65 66 67
    # emitted when all files of the revision has been loaded successfully
    revLoaded = pyqtSignal(object)

68 69 70
    StatusRole = Qt.UserRole + 1
    """Role for file change status"""

71 72 73 74
    # -1 and None are valid revision number
    FirstParent = -2
    SecondParent = -3

75 76
    def __init__(self, repoagent, parent=None, rev=None, namefilter=None,
                 statusfilter='MASC', flat=False):
77
        QAbstractItemModel.__init__(self, parent)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
78

79
        self._fileiconprovider = QFileIconProvider()
80
        self._iconcache = {}  # (path, status, subkind): icon
81
        self._repoagent = repoagent
82

83
        self._namefilter = pycompat.unicode(namefilter or '')
84
        assert all(c in 'MARSC' for c in statusfilter), repr(statusfilter)
85
        self._statusfilter = statusfilter
86
        self._changedfilesonly = False
87
        self._nodeop = _nodeopmap[bool(flat)]
Adrian Buehlmann's avatar
Adrian Buehlmann committed
88

89
        self._rootentry = self._newRevNode(rev)
90
        self._populate = _populaterepo
91
        self._rootpopulated = False
92

93
    def data(self, index, role=Qt.DisplayRole):
Adrian Buehlmann's avatar
Adrian Buehlmann committed
94
        if not index.isValid():
95
            return
Adrian Buehlmann's avatar
Adrian Buehlmann committed
96

97
        if role == Qt.DecorationRole:
98
            return self.fileIcon(index)
99 100
        if role == self.StatusRole:
            return self.fileStatus(index)
101 102

        e = index.internalPointer()
103
        if role in (Qt.DisplayRole, Qt.EditRole):
104
            return e.name
105

106
    def filePath(self, index):
107
        """Return path at the given index [unicode]"""
108 109 110 111 112
        if not index.isValid():
            return ''

        return index.internalPointer().path

113 114 115
    def fileData(self, index):
        """Returns the displayable file data at the given index"""
        repo = self._repoagent.rawRepo()
116
        if not index.isValid():
117
            return filedata.createNullData(repo)
118 119 120

        f = index.internalPointer()
        e = f.parent
121
        while e and e.ctx is None:
122
            e = e.parent
123
        assert e, 'root entry must have ctx'
124
        wfile = hglib.fromunicode(f.path[len(e.path):].lstrip('/'))
125
        rpath = hglib.fromunicode(e.path)
126 127
        if f.subkind:
            # TODO: use subrepo ctxs and status resolved by this model
128
            return filedata.createSubrepoData(e.ctx, e.pctx, wfile, f.status,
129
                                              rpath, f.subkind)
130
        if f.isdir:
131 132
            return filedata.createDirData(e.ctx, e.pctx, wfile, rpath)
        return filedata.createFileData(e.ctx, e.pctx, wfile, f.status, rpath)
133

134 135
    def subrepoType(self, index):
        """Return the subrepo type the specified index"""
136 137 138 139
        if not index.isValid():
            return
        e = index.internalPointer()
        return e.subkind
140

141 142
    def fileIcon(self, index):
        if not index.isValid():
143
            return QIcon()
144
        e = index.internalPointer()
145 146 147 148 149 150
        k = (e.path, e.status, e.subkind)
        try:
            return self._iconcache[k]
        except KeyError:
            self._iconcache[k] = ic = self._makeFileIcon(e)
            return ic
151

152
    def _makeFileIcon(self, e):
153
        if e.subkind in _subrepoType2IcoMap:
154
            ic = qtlib.geticon(_subrepoType2IcoMap[e.subkind])
155 156 157 158
            # use fine-tuned status overlay if any
            n = _subrepoStatus2IcoMap.get(e.status)
            if n:
                return qtlib.getoverlaidicon(ic, qtlib.geticon(n))
159
            ic = qtlib.getoverlaidicon(ic, qtlib.geticon('thg-subrepo'))
160
        elif e.isdir:
161
            ic = self._fileiconprovider.icon(QFileIconProvider.Folder)
162
        else:
163 164
            # do not use fileiconprovier.icon(fileinfo), which may return icon
            # with shell (i.e. status of working directory) overlay.
165 166
            # default file icon looks ugly with status overlay on Windows
            ic = qtlib.geticon('text-x-generic')
167 168 169 170 171

        if not e.status:
            return ic
        st = status.statusTypes[e.status]
        if st.icon:
172
            icOverlay = qtlib.geticon(st.icon)
173 174
            ic = qtlib.getoverlaidicon(ic, icOverlay)

175 176
        return ic

177 178 179 180 181
    def fileStatus(self, index):
        """Return the change status of the specified file"""
        if not index.isValid():
            return
        e = index.internalPointer()
182 183 184
        # TODO: 'S' should not be a status
        if e.subkind:
            return 'S'
185 186
        return e.status

187 188 189 190 191 192 193 194 195 196
    # TODO: this should be merged to fileStatus()
    def subrepoStatus(self, index):
        """Return the change status of the specified subrepo"""
        if not index.isValid():
            return
        e = index.internalPointer()
        if not e.subkind:
            return
        return e.status

197 198 199 200
    def isDir(self, index):
        if not index.isValid():
            return True  # root entry must be a directory
        e = index.internalPointer()
201
        return e.isdir
202

203
    def mimeData(self, indexes):
204
        files = [self.filePath(i) for i in indexes if i.isValid()]
205 206
        ctx = self._rootentry.ctx
        if ctx.rev() is not None:
207
            repo = self._repoagent.rawRepo()
208
            lfiles = pycompat.maplist(hglib.fromunicode, files)
209
            lbase, _fns = visdiff.snapshot(repo, lfiles, ctx)
210
            base = hglib.tounicode(lbase)
211 212
        else:
            # working copy
213
            base = self._repoagent.rootPath()
214 215

        m = QMimeData()
216
        m.setUrls([QUrl.fromLocalFile(os.path.join(base, e)) for e in files])
217 218 219 220 221
        return m

    def mimeTypes(self):
        return ['text/uri-list']

Adrian Buehlmann's avatar
Adrian Buehlmann committed
222
    def flags(self, index):
223
        f = super(ManifestModel, self).flags(index)
Adrian Buehlmann's avatar
Adrian Buehlmann committed
224
        if not index.isValid():
225
            return f
226 227
        if not (self.isDir(index) or self.fileStatus(index) == 'R'
                or self._populate is _populatepatch):
228 229
            f |= Qt.ItemIsDragEnabled
        return f
Adrian Buehlmann's avatar
Adrian Buehlmann committed
230

231
    def index(self, row, column, parent=QModelIndex()):
232
        if row < 0 or self.rowCount(parent) <= row or column != 0:
233
            return QModelIndex()
234
        return self.createIndex(row, column, self._parententry(parent).at(row))
Adrian Buehlmann's avatar
Adrian Buehlmann committed
235

236
    def indexFromPath(self, path, column=0):
237 238 239 240
        """Return index for the specified path if found [unicode]

        If not found, returns invalid index.
        """
241 242 243 244
        if not path:
            return QModelIndex()

        try:
245
            e = self._nodeop.findpath(self._rootentry, pycompat.unicode(path))
246 247 248 249 250
        except KeyError:
            return QModelIndex()

        return self.createIndex(e.parent.index(e.name), column, e)

Adrian Buehlmann's avatar
Adrian Buehlmann committed
251 252
    def parent(self, index):
        if not index.isValid():
253
            return QModelIndex()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
254

255 256
        e = index.internalPointer()
        if e.path:
257
            return self.indexFromPath(e.parent.path, index.column())
258
        else:
259
            return QModelIndex()
Adrian Buehlmann's avatar
Adrian Buehlmann committed
260

261 262 263 264 265
    def _parententry(self, parent):
        if parent.isValid():
            return parent.internalPointer()
        else:
            return self._rootentry
Adrian Buehlmann's avatar
Adrian Buehlmann committed
266

267
    def rowCount(self, parent=QModelIndex()):
268
        return len(self._parententry(parent))
Adrian Buehlmann's avatar
Adrian Buehlmann committed
269

270
    def columnCount(self, parent=QModelIndex()):
271
        return 1
Adrian Buehlmann's avatar
Adrian Buehlmann committed
272

273 274 275
    def rev(self, parent=QModelIndex()):
        """Revision number of the current changectx"""
        e = self._parententry(parent)
276
        if e.ctx is None or not _isreporev(e.ctx.rev()):
277 278 279
            return -1
        return e.ctx.rev()

280 281 282 283 284 285 286 287
    def baseRev(self, parent=QModelIndex()):
        """Revision of the base changectx where status is calculated from"""
        e = self._parententry(parent)
        if e.pctx is None or not _isreporev(e.pctx.rev()):
            return -1
        return e.pctx.rev()

    def setRev(self, rev, prev=FirstParent):
288 289
        """Change to the specified repository revision; None for working-dir"""
        roote = self._rootentry
290
        newroote = self._newRevNode(rev, prev)
291 292
        if (_samectx(newroote.ctx, roote.ctx)
            and _samectx(newroote.pctx, roote.pctx)):
293
            return
294 295
        self._populate = _populaterepo
        self._repopulateNodes(newroote=newroote)
296 297
        if self._rootpopulated:
            self.revLoaded.emit(self.rev())
298 299 300

    def setRawContext(self, ctx):
        """Change to the specified changectx in place of repository revision"""
301
        if _samectx(self._rootentry.ctx, ctx):
302 303 304 305 306 307 308 309 310 311 312
            return
        if _isreporev(ctx.rev()):
            repo = self._repoagent.rawRepo()
            try:
                if ctx == repo[ctx.rev()]:
                    return self.setRev(ctx.rev())
            except error.RepoLookupError:
                pass
        newroote = _Entry()
        newroote.ctx = ctx
        self._populate = _populatepatch
313
        self._repopulateNodes(newroote=newroote)
314 315
        if self._rootpopulated:
            self.revLoaded.emit(self.rev())
316

317 318 319 320
    def nameFilter(self):
        """Return the current name filter"""
        return self._namefilter

321
    @pyqtSlot(str)
322 323
    def setNameFilter(self, pattern):
        """Filter file name by partial match of glob pattern"""
324
        pattern = pycompat.unicode(pattern)
325 326
        if self._namefilter == pattern:
            return
327
        self._namefilter = pattern
328
        self._repopulateNodes()
329

330 331 332
    def statusFilter(self):
        """Return the current status filter"""
        return self._statusfilter
333

334
    # TODO: split or remove 'S' which causes several design flaws
335 336
    @pyqtSlot(str)
    def setStatusFilter(self, status):
Angel Ezquerra's avatar
Angel Ezquerra committed
337
        """Filter file tree by change status 'MARSC'"""
338
        status = str(status)
339
        assert all(c in 'MARSC' for c in status), repr(status)
340 341 342
        if self._statusfilter == status:
            return  # for performance reason
        self._statusfilter = status
343
        self._repopulateNodes()
344

345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
    def isChangedFilesOnly(self):
        """Whether or not to filter by ctx.files, i.e. to exclude files not
        changed in the current revision.

        If this filter is enabled, 'C' (clean) files are not listed.  For
        merge changeset, 'M' (modified) files in one side are also excluded.
        """
        return self._changedfilesonly

    def setChangedFilesOnly(self, changedonly):
        if self._changedfilesonly == bool(changedonly):
            return
        self._changedfilesonly = bool(changedonly)
        self._repopulateNodes()

360 361
    def isFlat(self):
        """Whether all entries are listed in the same level or per directory"""
362
        return self._nodeop is _listnodeop
363 364

    def setFlat(self, flat):
365
        if self.isFlat() == bool(flat):
366
            return
367 368
        # self._nodeop must be changed after layoutAboutToBeChanged; otherwise
        # client code may obtain invalid indexes in its slot
369
        self._repopulateNodes(newnodeop=_nodeopmap[bool(flat)])
370

371 372 373 374 375 376 377 378
    def canFetchMore(self, parent):
        if parent.isValid():
            return False
        return not self._rootpopulated

    def fetchMore(self, parent):
        if parent.isValid() or self._rootpopulated:
            return
379
        assert len(self._rootentry) == 0, self._rootentry
380 381
        newroote = self._rootentry.copyskel()
        self._populateNodes(newroote)
382 383 384
        last = len(newroote) - 1
        if last >= 0:
            self.beginInsertRows(parent, 0, last)
385 386
        self._rootentry = newroote
        self._rootpopulated = True
387 388
        if last >= 0:
            self.endInsertRows()
389
        self.revLoaded.emit(self.rev())
390

391
    def _repopulateNodes(self, newnodeop=None, newroote=None):
392
        """Recreate populated nodes if any"""
393 394 395 396
        if not self._rootpopulated:
            # no stale nodes
            if newnodeop:
                self._nodeop = newnodeop
397 398
            if newroote:
                self._rootentry = newroote
399 400
            return

401 402 403 404
        self.layoutAboutToBeChanged.emit()
        try:
            oldindexmap = [(i, self.filePath(i))
                           for i in self.persistentIndexList()]
405 406
            if newnodeop:
                self._nodeop = newnodeop
407 408
            if not newroote:
                newroote = self._rootentry.copyskel()
409 410
            self._populateNodes(newroote)
            self._rootentry = newroote
411 412 413 414 415
            for oi, path in oldindexmap:
                self.changePersistentIndex(oi, self.indexFromPath(path))
        finally:
            self.layoutChanged.emit()

416
    def _newRevNode(self, rev, prev=FirstParent):
417
        """Create empty root node for the specified revision"""
418 419
        if not _isreporev(rev):
            raise ValueError('unacceptable revision number: %r' % rev)
420 421
        if not _isreporev(prev):
            raise ValueError('unacceptable parent revision number: %r' % prev)
422
        repo = self._repoagent.rawRepo()
423
        roote = _Entry()
424
        roote.ctx = repo[rev]
425 426 427 428 429 430
        if prev == ManifestModel.FirstParent:
            roote.pctx = roote.ctx.p1()
        elif prev == ManifestModel.SecondParent:
            roote.pctx = roote.ctx.p2()
        else:
            roote.pctx = repo[prev]
431 432 433 434
        return roote

    def _populateNodes(self, roote):
        repo = self._repoagent.rawRepo()
435 436
        lpat = hglib.fromunicode(self._namefilter)
        match = _makematcher(repo, roote.ctx, lpat, self._changedfilesonly)
437
        self._populate(roote, repo, self._nodeop, self._statusfilter, match)
438
        roote.sort()
439

Adrian Buehlmann's avatar
Adrian Buehlmann committed
440

441 442
class _Entry(object):
    """Each file or directory"""
443

444
    __slots__ = ('_name', '_parent', 'status', 'ctx', 'pctx', 'subkind',
445
                 '_child', '_nameindex')
446

447 448 449
    def __init__(self, name='', parent=None):
        self._name = name
        self._parent = parent
450
        self.status = None
451
        self.ctx = None
452
        self.pctx = None
453
        self.subkind = None
454 455 456
        self._child = {}
        self._nameindex = []

457 458 459 460 461 462 463 464 465
    def copyskel(self):
        """Create unpopulated copy of this entry"""
        e = self.__class__()
        e.status = self.status
        e.ctx = self.ctx
        e.pctx = self.pctx
        e.subkind = self.subkind
        return e

466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
    @property
    def parent(self):
        return self._parent

    @property
    def path(self):
        if self.parent is None or not self.parent.name:
            return self.name
        else:
            return self.parent.path + '/' + self.name

    @property
    def name(self):
        return self._name

481 482
    @property
    def isdir(self):
483
        return bool(self.subkind or self._child)
484

485 486 487
    def __len__(self):
        return len(self._child)

488 489 490 491
    def __nonzero__(self):
        # leaf node should not be False because of len(node) == 0
        return True

492 493
    __bool__ = __nonzero__

494 495 496
    def __getitem__(self, name):
        return self._child[name]

497
    def makechild(self, name):
498 499
        if name not in self._child:
            self._nameindex.append(name)
500 501
        self._child[name] = e = self.__class__(name, parent=self)
        return e
502

503
    def putchild(self, name, e):
504
        assert not e.name and not e.parent, (e.name, e.parent)
505 506 507 508 509 510
        e._name = name
        e._parent = self
        if name not in self._child:
            self._nameindex.append(name)
        self._child[name] = e

511 512 513 514 515 516 517 518
    def __contains__(self, item):
        return item in self._child

    def at(self, index):
        return self._child[self._nameindex[index]]

    def index(self, name):
        return self._nameindex.index(name)
519 520 521

    def sort(self, reverse=False):
        """Sort the entries recursively; directories first"""
522
        for e in self._child.values():
523 524
            e.sort(reverse=reverse)
        self._nameindex.sort(
525
            key=lambda s: (not self[s].isdir, os.path.normcase(s)),
526
            reverse=reverse)
527

528

529 530 531 532
def _isreporev(rev):
    # patchctx.rev() returns str, which isn't a valid repository revision
    return rev is None or isinstance(rev, int)

533
def _samectx(ctx1, ctx2):
534 535 536
    # no fast way to detect changes in uncommitted ctx, just assumes different
    if ctx1.rev() is None or not _isreporev(ctx1.rev()):
        return False
537 538
    # compare hash in case it was stripped and recreated (e.g. by qrefresh)
    return ctx1 == ctx2 and ctx1.node() == ctx2.node()
539

540
# TODO: visual feedback to denote query type and error as in repofilter
541
def _makematcher(repo, ctx, pat, changedonly):
542
    cwd = b''  # always relative to repo root
543
    patterns = []
544
    if pat and b':' not in pat and b'*' not in pat:
545
        # mimic case-insensitive partial string match
546
        patterns.append(b'relre:(?i)' + re.escape(pat))
547 548
    elif pat:
        patterns.append(pat)
549 550 551

    include = []
    if changedonly:
552
        include.extend(b'path:%s' % p for p in ctx.files())
553 554
        if not include:
            # no match
555
            return scmutil.matchfiles(repo, [])  # TODO: use matchmod.never()
556

557
    try:
558
        return matchmod.match(repo.root, cwd, patterns, include=include,
559
                              default=b'relglob', auditor=repo.auditor, ctx=ctx)
560 561
    except (error.Abort, error.ParseError):
        # no match
562
        return scmutil.matchfiles(repo, [])  # TODO: use matchmod.never()
563 564


565
class _listnodeop(object):
566 567
    subreporecursive = False

568 569 570 571
    @staticmethod
    def findpath(e, path):
        return e[path]

572 573
    @staticmethod
    def makepath(e, path):
574
        return e.makechild(path)
575

576 577 578 579
    @staticmethod
    def putpath(e, path, c):
        e.putchild(path, c)

580
class _treenodeop(object):
581 582
    subreporecursive = True

583 584 585 586 587 588
    @staticmethod
    def findpath(e, path):
        for p in path.split('/'):
            e = e[p]
        return e

589 590 591 592
    @staticmethod
    def makepath(e, path):
        for p in path.split('/'):
            if p not in e:
593
                e.makechild(p)
594 595 596
            e = e[p]
        return e

597 598 599 600 601 602 603
    @staticmethod
    def putpath(e, path, c):
        rp = path.rfind('/')
        if rp >= 0:
            e = _treenodeop.makepath(e, path[:rp])
        e.putchild(path[rp + 1:], c)

604 605 606 607
_nodeopmap = {
    False: _treenodeop,
    True: _listnodeop,
    }
608

609

610
def _populaterepo(roote, repo, nodeop, statusfilter, match):
611
    if 'S' in statusfilter:
612
        _populatesubrepos(roote, repo, nodeop, statusfilter, match)
613 614

    ctx = roote.ctx
615
    pctx = roote.pctx
616 617

    with lfutil.lfstatus(repo):
618
        stat = repo.status(pctx, ctx, match, clean='C' in statusfilter)
619

620 621
    for st, files in zip('MAR!?IC', stat):
        if st not in statusfilter:
622
            continue
623
        for path in files:
624
            e = nodeop.makepath(roote, hglib.tounicode(path))
625
            e.status = st
626

627 628 629
def _comparesubstate(state1, state2):
    if state1 == state2:
        return 'C'
630
    elif state1 == hglib.nullsubrepostate:
631
        return 'A'
632
    elif state2 == hglib.nullsubrepostate:
633 634 635 636
        return 'R'
    else:
        return 'M'

637
def _populatesubrepos(roote, repo, nodeop, statusfilter, match):
638
    ctx = roote.ctx
639
    pctx = roote.pctx
640 641 642
    subpaths = set(pctx.substate)
    subpaths.update(ctx.substate)
    for path in subpaths:
643 644
        substate = ctx.substate.get(path, hglib.nullsubrepostate)
        psubstate = pctx.substate.get(path, hglib.nullsubrepostate)
645
        e = _Entry()
646
        e.status = _comparesubstate(psubstate, substate)
647 648 649 650 651
        if e.status == 'R':
            # denotes the original subrepo has been removed
            e.subkind = psubstate[2]
        else:
            e.subkind = substate[2]
652

653 654
        # do not call ctx.sub() unnecessarily, which may raise Abort or OSError
        # if git or svn executable not found
655
        if (nodeop.subreporecursive and e.subkind == 'hg' and e.status != 'R'
656
            and os.path.isdir(repo.wjoin(path))):
657
            smatch = matchmod.subdirmatcher(path, match)
658
            try:
659
                srepo = ctx.sub(path)._repo
660
                e.ctx = scmutil.revsymbol(srepo, substate[1])
661
                e.pctx = scmutil.revsymbol(srepo, psubstate[1] or b'null')
662
                _populaterepo(e, srepo, nodeop, statusfilter, smatch)
663
            except (error.RepoError, EnvironmentError):
664
                pass
665

666 667 668 669
        # subrepo is filtered out only if the node and its children do not
        # match the specified condition at all
        if len(e) > 0 or (e.status in statusfilter and match(path)):
            nodeop.putpath(roote, hglib.tounicode(path), e)
670

671 672 673 674 675 676 677 678 679 680 681 682
def _populatepatch(roote, repo, nodeop, statusfilter, match):
    ctx = roote.ctx
    stat = ctx.changesToParent(0)
    for st, files in zip('MAR', stat):
        if st not in statusfilter:
            continue
        for path in files:
            if not match(path):
                continue
            e = nodeop.makepath(roote, hglib.tounicode(path))
            e.status = st

683

684 685 686 687 688 689
class ManifestCompleter(QCompleter):
    """QCompleter for ManifestModel"""

    def splitPath(self, path):
        """
        >>> c = ManifestCompleter()
690
        >>> c.splitPath(u'foo/bar')
691 692 693 694
        [u'foo', u'bar']

        trailing slash appends extra '', so that QCompleter can descend to
        next level:
695
        >>> c.splitPath(u'foo/')
696 697
        [u'foo', u'']
        """
698
        return pycompat.unicode(path).split('/')
699 700 701 702 703 704 705 706

    def pathFromIndex(self, index):
        if not index.isValid():
            return ''
        m = self.model()
        if not m:
            return ''
        return m.filePath(index)