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)

repowidget.py 90.6 KB
Newer Older
1 2 3 4 5 6 7 8
# repowidget.py - TortoiseHg repository widget
#
# Copyright (C) 2007-2010 Logilab. All rights reserved.
# Copyright (C) 2010 Adrian Buehlmann <adrian@cadifra.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

9 10
from __future__ import absolute_import

11
import binascii
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
import os
import shlex  # used by runCustomCommand
import subprocess  # used by runCustomCommand

from .qtcore import (
    QFile,
    QIODevice,
    QItemSelectionModel,
    QMimeData,
    QSettings,
    QTimer,
    QUrl,
    Qt,
    pyqtSignal,
    pyqtSlot,
)
from .qtgui import (
    QAction,
    QApplication,
    QDesktopServices,
    QFileDialog,
    QIcon,
    QKeySequence,
    QMainWindow,
    QMenu,
    QMessageBox,
    QSplitter,
    QTabWidget,
    QVBoxLayout,
    QWidget,
)

from mercurial import (
    error,
    phases,
47
    pycompat,
48
    scmutil,
49 50 51
)
from mercurial.utils import (
    procutil,
52 53 54 55 56 57 58 59 60 61 62 63 64
)

from ..util import (
    hglib,
    paths,
    shlib,
)
from ..util.i18n import _
from . import (
    archive,
    backout,
    bisect,
    bookmark,
65
    close_branch,
66 67 68 69 70 71 72 73 74
    cmdcore,
    cmdui,
    compress,
    graft,
    hgemail,
    infobar,
    matching,
    merge,
    mq,
75
    pick,
76
    phabreview,
77 78 79 80 81 82 83 84 85 86 87 88 89 90
    postreview,
    prune,
    purge,
    qtlib,
    rebase,
    repomodel,
    resolve,
    revdetails,
    settings,
    shelve,
    sign,
    tag,
    thgimport,
    thgstrip,
91
    topics,
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
    update,
    visdiff,
)
from .commit import CommitWidget
from .docklog import ConsoleWidget
from .grep import SearchWidget
from .qtlib import (
    DemandWidget,
    InfoMsgBox,
    QuestionMsgBox,
    WarningMsgBox,
)
from .repofilter import RepoFilterBar
from .repoview import HgRepoView
from .sync import SyncWidget
107 108 109 110 111 112 113

# iswd = working directory
# isrev = the changeset has an integer revision number
# isctx = changectx or workingctx
# fixed = the changeset is considered permanent
# applied = an applied patch
# qgoto = applied patch or qparent
114 115
# isdraftorwd = working directory or changset is draft

116
_ENABLE_MENU_FUNCS = {
117 118 119 120 121
    'isrev'  : lambda ap, wd, tags, ph: not wd,
    'iswd'   : lambda ap, wd, tags, ph: bool(wd),
    'isctx'  : lambda ap, wd, tags, ph: True,
    'fixed'  : lambda ap, wd, tags, ph: not (ap or wd),
    'applied': lambda ap, wd, tags, ph: ap,
kiilerix's avatar
kiilerix committed
122
    'qgoto'  : lambda ap, wd, tags, ph: (b'qparent' in tags) or ap,
123 124
    'istrue' : lambda ap, wd, tags, ph: True,
    'isdraftorwd': lambda ap, wd, tags, ph: bool(ph > 0 or wd),
125 126 127
}


128
class RepoWidget(QWidget):
129

130
    currentTaskTabChanged = pyqtSignal()
131
    showMessageSignal = pyqtSignal(str)
132
    taskTabVisibilityChanged = pyqtSignal(bool)
133
    toolbarVisibilityChanged = pyqtSignal(bool)
134

135
    # TODO: progress can be removed if all actions are run as hg command
136
    progress = pyqtSignal(str, object, str, str, object)
137
    makeLogVisible = pyqtSignal(bool)
138

139 140
    revisionSelected = pyqtSignal(object)

141
    titleChanged = pyqtSignal(str)
142 143
    """Emitted when changed the expected title for the RepoWidget tab"""

144
    busyIconChanged = pyqtSignal()
Steve Borho's avatar
Steve Borho committed
145

146
    repoLinkClicked = pyqtSignal(str)
147 148
    """Emitted when clicked a link to open repository"""

149
    def __init__(self, repoagent, parent=None, bundle=None):
