PageRenderTime 121ms CodeModel.GetById 10ms app.highlight 101ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/repowidget.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 1130 lines | 1069 code | 42 blank | 19 comment | 60 complexity | 790e5ff1b2091dbdd18ffc212005002e MD5 | raw file
   1# repowidget.py - TortoiseHg repository widget
   2#
   3# Copyright (C) 2007-2010 Logilab. All rights reserved.
   4# Copyright (C) 2010 Adrian Buehlmann <adrian@cadifra.com>
   5#
   6# This software may be used and distributed according to the terms
   7# of the GNU General Public License, incorporated herein by reference.
   8
   9import binascii
  10import os
  11
  12from mercurial import util, revset, error
  13
  14from tortoisehg.util import shlib, hglib
  15
  16from tortoisehg.hgqt.i18n import _
  17from tortoisehg.hgqt.qtlib import geticon, getfont, QuestionMsgBox, InfoMsgBox
  18from tortoisehg.hgqt.qtlib import CustomPrompt, SharedWidget, DemandWidget
  19from tortoisehg.hgqt.repomodel import HgRepoListModel
  20from tortoisehg.hgqt import cmdui, update, tag, backout, merge, visdiff
  21from tortoisehg.hgqt import archive, thgimport, thgstrip, run, purge, bookmark
  22from tortoisehg.hgqt import bisect, rebase, resolve, thgrepo, compress, mq
  23from tortoisehg.hgqt import qdelete, qreorder, qrename, qfold, shelve
  24
  25from tortoisehg.hgqt.repofilter import RepoFilterBar
  26from tortoisehg.hgqt.repoview import HgRepoView
  27from tortoisehg.hgqt.revdetails import RevDetailsWidget
  28from tortoisehg.hgqt.commit import CommitWidget
  29from tortoisehg.hgqt.manifestdialog import ManifestTaskWidget
  30from tortoisehg.hgqt.sync import SyncWidget
  31from tortoisehg.hgqt.grep import SearchWidget
  32from tortoisehg.hgqt.quickbar import GotoQuickBar
  33from tortoisehg.hgqt.pbranch import PatchBranchWidget
  34
  35from PyQt4.QtCore import *
  36from PyQt4.QtGui import *
  37
  38class RepoWidget(QWidget):
  39
  40    showMessageSignal = pyqtSignal(QString)
  41    closeSelfSignal = pyqtSignal(QWidget)
  42
  43    output = pyqtSignal(QString, QString)
  44    progress = pyqtSignal(QString, object, QString, QString, object)
  45    makeLogVisible = pyqtSignal(bool)
  46
  47    revisionSelected = pyqtSignal(object)
  48
  49    titleChanged = pyqtSignal(unicode)
  50    """Emitted when changed the expected title for the RepoWidget tab"""
  51
  52    repoLinkClicked = pyqtSignal(unicode)
  53    """Emitted when clicked a link to open repository"""
  54
  55    singlecmenu = None
  56    unappcmenu = None
  57    paircmenu = None
  58    multicmenu = None
  59
  60    def __init__(self, repo, parent=None):
  61        QWidget.__init__(self, parent, acceptDrops=True)
  62
  63        self.repo = repo
  64        repo.repositoryChanged.connect(self.repositoryChanged)
  65        repo.repositoryDestroyed.connect(self.repositoryDestroyed)
  66        repo.configChanged.connect(self.configChanged)
  67        self.revsetfilter = False
  68        self.branch = ''
  69        self.bundle = None
  70        self.revset = set()
  71        self.namedTabs = {}
  72
  73        if repo.parents()[0].rev() == -1:
  74            self._reload_rev = 'tip'
  75        else:
  76            self._reload_rev = '.'
  77        self.currentMessage = ''
  78        self.runner = None
  79        self.dirty = False
  80
  81        self.setupUi()
  82        self.createActions()
  83        self.restoreSettings()
  84        self.setupModels()
  85
  86    def setupUi(self):
  87        SP = QSizePolicy
  88
  89        self.repotabs_splitter = QSplitter(orientation=Qt.Vertical)
  90        self.setLayout(QVBoxLayout())
  91        self.layout().setContentsMargins(0, 0, 0, 0)
  92        self.layout().setSpacing(0)
  93
  94        hbox = QHBoxLayout()
  95        hbox.setContentsMargins(0, 0, 0, 0)
  96        hbox.setSpacing(0)
  97        self.layout().addLayout(hbox)
  98
  99        self.gototb = tb = GotoQuickBar(self)
 100        tb.setObjectName('gototb')
 101        tb.gotoSignal.connect(self.goto)
 102        hbox.addWidget(tb)
 103
 104        self.bundleAccept = b = QPushButton(_('Accept'))
 105        b.setShown(False)
 106        b.setToolTip(_('Pull incoming changesets into your repository'))
 107        b.clicked.connect(self.acceptBundle)
 108        hbox.addWidget(b)
 109        self.bundleReject = b = QPushButton(_('Reject'))
 110        b.setToolTip(_('Reject incoming changesets'))
 111        b.clicked.connect(self.rejectBundle)
 112        b.setShown(False)
 113        hbox.addWidget(b)
 114
 115        self.filterbar = RepoFilterBar(self.repo, self)
 116        self.filterbar.branchChanged.connect(self.setBranch)
 117        self.filterbar.progress.connect(self.progress)
 118        self.filterbar.showMessage.connect(self.showMessage)
 119        self.filterbar.revisionSet.connect(self.setRevisionSet)
 120        self.filterbar.clearSet.connect(self.clearSet)
 121        self.filterbar.filterToggled.connect(self.filterToggled)
 122        hbox.addWidget(self.filterbar)
 123
 124        self.filterbar.hide()
 125
 126        self.revsetfilter = self.filterbar.filtercb.isChecked()
 127
 128        self.layout().addWidget(self.repotabs_splitter)
 129
 130        self.repoview = view = HgRepoView(self.repo, self)
 131        view.revisionClicked.connect(self.onRevisionClicked)
 132        view.revisionSelected.connect(self.onRevisionSelected)
 133        view.revisionAltClicked.connect(self.onRevisionSelected)
 134        view.revisionActivated.connect(self.revision_activated)
 135        view.showMessage.connect(self.showMessage)
 136        view.menuRequested.connect(self.viewMenuRequest)
 137
 138        sp = SP(SP.Expanding, SP.Expanding)
 139        sp.setHorizontalStretch(0)
 140        sp.setVerticalStretch(1)
 141        sp.setHeightForWidth(self.repoview.sizePolicy().hasHeightForWidth())
 142        view.setSizePolicy(sp)
 143        view.setFrameShape(QFrame.StyledPanel)
 144
 145        self.repotabs_splitter.addWidget(self.repoview)
 146        self.repotabs_splitter.setCollapsible(0, False)
 147        self.repotabs_splitter.setStretchFactor(0, 1)
 148
 149        self.taskTabsWidget = tt = QTabWidget()
 150        self.repotabs_splitter.addWidget(self.taskTabsWidget)
 151        self.repotabs_splitter.setStretchFactor(1, 1)
 152        tt.setDocumentMode(True)
 153        self.updateTaskTabs()
 154
 155        self.revDetailsWidget = w = RevDetailsWidget(self.repo)
 156        w.linkActivated.connect(self._openLink)
 157        w.revisionSelected.connect(self.repoview.goto)
 158        w.grepRequested.connect(self.grep)
 159        w.showMessage.connect(self.showMessage)
 160        self.logTabIndex = idx = tt.addTab(w, geticon('log'), '')
 161        tt.setTabToolTip(idx, _("Revision details"))
 162
 163        self.commitDemand = w = DemandWidget(self.createCommitWidget)
 164        self.commitTabIndex = idx = tt.addTab(w, geticon('Checkmark'), '')
 165        tt.setTabToolTip(idx, _("Commit"))
 166
 167        self.manifestDemand = w = DemandWidget(self.createManifestWidget)
 168        self.manifestTabIndex = idx = tt.addTab(w, geticon('annotate'), '')
 169        tt.setTabToolTip(idx, _('Manifest'))
 170
 171        self.grepDemand = w = DemandWidget(self.createGrepWidget)
 172        self.grepTabIndex = idx = tt.addTab(w, geticon('repobrowse'), '')
 173        tt.setTabToolTip(idx, _("Search"))
 174
 175        self.syncDemand = w = DemandWidget(self.createSyncWidget)
 176        self.syncTabIndex = idx = tt.addTab(w, geticon('view-refresh'), '')
 177        tt.setTabToolTip(idx, _("Synchronize"))
 178
 179        self.mqDemand = w = DemandWidget(self.createMQWidget)
 180        if 'mq' in self.repo.extensions():
 181            self.mqTabIndex = idx = tt.addTab(w, geticon('mq'), '')
 182            tt.setTabToolTip(idx, _("Patch Queue"))
 183            self.namedTabs['mq'] = idx
 184        else:
 185            self.mqTabIndex = -1
 186
 187        self.pbranchDemand = w = DemandWidget(self.createPatchBranchWidget)
 188        if 'pbranch' in self.repo.extensions():
 189            self.pbranchTabIndex = idx = tt.addTab(w, geticon('branch'), '')
 190            tt.setTabToolTip(idx, _("Patch Branch"))
 191            self.namedTabs['pbranch'] = idx
 192        else:
 193            self.pbranchTabIndex = -1
 194
 195    def switchToNamedTaskTab(self, tabname):
 196        if tabname in self.namedTabs:
 197            idx = self.namedTabs[tabname]
 198            self.taskTabsWidget.setCurrentIndex(idx)
 199
 200    def title(self):
 201        """Returns the expected title for this widget [unicode]"""
 202        if self.bundle:
 203            return _('%s <incoming>') % self.repo.shortname
 204        elif self.branch:
 205            return '%s [%s]' % (self.repo.shortname, self.branch)
 206        else:
 207            return self.repo.shortname
 208
 209    @pyqtSlot()
 210    def toggleFilterBar(self):
 211        """Toggle display repowidget filter bar"""
 212        vis = self.filterbar.isVisible()
 213        self.filterbar.setVisible(not vis)
 214
 215    @pyqtSlot()
 216    def toggleGotoBar(self):
 217        """Toggle display repowidget goto bar"""
 218        vis = self.gototb.isVisible()
 219        self.gototb.setVisible(not vis)
 220
 221    @pyqtSlot(unicode)
 222    def _openLink(self, link):
 223        link = unicode(link)
 224        handlers = {'cset': self.goto,
 225                    'subrepo': self.repoLinkClicked.emit,
 226                    'shelve' : self.shelve}
 227        if ':' in link:
 228            scheme, param = link.split(':', 1)
 229            hdr = handlers.get(scheme)
 230            if hdr:
 231                return hdr(param)
 232
 233        QDesktopServices.openUrl(QUrl(link))
 234
 235    def getCommitWidget(self):
 236        return getattr(self.repo, '_commitwidget', None)  # TODO: ugly
 237
 238    def createCommitWidget(self):
 239        cw = self.getCommitWidget()
 240        if not cw:
 241            pats = {}
 242            opts = {}
 243            b = QPushButton(_('Commit'))
 244            b.setAutoDefault(True)
 245            f = b.font()
 246            f.setWeight(QFont.Bold)
 247            b.setFont(f)
 248            cw = CommitWidget(pats, opts, self.repo.root, True, self)
 249            cw.buttonHBox.addWidget(b)
 250            cw.commitButtonEnable.connect(b.setEnabled)
 251            b.clicked.connect(cw.commit)
 252            cw.loadSettings(QSettings(), 'workbench')
 253            QTimer.singleShot(0, cw.reload)
 254            self.repo._commitwidget = cw
 255
 256        cw = SharedWidget(cw)
 257        cw.output.connect(self.output)
 258        cw.progress.connect(self.progress)
 259        cw.makeLogVisible.connect(self.makeLogVisible)
 260        cw.linkActivated.connect(self._openLink)
 261
 262        cw.showMessage.connect(self.showMessage)
 263        return cw
 264
 265    def createManifestWidget(self):
 266        if isinstance(self.rev, basestring):
 267            rev = None
 268        else:
 269            rev = self.rev
 270        w = ManifestTaskWidget(self.repo, rev, self)
 271        w.loadSettings(QSettings(), 'workbench')
 272        w.revChanged.connect(self.repoview.goto)
 273        w.revisionHint.connect(self.showMessage)
 274        w.grepRequested.connect(self.grep)
 275        return w
 276
 277    def createSyncWidget(self):
 278        sw = getattr(self.repo, '_syncwidget', None)  # TODO: ugly
 279        if not sw:
 280            sw = SyncWidget(self.repo, True, self)
 281            self.repo._syncwidget = sw
 282        sw = SharedWidget(sw)
 283        sw.output.connect(self.output)
 284        sw.progress.connect(self.progress)
 285        sw.makeLogVisible.connect(self.makeLogVisible)
 286        sw.outgoingNodes.connect(self.setOutgoingNodes)
 287        sw.showMessage.connect(self.showMessage)
 288        sw.incomingBundle.connect(self.setBundle)
 289        sw.refreshTargets(self.rev)
 290        return sw
 291
 292    @pyqtSlot(QString)
 293    def setBundle(self, bfile):
 294        self.bundle = unicode(bfile)
 295        oldlen = len(self.repo)
 296        self.repo = thgrepo.repository(self.repo.ui, self.repo.root,
 297                                       bundle=self.bundle)
 298        self.repoview.setRepo(self.repo)
 299        self.revDetailsWidget.setRepo(self.repo)
 300        self.bundleAccept.setHidden(False)
 301        self.bundleReject.setHidden(False)
 302        self.filterbar.revsetle.setText('incoming()')
 303        self.filterbar.setEnabled(False)
 304        self.filterbar.show()
 305        self.titleChanged.emit(self.title())
 306        newlen = len(self.repo)
 307        self.revset = [self.repo[n].node() for n in range(oldlen, newlen)]
 308        self.repomodel.revset = self.revset
 309        self.reload()
 310
 311    def clearBundle(self):
 312        self.bundleAccept.setHidden(True)
 313        self.bundleReject.setHidden(True)
 314        self.filterbar.setEnabled(True)
 315        self.filterbar.revsetle.setText('')
 316        self.revset = []
 317        self.repomodel.revset = self.revset
 318        self.bundle = None
 319        self.titleChanged.emit(self.title())
 320        self.repo = thgrepo.repository(self.repo.ui, self.repo.root)
 321        self.repoview.setRepo(self.repo)
 322        self.revDetailsWidget.setRepo(self.repo)
 323
 324    def acceptBundle(self):
 325        self.taskTabsWidget.setCurrentIndex(self.syncTabIndex)
 326        self.syncDemand.pullBundle(self.bundle, None)
 327        self.clearBundle()
 328
 329    def rejectBundle(self):
 330        self.clearBundle()
 331        self.reload()
 332
 333    def clearSet(self):
 334        self.revset = []
 335        if self.revsetfilter:
 336            self.reload()
 337        else:
 338            self.repomodel.revset = []
 339            self.refresh()
 340
 341    def setRevisionSet(self, nodes):
 342        self.revset = [self.repo[n].node() for n in nodes]
 343        if self.revsetfilter:
 344            self.reload()
 345        else:
 346            self.repomodel.revset = self.revset
 347            self.refresh()
 348
 349    @pyqtSlot(bool)
 350    def filterToggled(self, checked):
 351        self.revsetfilter = checked
 352        if self.revset:
 353            self.repomodel.filterbyrevset = checked
 354            self.reload()
 355
 356    def setOutgoingNodes(self, nodes):
 357        self.filterbar.revsetle.setText('outgoing()')
 358        self.filterbar.show()
 359        self.setRevisionSet(nodes)
 360
 361    def createGrepWidget(self):
 362        upats = {}
 363        gw = SearchWidget(upats, self.repo, self)
 364        gw.setRevision(self.repoview.current_rev)
 365        gw.showMessage.connect(self.showMessage)
 366        gw.progress.connect(self.progress)
 367        gw.revisionSelected.connect(self.goto)
 368        return gw
 369
 370    def createMQWidget(self):
 371        mqw = mq.MQWidget(self.repo, self)
 372        mqw.output.connect(self.output)
 373        mqw.progress.connect(self.progress)
 374        mqw.makeLogVisible.connect(self.makeLogVisible)
 375        mqw.showMessage.connect(self.showMessage)
 376        return mqw
 377
 378    def createPatchBranchWidget(self):
 379        pbw = PatchBranchWidget(self.repo, parent=self)
 380        pbw.output.connect(self.output)
 381        pbw.progress.connect(self.progress)
 382        pbw.makeLogVisible.connect(self.makeLogVisible)
 383        return pbw
 384
 385    def reponame(self):
 386        return self.repo.shortname
 387
 388    @property
 389    def rev(self):
 390        """Returns the current active revision"""
 391        return self.repoview.current_rev
 392
 393    def showMessage(self, msg):
 394        self.currentMessage = msg
 395        if self.isVisible():
 396            self.showMessageSignal.emit(msg)
 397
 398    def showEvent(self, event):
 399        QWidget.showEvent(self, event)
 400        self.showMessageSignal.emit(self.currentMessage)
 401        if self.dirty:
 402            print 'page was dirty, reloading...'
 403            self.reload()
 404            self.dirty = False
 405
 406    def createActions(self):
 407        QShortcut(QKeySequence('CTRL+P'), self, self.gotoParent)
 408
 409    def dragEnterEvent(self, event):
 410        paths = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
 411        if util.any(os.path.isfile(p) for p in paths):
 412            event.setDropAction(Qt.CopyAction)
 413            event.accept()
 414
 415    def dropEvent(self, event):
 416        paths = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
 417        filepaths = [p for p in paths if os.path.isfile(p)]
 418        if filepaths:
 419            self.thgimport(filepaths)
 420            event.setDropAction(Qt.CopyAction)
 421            event.accept()
 422
 423    ## Begin Workbench event forwards
 424
 425    def back(self):
 426        self.repoview.back()
 427
 428    def forward(self):
 429        self.repoview.forward()
 430
 431    def bisect(self):
 432        dlg = bisect.BisectDialog(self.repo, {}, self)
 433        dlg.finished.connect(dlg.deleteLater)
 434        dlg.exec_()
 435
 436    def resolve(self):
 437        dlg = resolve.ResolveDialog(self.repo, self)
 438        dlg.finished.connect(dlg.deleteLater)
 439        dlg.exec_()
 440
 441    def thgimport(self, paths=None):
 442        dlg = thgimport.ImportDialog(repo=self.repo, parent=self)
 443        dlg.finished.connect(dlg.deleteLater)
 444        if paths:
 445            dlg.setfilepaths(paths)
 446        dlg.exec_()
 447
 448    def shelve(self, arg=None):
 449        dlg = shelve.ShelveDialog(self.repo, self)
 450        dlg.finished.connect(dlg.deleteLater)
 451        dlg.exec_()
 452
 453    def verify(self):
 454        cmdline = ['--repository', self.repo.root, 'verify', '--verbose']
 455        dlg = cmdui.Dialog(cmdline, self)
 456        dlg.exec_()
 457
 458    def recover(self):
 459        cmdline = ['--repository', self.repo.root, 'recover', '--verbose']
 460        dlg = cmdui.Dialog(cmdline, self)
 461        dlg.exec_()
 462
 463    def rollback(self):
 464        def read_undo():
 465            if os.path.exists(self.repo.sjoin('undo')):
 466                try:
 467                    args = self.repo.opener('undo.desc', 'r').read().splitlines()
 468                    if args[1] != 'commit':
 469                        return None
 470                    return args[1], int(args[0])
 471                except (IOError, IndexError, ValueError):
 472                    pass
 473            return None
 474        data = read_undo()
 475        if data is None:
 476            InfoMsgBox(_('No transaction available'),
 477                       _('There is no rollback transaction available'))
 478            return
 479        elif data[0] == 'commit':
 480            if not QuestionMsgBox(_('Undo last commit?'),
 481                   _('Undo most recent commit (%d), preserving file changes?') %
 482                   data[1]):
 483                return
 484        else:
 485            if not QuestionMsgBox(_('Undo last transaction?'),
 486                    _('Rollback to revision %d (undo %s)?') %
 487                    (data[1]-1, data[0])):
 488                return
 489            try:
 490                rev = self.repo['.'].rev()
 491            except Exception, e:
 492                InfoMsgBox(_('Repository Error'),
 493                           _('Unable to determine working copy revision\n') +
 494                           hglib.tounicode(e))
 495                return
 496            if rev >= data[1] and not QuestionMsgBox(
 497                    _('Remove current working revision?'),
 498                    _('Your current working revision (%d) will be removed '
 499                      'by this rollback, leaving uncommitted changes.\n '
 500                      'Continue?' % rev)):
 501                    return
 502        cmdline = ['rollback', '--repository', self.repo.root, '--verbose']
 503        self.runCommand(_('Rollback - TortoiseHg'), cmdline)
 504
 505    def purge(self):
 506        dlg = purge.PurgeDialog(self.repo, self)
 507        dlg.setWindowFlags(Qt.Sheet)
 508        dlg.setWindowModality(Qt.WindowModal)
 509        dlg.showMessage.connect(self.showMessage)
 510        dlg.progress.connect(self.progress)
 511        dlg.finished.connect(dlg.deleteLater)
 512        dlg.exec_()
 513
 514    ## End workbench event forwards
 515
 516    @pyqtSlot(unicode, dict)
 517    def grep(self, pattern='', opts={}):
 518        """Open grep task tab"""
 519        opts = dict((str(k), str(v)) for k, v in opts.iteritems())
 520        self.taskTabsWidget.setCurrentIndex(self.grepTabIndex)
 521        self.grepDemand.setSearch(pattern, **opts)
 522
 523    def setupModels(self):
 524        # Filter revision set in case revisions were removed
 525        self.revset = [r for r in self.revset if r in self.repo]
 526        self.repomodel = HgRepoListModel(self.repo, self.branch, self.revset,
 527                                         self.revsetfilter, self)
 528        self.repomodel.filled.connect(self.modelFilled)
 529        self.repomodel.loaded.connect(self.modelLoaded)
 530        self.repomodel.showMessage.connect(self.showMessage)
 531        self.repoview.setModel(self.repomodel)
 532        self.gototb.setCompletionKeys(self.repo.tags().keys())
 533
 534    def modelFilled(self):
 535        'initial batch of revisions loaded'
 536        self.repoview.resizeColumns()
 537        self.repoview.goto(self._reload_rev) # emits revision_selected
 538        self.revDetailsWidget.finishReload()
 539
 540    def modelLoaded(self):
 541        'all revisions loaded (graph generator completed)'
 542        # Perhaps we can update a GUI element later, to indicate full load
 543        pass
 544
 545    def onRevisionClicked(self, rev):
 546        'User clicked on a repoview row'
 547        tw = self.taskTabsWidget
 548        if rev is None:
 549            # Clicking on working copy switches to commit tab
 550            tw.setCurrentIndex(self.commitTabIndex)
 551        else:
 552            # Clicking on a normal revision switches from commit tab
 553            tw.setCurrentIndex(self.logTabIndex)
 554
 555    def onRevisionSelected(self, rev):
 556        'View selection changed, could be a reload'
 557        if self.repomodel.graph is None:
 558            return
 559        if type(rev) != str: # unapplied patch
 560            self.manifestDemand.forward('setRev', rev)
 561            self.grepDemand.forward('setRevision', rev)
 562            self.syncDemand.forward('refreshTargets', rev)
 563        self.revDetailsWidget.revision_selected(rev)
 564        self.revisionSelected.emit(rev)
 565
 566    def gotoParent(self):
 567        self.repoview.clearSelection()
 568        self.goto('.')
 569
 570    def goto(self, rev):
 571        self._reload_rev = rev
 572        self.repoview.goto(rev)
 573
 574    def revision_activated(self, rev=None):
 575        rev = rev or self.rev
 576        if isinstance(rev, basestring):  # unapplied patch
 577            return
 578        dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], {'change':rev})
 579        if dlg:
 580            dlg.exec_()
 581
 582    def reload(self):
 583        'Initiate a refresh of the repo model, rebuild graph'
 584        self.repo.thginvalidate()
 585        self.rebuildGraph()
 586        self.filterbar.refresh()
 587        if self.taskTabsWidget.currentIndex() == self.commitTabIndex:
 588            self.commitDemand.forward('reload')
 589
 590    def rebuildGraph(self):
 591        self.showMessage('')
 592        if self.rev is None or len(self.repo) > self.rev:
 593            self._reload_rev = self.rev
 594        else:
 595            self._reload_rev = 'tip'
 596        self.setupModels()
 597        self.revDetailsWidget.record()
 598
 599    def reloadTaskTab(self):
 600        tti = self.taskTabsWidget.currentIndex()
 601        if tti == self.logTabIndex:
 602            ttw = self.revDetailsWidget
 603        elif tti == self.commitTabIndex:
 604            ttw = self.commitDemand.get()
 605        elif tti == self.manifestTabIndex:
 606            ttw = self.manifestDemand.get()
 607        elif tti == self.syncTabIndex:
 608            ttw = self.syncDemand.get()
 609        elif tti == self.grepTabIndex:
 610            ttw = self.grepDemand.get()
 611        elif tti == self.pbranchTabIndex:
 612            ttw = self.pbranchDemand.get()
 613        elif tti == self.mqTabIndex:
 614            ttw = self.mqDemand.get()
 615        if ttw:
 616            ttw.reload()
 617
 618    def refresh(self):
 619        'Refresh the repo model view, clear cached data'
 620        self.repo.thginvalidate()
 621        self.repomodel.invalidate()
 622        self.revDetailsWidget.reload()
 623        self.filterbar.refresh()
 624
 625    def repositoryDestroyed(self):
 626        'Repository has detected itself to be deleted'
 627        self.closeSelfSignal.emit(self)
 628
 629    def repositoryChanged(self):
 630        'Repository has detected a changelog / dirstate change'
 631        if self.isVisible():
 632            try:
 633                self.rebuildGraph()
 634            except (error.RevlogError, error.RepoError), e:
 635                self.showMessage(hglib.tounicode(str(e)))
 636                self.repomodel = HgRepoListModel(None, None, None, False, self)
 637                self.repoview.setModel(self.repomodel)
 638        else:
 639            self.dirty = True
 640
 641    def configChanged(self):
 642        'Repository is reporting its config files have changed'
 643        self.repomodel.invalidate()
 644        self.revDetailsWidget.reload()
 645        self.titleChanged.emit(self.title())
 646        self.updateTaskTabs()
 647
 648    def updateTaskTabs(self):
 649        val = self.repo.ui.config('tortoisehg', 'tasktabs', 'off').lower()
 650        if val == 'east':
 651            self.taskTabsWidget.setTabPosition(QTabWidget.East)
 652        elif val == 'west':
 653            self.taskTabsWidget.setTabPosition(QTabWidget.West)
 654        else:
 655            self.taskTabsWidget.tabBar().hide()
 656
 657    @pyqtSlot(unicode, bool)
 658    def setBranch(self, branch, allparents=True):
 659        'Change the branch filter'
 660        self.branch = branch
 661        self.repomodel.setBranch(branch=branch, allparents=allparents)
 662        self.titleChanged.emit(self.title())
 663
 664    ##
 665    ## Workbench methods
 666    ##
 667
 668    def canGoBack(self):
 669        return self.repoview.canGoBack()
 670
 671    def canGoForward(self):
 672        return self.repoview.canGoForward()
 673
 674    def storeSettings(self):
 675        self.revDetailsWidget.storeSettings()
 676        s = QSettings()
 677        repoid = str(self.repo[0])
 678        s.setValue('repowidget/splitter-'+repoid,
 679                   self.repotabs_splitter.saveState())
 680
 681    def restoreSettings(self):
 682        self.revDetailsWidget.restoreSettings()
 683        s = QSettings()
 684        repoid = str(self.repo[0])
 685        self.repotabs_splitter.restoreState(
 686            s.value('repowidget/splitter-'+repoid).toByteArray())
 687
 688    def okToContinue(self):
 689        return self.commitDemand.forward('canExit', default=True) and \
 690               self.syncDemand.forward('canExit', default=True) and \
 691               self.mqDemand.forward('canExit', default=True)
 692
 693    def closeRepoWidget(self):
 694        '''returns False if close should be aborted'''
 695        if not self.okToContinue():
 696            return False
 697        if self.isVisible():
 698            # assuming here that there is at most one RepoWidget visible
 699            self.storeSettings()
 700        self.revDetailsWidget.storeSettings()
 701        s = QSettings()
 702        self.commitDemand.forward('saveSettings', s, 'workbench')
 703        self.manifestDemand.forward('saveSettings', s, 'workbench')
 704        self.filterbar.storeConfigs(s)
 705        return True
 706
 707    def incoming(self):
 708        self.taskTabsWidget.setCurrentIndex(self.syncTabIndex)
 709        self.syncDemand.get().incoming()
 710
 711    def pull(self):
 712        self.taskTabsWidget.setCurrentIndex(self.syncTabIndex)
 713        self.syncDemand.get().pull()
 714
 715    def outgoing(self):
 716        self.taskTabsWidget.setCurrentIndex(self.syncTabIndex)
 717        self.syncDemand.get().outgoing()
 718
 719    def push(self):
 720        self.taskTabsWidget.setCurrentIndex(self.syncTabIndex)
 721        self.syncDemand.get().push()
 722
 723    def qpush(self):
 724        """QPush a patch from MQ"""
 725        self.taskTabsWidget.setCurrentIndex(self.mqTabIndex)
 726        self.mqDemand.get().onPush()
 727
 728    def qpop(self):
 729        """QPop a patch from MQ"""
 730        self.taskTabsWidget.setCurrentIndex(self.mqTabIndex)
 731        self.mqDemand.get().onPop()
 732
 733    ##
 734    ## Repoview context menu
 735    ##
 736
 737    def viewMenuRequest(self, point, selection):
 738        'User requested a context menu in repo view widget'
 739
 740        # selection is a list of the currently selected revisions.
 741        # Integers for changelog revisions, None for the working copy,
 742        # or strings for unapplied patches.
 743
 744        if len(selection) == 0:
 745            return
 746        allunapp = False
 747        if 'mq' in self.repo.extensions():
 748            for rev in selection:
 749                if not self.repo.changectx(rev).thgmqunappliedpatch():
 750                    break
 751            else:
 752                allunapp = True
 753        if allunapp:
 754            self.unnapliedPatchMenu(point, selection)
 755        elif len(selection) == 1:
 756            self.singleSelectionMenu(point, selection)
 757        elif len(selection) == 2:
 758            self.doubleSelectionMenu(point, selection)
 759        else:
 760            self.multipleSelectionMenu(point, selection)
 761
 762    def unnapliedPatchMenu(self, point, selection):
 763        def qdeleteact():
 764            """Delete unapplied patch(es)"""
 765            dlg = qdelete.QDeleteDialog(self.repo, self.menuselection, self)
 766            dlg.finished.connect(dlg.deleteLater)
 767            dlg.output.connect(self.output)
 768            dlg.makeLogVisible.connect(self.makeLogVisible)
 769            dlg.exec_()
 770        def qreorderact():
 771            def checkGuardsOrComments():
 772                cont = True
 773                for p in self.repo.mq.full_series:
 774                    if '#' in p:
 775                        cont = QuestionMsgBox('Confirm qreorder',
 776                                _('<p>ATTENTION!<br>'
 777                                  'Guard or comment found.<br>'
 778                                  'Reordering patches will destroy them.<br>'
 779                                  '<br>Continue?</p>'), parent=self,
 780                                  defaultbutton=QMessageBox.No)
 781                        break
 782                return cont
 783            if checkGuardsOrComments():
 784                dlg = qreorder.QReorderDialog(self.repo, self)
 785                dlg.finished.connect(dlg.deleteLater)
 786                dlg.exec_()
 787        def qfoldact():
 788            dlg = qfold.QFoldDialog(self.repo, self.menuselection, self)
 789            dlg.finished.connect(dlg.deleteLater)
 790            dlg.output.connect(self.output)
 791            dlg.makeLogVisible.connect(self.makeLogVisible)
 792            dlg.exec_()
 793
 794        # Special menu for unapplied patches
 795        if not self.unappcmenu:
 796            menu = QMenu(self)
 797            acts = []
 798            for name, cb in (
 799                (_('Goto patch'), self.qgotoRevision),
 800                (_('Rename patch'), self.qrenameRevision),
 801                (_('Fold patches'), qfoldact),
 802                (_('Delete patches'), qdeleteact),
 803                (_('Reorder patches'), qreorderact)):
 804                act = QAction(name, self)
 805                act.triggered.connect(cb)
 806                acts.append(act)
 807                menu.addAction(act)
 808            self.unappcmenu = menu
 809            self.unappacts = acts
 810        self.menuselection = selection
 811        self.unappacts[0].setEnabled(len(selection) == 1)
 812        self.unappacts[1].setEnabled(len(selection) == 1)
 813        self.unappacts[2].setEnabled('qtip' in self.repo.tags())
 814        self.unappcmenu.exec_(point)
 815
 816    def singleSelectionMenu(self, point, selection):
 817        if not self.singlecmenu:
 818            items = []
 819
 820            # isrev = the changeset has an integer revision number
 821            # isctx = changectx or workingctx, not PatchContext
 822            # fixed = the changeset is considered permanent
 823            # patch = any patch applied or not
 824            # qpar  = patch queue parent
 825            isrev   = lambda ap, up, qp, wd: not (up or wd)
 826            isctx   = lambda ap, up, qp, wd: not up
 827            fixed   = lambda ap, up, qp, wd: not (ap or up or wd)
 828            patch   = lambda ap, up, qp, wd: ap or up
 829            qpar    = lambda ap, up, qp, wd: qp
 830            applied = lambda ap, up, qp, wd: ap
 831            unapp   = lambda ap, up, qp, wd: up
 832
 833            exs = self.repo.extensions()
 834            menu = QMenu(self)
 835            for ext, func, desc, icon, cb in (
 836                (None, isrev, _('Update...'), 'update',
 837                    self.updateToRevision),
 838                (None, fixed, _('Merge with...'), 'merge',
 839                    self.mergeWithRevision),
 840                (None, isctx, _('Browse at rev...'), None,
 841                    self.manifestRevision),
 842                (None, fixed, _('Tag...'), 'tag', self.tagToRevision),
 843                ('bookmarks', fixed, _('Bookmark...'), 'bookmark',
 844                    self.bookmarkRevision),
 845                (None, fixed, _('Backout...'), None, self.backoutToRevision),
 846                (None, isrev, _('Export patch'), None, self.exportRevisions),
 847                (None, isrev, _('Email patch...'), None, self.emailRevision),
 848                (None, isrev, _('Archive...'), None, self.archiveRevision),
 849                (None, isrev, _('Copy hash'), None, self.copyHash),
 850                ('transplant', fixed, _('Transplant to local'), None,
 851                    self.transplantRevision),
 852                ('rebase', None, None, None, None),
 853                ('rebase', fixed, _('Rebase...'), None, self.rebaseRevision),
 854                ('mq', None, None, None, None),
 855                ('mq', fixed, _('Import to MQ'), None, self.qimportRevision),
 856                ('mq', applied, _('Finish patch'), None, self.qfinishRevision),
 857                ('mq', qpar, _('Pop all patches'), None, self.qpopAllRevision),
 858                ('mq', patch, _('Goto patch'), None, self.qgotoRevision),
 859                ('mq', patch, _('Rename patch'), None, self.qrenameRevision),
 860                ('mq', fixed, _('Strip...'), None, self.stripRevision),
 861                ('reviewboard', fixed, _('Post to Review Board...'),
 862                    'reviewboard', self.sendToReviewBoard)):
 863                if ext and ext not in exs:
 864                    continue
 865                if desc is None:
 866                    menu.addSeparator()
 867                else:
 868                    act = QAction(desc, self)
 869                    act.triggered.connect(cb)
 870                    if icon:
 871                        act.setIcon(geticon(icon))
 872                    act.enableFunc = func
 873                    menu.addAction(act)
 874                    items.append(act)
 875            self.singlecmenu = menu
 876            self.singlecmenuitems = items
 877
 878        ctx = self.repo.changectx(self.rev)
 879        applied = ctx.thgmqappliedpatch()
 880        unapp = ctx.thgmqunappliedpatch()
 881        qparent = 'qparent' in ctx.tags()
 882        working = self.rev is None
 883
 884        for item in self.singlecmenuitems:
 885            enabled = item.enableFunc(applied, unapp, qparent, working)
 886            item.setEnabled(enabled)
 887
 888        self.singlecmenu.exec_(point)
 889
 890    def exportRevisions(self, revisions):
 891        if not revisions:
 892            revisions = [self.rev]
 893        epath = os.path.join(self.repo.root, self.repo.shortname + '_%r.patch')
 894        cmdline = ['export', '--repository', self.repo.root, '--verbose',
 895                   '--output', epath]
 896        for rev in revisions:
 897            cmdline.extend(['--rev', str(rev)])
 898        self.runCommand(_('Export - TortoiseHg'), cmdline)
 899
 900    def doubleSelectionMenu(self, point, selection):
 901        for r in selection:
 902            # No pair menu if working directory or unapplied patch
 903            if type(r) is not int:
 904                return
 905        def dagrange():
 906            revA, revB = self.menuselection
 907            if revA > revB:
 908                B, A = self.menuselection
 909            else:
 910                A, B = self.menuselection
 911            func = revset.match('%s::%s' % (A, B))
 912            return [c for c in func(self.repo, range(len(self.repo)))]
 913
 914        def exportPair():
 915            self.exportRevisions(self.menuselection)
 916        def exportDagRange():
 917            l = dagrange()
 918            if l:
 919                self.exportRevisions(l)
 920        def diffPair():
 921            revA, revB = self.menuselection
 922            dlg = visdiff.visualdiff(self.repo.ui, self.repo, [],
 923                    {'rev':(str(revA), str(revB))})
 924            if dlg:
 925                dlg.exec_()
 926        def emailPair():
 927            run.email(self.repo.ui, rev=self.menuselection, repo=self.repo)
 928        def emailDagRange():
 929            l = dagrange()
 930            if l:
 931                run.email(self.repo.ui, rev=l, repo=self.repo)
 932        def bisectNormal():
 933            revA, revB = self.menuselection
 934            opts = {'good':str(revA), 'bad':str(revB)}
 935            dlg = bisect.BisectDialog(self.repo, opts, self)
 936            dlg.finished.connect(dlg.deleteLater)
 937            dlg.exec_()
 938        def bisectReverse():
 939            revA, revB = self.menuselection
 940            opts = {'good':str(revB), 'bad':str(revA)}
 941            dlg = bisect.BisectDialog(self.repo, opts, self)
 942            dlg.finished.connect(dlg.deleteLater)
 943            dlg.exec_()
 944        def compressDlg():
 945            ctxa = self.repo[self.menuselection[0]]
 946            ctxb = self.repo[self.menuselection[1]]
 947            if ctxa.ancestor(ctxb) == ctxb:
 948                revs = self.menuselection[:]
 949            elif ctxa.ancestor(ctxb) == ctxa:
 950                revs = self.menuselection[:]
 951                revs.reverse()
 952            else:
 953                InfoMsgBox(_('Unable to compress history'),
 954                           _('Selected changeset pair not related'))
 955                return
 956            dlg = compress.CompressDialog(self.repo, revs, self)
 957            dlg.finished.connect(dlg.deleteLater)
 958            dlg.exec_()
 959
 960
 961        if not self.paircmenu:
 962            menu = QMenu(self)
 963            for name, cb in (
 964                    (_('Visual Diff...'), diffPair),
 965                    (_('Export Pair'), exportPair),
 966                    (_('Email Pair...'), emailPair),
 967                    (_('Export DAG Range'), exportDagRange),
 968                    (_('Email DAG Range...'), emailDagRange),
 969                    (_('Bisect - Good, Bad...'), bisectNormal),
 970                    (_('Bisect - Bad, Good...'), bisectReverse),
 971                    (_('Compress History...'), compressDlg)
 972                    ):
 973                a = QAction(name, self)
 974                a.triggered.connect(cb)
 975                menu.addAction(a)
 976            if 'reviewboard' in self.repo.extensions():
 977                a = QAction(_('Post Pair to Review Board...'), self)
 978                a.triggered.connect(self.sendToReviewBoard)
 979                menu.addAction(a)
 980            self.paircmenu = menu
 981        self.menuselection = selection
 982        self.paircmenu.exec_(point)
 983
 984    def multipleSelectionMenu(self, point, selection):
 985        for r in selection:
 986            # No multi menu if working directory or unapplied patch
 987            if type(r) is not int:
 988                return
 989        def exportSel():
 990            self.exportRevisions(self.menuselection)
 991        def emailSel():
 992            run.email(self.repo.ui, rev=self.menuselection, repo=self.repo)
 993        if not self.multicmenu:
 994            menu = QMenu(self)
 995            for name, cb in (
 996                    (_('Export Selected'), exportSel),
 997                    (_('Email Selected...'), emailSel),
 998                    ):
 999                a = QAction(name, self)