150
        QWidget.__init__(self, parent, acceptDrops=True)
151

152
        self._repoagent = repoagent
153
        self.bundlesource = None  # source URL of incoming bundle [unicode]
154
        self.outgoingMode = False
155
        self._busyIconNames = []
156
        self._namedTabs = {}
157
        self.destroyed.connect(self.repo.thginvalidate)
158

159
        self.currentMessage = ''
160

161
        self.setupUi()
162
        self.createActions()
163
        self.loadSettings()
164
        self._initModel()
165

166 167 168
        self._lastTaskTabVisible = self.isTaskTabVisible()
        self.repotabs_splitter.splitterMoved.connect(self._onSplitterMoved)

169 170 171
        if bundle:
            self.setBundle(bundle)

172 173 174
        self._dialogs = qtlib.DialogKeeper(
            lambda self, dlgmeth, *args: dlgmeth(self, *args), parent=self)

175 176 177
        # listen to change notification after initial settings are loaded
        repoagent.repositoryChanged.connect(self.repositoryChanged)
        repoagent.configChanged.connect(self.configChanged)
178 179

        QTimer.singleShot(0, self._initView)
180

181
    def setupUi(self):
182 183
        self.repotabs_splitter = QSplitter(orientation=Qt.Vertical)
        self.setLayout(QVBoxLayout())
184
        self.layout().setContentsMargins(0, 0, 0, 0)
185
        self.layout().setSpacing(0)
186

187
        # placeholder to shift repoview while infobar is overlaid
188
        self._repoviewFrame = infobar.InfoBarPlaceholder(self._repoagent, self)
189 190
        self._repoviewFrame.linkActivated.connect(self._openLink)

191
        self.filterbar = RepoFilterBar(self._repoagent, self)
192
        self.layout().addWidget(self.filterbar)
193

194
        self.filterbar.branchChanged.connect(self.setBranch)
195
        self.filterbar.showHiddenChanged.connect(self.setShowHidden)
196
        self.filterbar.showGraftSourceChanged.connect(self.setShowGraftSource)
197
        self.filterbar.setRevisionSet.connect(self.setRevisionSet)
198
        self.filterbar.filterToggled.connect(self.filterToggled)
199
        self.filterbar.visibilityChanged.connect(self.toolbarVisibilityChanged)
200
        self.filterbar.hide()
201

202
        self.layout().addWidget(self.repotabs_splitter)
203

204
        cs = ('Workbench', _('Workbench Log Columns'))
205 206
        self.repoview = view = HgRepoView(self._repoagent, 'repoWidget', cs,
                                          self)
207
        view.clicked.connect(self._clearInfoMessage)
208
        view.revisionSelected.connect(self.onRevisionSelected)
209
        view.revisionActivated.connect(self.onRevisionActivated)
210
        view.showMessage.connect(self.showMessage)
211
        view.menuRequested.connect(self.viewMenuRequest)
212
        self._repoviewFrame.setView(view)
213

214
        self.repotabs_splitter.addWidget(self._repoviewFrame)
215
        self.repotabs_splitter.setCollapsible(0, True)
216
        self.repotabs_splitter.setStretchFactor(0, 1)
217

218
        self.taskTabsWidget = tt = QTabWidget()
219
        self.repotabs_splitter.addWidget(self.taskTabsWidget)
220
        self.repotabs_splitter.setStretchFactor(1, 1)
221 222
        tt.setDocumentMode(True)
        self.updateTaskTabs()
223
        tt.currentChanged.connect(self.currentTaskTabChanged)
224

225 226
        w = revdetails.RevDetailsWidget(self._repoagent, self)
        self.revDetailsWidget = w
227
        self.revDetailsWidget.filelisttbar.setStyleSheet(qtlib.tbstylesheet)
228
        w.linkActivated.connect(self._openLink)
229
        w.revisionSelected.connect(self.repoview.goto)
230
        w.grepRequested.connect(self.grep)
231
        w.showMessage.connect(self.showMessage)
232
        w.revsetFilterRequested.connect(self.setFilter)
233 234
        w.runCustomCommandRequested.connect(
            self.handleRunCustomCommandRequest)
235
        idx = tt.addTab(w, qtlib.geticon('hg-log'), '')
236
        self._namedTabs['log'] = idx
237
        tt.setTabToolTip(idx, _("Revision details", "tab tooltip"))
238

239
        self.commitDemand = w = DemandWidget('createCommitWidget', self)
240
        idx = tt.addTab(w, qtlib.geticon('hg-commit'), '')
241
        self._namedTabs['commit'] = idx
242 243 244
        tt.setTabToolTip(idx, _("Commit", "tab tooltip"))

        self.grepDemand = w = DemandWidget('createGrepWidget', self)
245
        idx = tt.addTab(w, qtlib.geticon('hg-grep'), '')
246
        self._namedTabs['grep'] = idx
247 248
        tt.setTabToolTip(idx, _("Search", "tab tooltip"))

249 250
        w = ConsoleWidget(self._repoagent, self)
        self.consoleWidget = w
251
        w.closeRequested.connect(self.switchToPreferredTaskTab)
252
        idx = tt.addTab(w, qtlib.geticon('thg-console'), '')
253
        self._namedTabs['console'] = idx
254 255
        tt.setTabToolTip(idx, _("Console log", "tab tooltip"))

256
        self.syncDemand = w = DemandWidget('createSyncWidget', self)
257
        idx = tt.addTab(w, qtlib.geticon('thg-sync'), '')
258
        self._namedTabs['sync'] = idx
259
        tt.setTabToolTip(idx, _("Synchronize", "tab tooltip"))
260

261 262 263
    @pyqtSlot()
    def _initView(self):
        self._updateRepoViewForModel()
264 265 266 267 268 269
        # restore column widths when model is initially loaded.  For some
        # reason, this needs to be deferred after updating the view.  Otherwise
        # repoview.HgRepoView.resizeEvent() fires as the vertical scrollbar is
        # added, which causes the last column to grow by the scrollbar width on
        # each restart (and steal from the description width).
        QTimer.singleShot(0, self.repoview.resizeColumns)
270

271
        # select the widget chosen by the user
272
        name = self._repoagent.configString('tortoisehg', 'defaultwidget')
273 274 275 276
        if name:
            name = {'revdetails': 'log', 'search': 'grep'}.get(name, name)
            self.taskTabsWidget.setCurrentIndex(self._namedTabs.get(name, 0))

277
    def currentTaskTabName(self):
278
        indexmap = dict((idx, name)
279
                        for name, idx in self._namedTabs.items())
280 281
        return indexmap.get(self.taskTabsWidget.currentIndex())

282
    @pyqtSlot(str)
283
    def switchToNamedTaskTab(self, tabname):
284
        tabname = str(tabname)
285 286
        if tabname in self._namedTabs:
            idx = self._namedTabs[tabname]
287 288 289
            # refresh status even if current widget is already a 'commit'
            if (tabname == 'commit'
                and self.taskTabsWidget.currentIndex() == idx):
290
                self._refreshCommitTabIfNeeded()
291
            self.taskTabsWidget.setCurrentIndex(idx)
292

293
            # restore default splitter position if task tab is invisible
294 295 296 297 298 299 300 301 302 303 304 305
            self.setTaskTabVisible(True)

    def isTaskTabVisible(self):
        return self.repotabs_splitter.sizes()[1] > 0

    def setTaskTabVisible(self, visible):
        if visible == self.isTaskTabVisible():
            return
        if visible:
            self.repotabs_splitter.setSizes([1, 1])
        else:
            self.repotabs_splitter.setSizes([1, 0])
306 307 308 309 310 311 312 313 314 315 316 317
        self._updateLastTaskTabState(visible)

    @pyqtSlot()
    def _onSplitterMoved(self):
        visible = self.isTaskTabVisible()
        if self._lastTaskTabVisible == visible:
            return
        self._updateLastTaskTabState(visible)

    def _updateLastTaskTabState(self, visible):
        self._lastTaskTabVisible = visible
        self.taskTabVisibilityChanged.emit(visible)
318

319 320 321 322
    @property
    def repo(self):
        return self._repoagent.rawRepo()

323
    def repoRootPath(self):
324
        return self._repoagent.rootPath()
325

326 327 328
    def repoDisplayName(self):
        return self._repoagent.displayName()

329 330
    def title(self):
        """Returns the expected title for this widget [unicode]"""
331
        name = self._repoagent.shortName()
332
        if self._repoagent.overlayUrl():
333
            return _('%s <incoming>') % name