1000                a.triggered.connect(cb)
1001                menu.addAction(a)
1002            if 'reviewboard' in self.repo.extensions():
1003                a = QAction(_('Post Selected to Review Board...'), self)
1004                a.triggered.connect(self.sendToReviewBoard)
1005                menu.addAction(a)
1006            self.multicmenu = menu
1007        self.menuselection = selection
1008        self.multicmenu.exec_(point)
1009
1010    def updateToRevision(self):
1011        dlg = update.UpdateDialog(self.repo, self.rev, self)
1012        dlg.output.connect(self.output)
1013        dlg.makeLogVisible.connect(self.makeLogVisible)
1014        dlg.progress.connect(self.progress)
1015        dlg.finished.connect(dlg.deleteLater)
1016        dlg.exec_()
1017
1018    def manifestRevision(self):
1019        run.manifest(self.repo.ui, repo=self.repo, rev=self.rev)
1020
1021    def mergeWithRevision(self):
1022        dlg = merge.MergeDialog(self.rev, self.repo, self)
1023        dlg.exec_()
1024
1025    def tagToRevision(self):
1026        dlg = tag.TagDialog(self.repo, rev=str(self.rev), parent=self)
1027        dlg.localTagChanged.connect(self.refresh)
1028        dlg.showMessage.connect(self.showMessage)
1029        dlg.finished.connect(dlg.deleteLater)
1030        dlg.exec_()
1031
1032    def bookmarkRevision(self):
1033        dlg = bookmark.BookmarkDialog(self.repo, str(self.rev), self)
1034        dlg.showMessage.connect(self.showMessage)
1035        dlg.finished.connect(dlg.deleteLater)
1036        dlg.exec_()
1037
1038    def transplantRevision(self):
1039        cmdline = ['transplant', '--repository', self.repo.root, str(self.rev)]
1040        self.runCommand(_('Transplant - TortoiseHg'), cmdline)
1041
1042    def backoutToRevision(self):
1043        dlg = backout.BackoutDialog(self.repo, str(self.rev), self)
1044        dlg.finished.connect(dlg.deleteLater)
1045        dlg.exec_()
1046
1047    def stripRevision(self):
1048        'Strip the selected revision and all descendants'
1049        dlg = thgstrip.StripDialog(self.repo, rev=str(self.rev), parent=self)
1050        dlg.finished.connect(dlg.deleteLater)
1051        dlg.exec_()
1052
1053    def sendToReviewBoard(self):
1054        run.postreview(self.repo.ui, rev=self.repoview.selectedRevisions(),
1055          repo=self.repo)
1056
1057    def emailRevision(self):
1058        run.email(self.repo.ui, rev=self.repoview.selectedRevisions(),
1059                  repo=self.repo)
1060
1061    def archiveRevision(self):
1062        dlg = archive.ArchiveDialog(self.repo.ui, self.repo, self.rev, self)
1063        dlg.makeLogVisible.connect(self.makeLogVisible)
1064        dlg.output.connect(self.output)
1065        dlg.progress.connect(self.progress)
1066        dlg.exec_()
1067
1068    def copyHash(self):
1069        clip = QApplication.clipboard()
1070        clip.setText(binascii.hexlify(self.repo[self.rev].node()))
1071
1072    def rebaseRevision(self):
1073        """Rebase selected revision on top of working directory parent"""
1074        opts = {'source' : self.rev, 'dest': self.repo['.'].rev()}
1075        dlg = rebase.RebaseDialog(self.repo, self, **opts)
1076        dlg.finished.connect(dlg.deleteLater)
1077        dlg.exec_()
1078
1079    def qimportRevision(self):
1080        """QImport revision and all descendents to MQ"""
1081        if 'qparent' in self.repo.tags():
1082            endrev = 'qparent'
1083        else:
1084            endrev = ''
1085        cmdline = ['qimport', '--rev', '%s::%s' % (self.rev, endrev),
1086                   '--repository', self.repo.root]
1087        self.runCommand(_('QImport - TortoiseHg'), cmdline)
1088
1089    def qfinishRevision(self):
1090        """Finish applied patches up to and including selected revision"""
1091        cmdline = ['qfinish', 'qbase::%s' % self.rev,
1092                   '--repository', self.repo.root]
1093        self.runCommand(_('QFinish - TortoiseHg'), cmdline)
1094
1095    def qpopAllRevision(self):
1096        """Unapply all patches"""
1097        self.taskTabsWidget.setCurrentIndex(self.mqTabIndex)
1098        self.mqDemand.get().onPopAll()
1099
1100    def qgotoRevision(self):
1101        """Make REV the top applied patch"""
1102        patchname = self.repo.changectx(self.rev).thgmqpatchname()
1103        cmdline = ['qgoto', str(patchname),  # FIXME force option
1104                   '--repository', self.repo.root]
1105        self.runCommand(_('QGoto - TortoiseHg'), cmdline)
1106
1107    def qrenameRevision(self):
1108        """Rename the selected MQ patch"""
1109        patchname = self.repo.changectx(self.rev).thgmqpatchname()
1110        dlg = qrename.QRenameDialog(self.repo, patchname, self)
1111        dlg.finished.connect(dlg.deleteLater)
1112        dlg.output.connect(self.output)
1113        dlg.makeLogVisible.connect(self.makeLogVisible)
1114        dlg.exec_()
1115
1116    def runCommand(self, title, cmdline):
1117        if self.runner:
1118            InfoMsgBox(_('Unable to start'),
1119                       _('Previous command is still running'))
1120            return
1121        def finished(ret):
1122            self.repo.decrementBusyCount()
1123            self.runner = None
1124        self.runner = cmdui.Runner(title, False, self)
1125        self.runner.output.connect(self.output)
1126        self.runner.progress.connect(self.progress)
1127        self.runner.makeLogVisible.connect(self.makeLogVisible)
1128        self.runner.commandFinished.connect(finished)
1129        self.repo.incrementBusyCount()
1130        self.runner.run(cmdline)