334
        elif self.repomodel.branch():
335
            return u'%s [%s]' % (name, self.repomodel.branch())
336
        else:
337
            return name
338

339 340 341 342 343 344
    def busyIcon(self):
        if self._busyIconNames:
            return qtlib.geticon(self._busyIconNames[-1])
        else:
            return QIcon()

345 346 347
    def filterBar(self):
        return self.filterbar

348 349 350 351 352
    def filterBarVisible(self):
        return self.filterbar.isVisible()

    @pyqtSlot(bool)
    def toggleFilterBar(self, checked):
353
        """Toggle display repowidget filter bar"""
354 355
        if self.filterbar.isVisibleTo(self) == checked:
            return
356
        self.filterbar.setVisible(checked)
357 358
        if checked:
            self.filterbar.setFocus()
359

360 361 362 363 364 365
    def _openRepoLink(self, upath):
        path = hglib.fromunicode(upath)
        if not os.path.isabs(path):
            path = self.repo.wjoin(path)
        self.repoLinkClicked.emit(hglib.tounicode(path))

366
    @pyqtSlot(str)
367
    def _openLink(self, link):
368
        link = pycompat.unicode(link)
369
        handlers = {'cset': self.goto,
370
                    'log': lambda a: self.makeLogVisible.emit(True),
371
                    'repo': self._openRepoLink,
372
                    'shelve' : self.shelve}
373
        if ':' in link:
374
            scheme, param = link.split(':', 1)
375 376 377
            hdr = handlers.get(scheme)
            if hdr:
                return hdr(param)
378 379 380 381
        if os.path.isabs(link):
            qtlib.openlocalurl(link)
        else:
            QDesktopServices.openUrl(QUrl(link))
382

383
    def setInfoBar(self, cls, *args, **kwargs):
384
        return self._repoviewFrame.setInfoBar(cls, *args, **kwargs)
385

386
    def clearInfoBar(self, priority=None):
387
        return self._repoviewFrame.clearInfoBar(priority)
388

389
    def createCommitWidget(self):
390
        pats, opts = {}, {}
391
        cw = CommitWidget(self._repoagent, pats, opts, self, rev=self.rev)
392
        cw.buttonHBox.addWidget(cw.commitSetupButton())
393
        cw.loadSettings(QSettings(), 'Workbench')
394

395
        cw.progress.connect(self.progress)
396
        cw.linkActivated.connect(self._openLink)
397
        cw.showMessage.connect(self.showMessage)
398
        cw.grepRequested.connect(self.grep)
399 400
        cw.runCustomCommandRequested.connect(
            self.handleRunCustomCommandRequest)
401
        QTimer.singleShot(0, self._initCommitWidgetLate)
402
        return cw
403

404 405 406 407 408 409 410 411 412
    @pyqtSlot()
    def _initCommitWidgetLate(self):
        cw = self.commitDemand.get()
        cw.reload()
        # auto-refresh should be enabled after initial reload(); otherwise
        # refreshWctx() can be doubled
        self.taskTabsWidget.currentChanged.connect(
            self._refreshCommitTabIfNeeded)

413
    def createSyncWidget(self):
414
        sw = SyncWidget(self._repoagent, self)
415
        sw.newCommand.connect(self._handleNewSyncCommand)
416
        sw.outgoingNodes.connect(self.setOutgoingNodes)
417
        sw.showMessage.connect(self.showMessage)
418
        sw.showMessage.connect(self._repoviewFrame.showMessage)
419
        sw.incomingBundle.connect(self.setBundle)
420
        sw.pullCompleted.connect(self.onPullCompleted)
421
        sw.pushCompleted.connect(self.clearRevisionSet)
422
        sw.refreshTargets(self.rev)
423
        sw.switchToRequest.connect(self.switchToNamedTaskTab)
424
        return sw
425

426 427 428
    @pyqtSlot(cmdcore.CmdSession)
    def _handleNewSyncCommand(self, sess):
        self._handleNewCommand(sess)
429 430 431 432
        if sess.isFinished():
            return
        sess.commandFinished.connect(self._onSyncCommandFinished)
        self._setBusyIcon('thg-sync')
433

434 435 436 437 438
    @pyqtSlot()
    def _onSyncCommandFinished(self):
        self._clearBusyIcon('thg-sync')

    def _setBusyIcon(self, iconname):
439
        self._busyIconNames.append(iconname)
440
        self.busyIconChanged.emit()
Steve Borho's avatar
Steve Borho committed
441

442
    def _clearBusyIcon(self, iconname):
443 444
        if iconname in self._busyIconNames:
            self._busyIconNames.remove(iconname)
445
        self.busyIconChanged.emit()
Steve Borho's avatar
Steve Borho committed
446

447
    @pyqtSlot(str)
448
    def setFilter(self, filter):
449
        self.filterbar.setQuery(filter)
450
        self.filterbar.setVisible(True)
451
        self.filterbar.runQuery()
452

453
    @pyqtSlot(str, str)
454
    def setBundle(self, bfile, bsource=None):
455
        if self._repoagent.overlayUrl():
456
            self.clearBundle()
457
        self.bundlesource = bsource and pycompat.unicode(bsource) or None
458
        oldlen = len(self.repo)
459 460
        # no "bundle:<bfile>" because bfile may contain "+" separator
        self._repoagent.setOverlay(bfile)
461 462
        self.filterbar.setQuery('bundle()')
        self.filterbar.runQuery()
463 464 465
        self.titleChanged.emit(self.title())
        newlen = len(self.repo)

466
        w = self.setInfoBar(infobar.ConfirmInfoBar,
467
            _('Found %d incoming changesets') % (newlen - oldlen))
468
        assert w
469
        w.acceptButton.setText(_('Pull'))
470 471
        w.acceptButton.setToolTip(_('Pull incoming changesets into '
                                    'your repository'))
472
        w.rejectButton.setText(_('Cancel'))
473
        w.rejectButton.setToolTip(_('Reject incoming changesets'))
474
        w.accepted.connect(self.acceptBundle)
475
        w.rejected.connect(self.clearBundle)
476

477
    @pyqtSlot()
478
    def clearBundle(self):
479
        self.clearRevisionSet()
480
        self.bundlesource = None
481
        self._repoagent.clearOverlay()
482
        self.titleChanged.emit(self.title())
483

484
    @pyqtSlot()
485
    def onPullCompleted(self):
486
        if self._repoagent.overlayUrl():
487
            self.clearBundle()
488

489
    @pyqtSlot()
490
    def acceptBundle(self):
491
        bundle = self._repoagent.overlayUrl()
492
        if bundle:
493
            w = self.syncDemand.get()
494
            w.pullBundle(bundle, None, self.bundlesource)
495

496
    @pyqtSlot()
497
    def pullBundleToRev(self):
498
        bundle = self._repoagent.overlayUrl()
499
        if bundle:
500
            # manually remove infobar to work around unwanted clearBundle
501
            # during pull operation (issue #2596)
502
            self._repoviewFrame.discardInfoBar()
503

504
            w = self.syncDemand.get()
505
            w.pullBundle(bundle, self.repo[self.rev].hex(), self.bundlesource)
506

507
    @pyqtSlot()
508
    def clearRevisionSet(self):
509
        self.filterbar.setQuery('')
510
        self.setRevisionSet('')
511

512 513 514
    def setRevisionSet(self, revspec):
        self.repomodel.setRevset(revspec)
        if not revspec:
515
            self.outgoingMode = False
516 517 518

    @pyqtSlot(bool)
    def filterToggled(self, checked):
519
        self.repomodel.setFilterByRevset(checked)
520

521
    def setOutgoingNodes(self, nodes):
522
        self.filterbar.setQuery('outgoing()')
523
        revs = [self.repo[n].rev() for n in nodes]
524
        self.setRevisionSet(hglib.compactrevs(revs))
525
        self.outgoingMode = True
526 527
        numnodes = len(nodes)
        numoutgoing = numnodes
528

529 530 531 532 533 534
        if self.syncDemand.get().isTargetSelected():
            # Outgoing preview is already filtered by target selection
            defaultpush = None
        else:
            # Read the tortoisehg.defaultpush setting to determine what to push
            # by default, and set the button label and action accordingly
535 536
            defaultpush = self._repoagent.configString(
                'tortoisehg', 'defaultpush')
537 538 539
        rev = None
        branch = None
        pushall = False
540 541
        # note that we assume that none of the revisions
        # on the nodes/revs lists is secret
542
        if defaultpush == 'branch':
543
            branch = self.repo[b'.'].branch()
544 545
            ubranch = hglib.tounicode(branch)
            # Get the list of revs that will be actually pushed
546
            outgoingrevs = self.repo.revs(b'%ld and branch(.)', revs)
547
            numoutgoing = len(outgoingrevs)
548
        elif defaultpush == 'revision':
549
            rev = self.repo[b'.'].rev()
550 551
            # Get the list of revs that will be actually pushed
            # excluding (potentially) the current rev
552
            outgoingrevs = self.repo.revs(b'%ld and ::.', revs)
553 554 555 556
            numoutgoing = len(outgoingrevs)
            maxrev = rev
            if numoutgoing > 0:
                maxrev = max(outgoingrevs)
557 558
        else:
            pushall = True
559

560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
        # Set the default acceptbuttontext
        # Note that the pushall case uses the default accept button text
        if branch is not None:
            acceptbuttontext = _('Push current branch (%s)') % ubranch
        elif rev is not None:
            if maxrev == rev:
                acceptbuttontext = _('Push up to current revision (#%d)') % rev
            else:
                acceptbuttontext = _('Push up to revision #%d') % maxrev
        else:
            acceptbuttontext = _('Push all')

        if numnodes == 0:
            msg = _('no outgoing changesets')
        elif numoutgoing == 0:
            if branch:
                msg = _('no outgoing changesets in current branch (%s) '
                    '/ %d in total') % (ubranch, numnodes)
            elif rev is not None:
                if maxrev == rev:
                    msg = _('no outgoing changesets up to current revision '
                            '(#%d) / %d in total') % (rev, numnodes)
                else:
                    msg = _('no outgoing changesets up to revision #%d '
                            '/ %d in total') % (maxrev, numnodes)
        elif numoutgoing == numnodes:
            # This case includes 'Push all' among others
            msg = _('%d outgoing changesets') % numoutgoing
        elif branch:
            msg = _('%d outgoing changesets in current branch (%s) '
                    '/ %d in total') % (numoutgoing, ubranch, numnodes)
        elif rev:
            if maxrev == rev:
                msg = _('%d outgoing changesets up to current revision (#%d) '
                        '/ %d in total') % (numoutgoing, rev, numnodes)
            else:
                msg = _('%d outgoing changesets up to revision #%d '
                        '/ %d in total') % (numoutgoing, maxrev, numnodes)
        else:
            # This should never happen but we leave this else clause
            # in case there is a flaw in the logic above (e.g. due to
            # a future change in the code)
            msg = _('%d outgoing changesets') % numoutgoing

604
        w = self.setInfoBar(infobar.ConfirmInfoBar, msg.strip())
605 606 607 608 609
        assert w

        if numoutgoing == 0:
            acceptbuttontext = _('Nothing to push')
            w.acceptButton.setEnabled(False)
610
        w.acceptButton.setText(acceptbuttontext)
611 612
        w.accepted.connect(lambda: self.push(False,
            rev=rev, branch=branch, pushall=pushall))  # TODO: to the same URL
613 614
        w.rejected.connect(self.clearRevisionSet)

615 616
    def createGrepWidget(self):
        upats = {}
617
        gw = SearchWidget(self._repoagent, upats, self)
618
        gw.setRevision(self.repoview.current_rev)
619
        gw.showMessage.connect(self.showMessage)
620
        gw.progress.connect(self.progress)
621
        gw.revisionSelected.connect(self.goto)
622 623
        return gw

624 625 626 627 628
    @property
    def rev(self):
        """Returns the current active revision"""
        return self.repoview.current_rev

629 630 631 632
    def gotoRev(self, revspec):
        """Select and scroll to the specified revision"""
        try:
            # try instant look up
633
            if scmutil.isrevsymbol(self.repo, hglib.fromunicode(revspec)):
634 635 636 637 638 639 640 641 642 643
                self.repoview.goto(revspec)
                return
        except error.LookupError:
            pass  # ambiguous node

        cmdline = hglib.buildcmdargs('log', rev=revspec, template='{rev}\n')
        sess = self._runCommand(cmdline)
        sess.setCaptureOutput(True)
        sess.commandFinished.connect(self._onGotoRevQueryFinished)

644
    @pyqtSlot(int)
645 646 647 648 649 650 651 652
    def _onGotoRevQueryFinished(self, ret):
        sess = self.sender()
        if ret != 0:
            # warnings (e.g. "ambiguous identifier") aren't captured by
            # showOutput(), so we have to set the error explicitly.
            self.setInfoBar(infobar.CommandErrorInfoBar,
                            sess.errorString() or sess.warningString())
            return
653
        output = bytes(sess.readAll())
654 655 656 657 658 659 660 661
        if not output:
            # TODO: maybe this should be a warning bar since there would be no
            # information in log window.
            self.setInfoBar(infobar.CommandErrorInfoBar, _('No revision found'))
            return
        rev = int(output.splitlines()[-1])  # pick last rev as "hg update" does
        self.repoview.goto(rev)

662
    def showMessage(self, msg):
663 664 665 666
        self.currentMessage = msg
        if self.isVisible():
            self.showMessageSignal.emit(msg)

667
    def keyPressEvent(self, event):
668
        if self._repoviewFrame.activeInfoBar() and event.key() == Qt.Key_Escape:
669
            self.clearInfoBar(infobar.INFO)
670 671 672
        else:
            QWidget.keyPressEvent(self, event)

673
    def showEvent(self, event):
674
        QWidget.showEvent(self, event)
675
        self.showMessageSignal.emit(self.currentMessage)
676 677 678 679
        if not event.spontaneous():
            # RepoWidget must be the main widget in any window, so grab focus
            # when it gets visible at start-up or by switching tabs.
            self.repoview.setFocus()
680 681

    def createActions(self):
682
        self._mqActions = None
683
        if b'mq' in self.repo.extensions():
684 685
            self._mqActions = mq.PatchQueueActions(self)
            self._mqActions.setRepoAgent(self._repoagent)
686
            self.generateUnappliedPatchMenu()
687

688 689 690
        self.generateSingleMenu()
        self.generatePairMenu()
        self.generateMultipleSelectionMenu()
691
        self.generateBundleMenu()
692
        self.generateOutgoingMenu()
693

694 695 696 697 698 699 700
    def detectPatches(self, paths):
        filepaths = []
        for p in paths:
            if not os.path.isfile(p):
                continue
            try:
                pf = open(p, 'rb')
701
                earlybytes = pf.read(4096)
702
                if b'\0' in earlybytes:
703 704
                    continue
                pf.seek(0)
705 706 707
                with hglib.extractpatch(self.repo.ui, pf) as data:
                    if data.get('filename'):
                        filepaths.append(p)
708
            except EnvironmentError:
709 710 711
                pass
        return filepaths

712
    def dragEnterEvent(self, event):
713
        paths = [pycompat.unicode(u.toLocalFile()) for u in event.mimeData().urls()]
714
        if self.detectPatches(paths):
715 716 717 718
            event.setDropAction(Qt.CopyAction)
            event.accept()

    def dropEvent(self, event):
719
        paths = [pycompat.unicode(u.toLocalFile()) for u in event.mimeData().urls()]
720 721 722 723 724 725
        patches = self.detectPatches(paths)
        if not patches:
            return
        event.setDropAction(Qt.CopyAction)
        event.accept()
        self.thgimport(patches)
726

727 728
    ## Begin Workbench event forwards

729 730 731 732 733 734
    def back(self):
        self.repoview.back()

    def forward(self):
        self.repoview.forward()

735
    def bisect(self):
736 737 738 739
        self._dialogs.open(RepoWidget._createBisectDialog)

    def _createBisectDialog(self):
        dlg = bisect.BisectDialog(self._repoagent, self)
740
        dlg.newCandidate.connect(self.gotoParent)
741
        return dlg
742

743
    def resolve(self):
744
        dlg = resolve.ResolveDialog(self._repoagent, self)
Steve Borho's avatar
Steve Borho committed
745 746
        dlg.exec_()

747
    def thgimport(self, paths=None):
748
        dlg = thgimport.ImportDialog(self._repoagent, self)
749 750
        if paths:
            dlg.setfilepaths(paths)
751 752
        if dlg.exec_() == 0:
            self.gotoTip()
753

754 755 756 757
    def unbundle(self):
         w = self.syncDemand.get()
         w.unbundle()

758
    def shelve(self, arg=None):
759 760 761
        self._dialogs.open(RepoWidget._createShelveDialog)

    def _createShelveDialog(self):
762
        dlg = shelve.ShelveDialog(self._repoagent)