# repowidget.py - TortoiseHg repository widget # # Copyright (C) 2007-2010 Logilab. All rights reserved. # Copyright (C) 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from __future__ import absolute_import import binascii 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, pycompat, scmutil, ) from mercurial.utils import ( procutil, ) from ..util import ( hglib, paths, shlib, ) from ..util.i18n import _ from . import ( archive, backout, bisect, bookmark, close_branch, cmdcore, cmdui, compress, graft, hgemail, infobar, matching, merge, mq, pick, phabreview, postreview, prune, purge, qtlib, rebase, repomodel, resolve, revdetails, settings, shelve, sign, tag, thgimport, thgstrip, topics, 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 # 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 # isdraftorwd = working directory or changset is draft _ENABLE_MENU_FUNCS = { '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, 'qgoto' : lambda ap, wd, tags, ph: (b'qparent' in tags) or ap, 'istrue' : lambda ap, wd, tags, ph: True, 'isdraftorwd': lambda ap, wd, tags, ph: bool(ph > 0 or wd), } class RepoWidget(QWidget): currentTaskTabChanged = pyqtSignal() showMessageSignal = pyqtSignal(str) taskTabVisibilityChanged = pyqtSignal(bool) toolbarVisibilityChanged = pyqtSignal(bool) # TODO: progress can be removed if all actions are run as hg command progress = pyqtSignal(str, object, str, str, object) makeLogVisible = pyqtSignal(bool) revisionSelected = pyqtSignal(object) titleChanged = pyqtSignal(str) """Emitted when changed the expected title for the RepoWidget tab""" busyIconChanged = pyqtSignal() repoLinkClicked = pyqtSignal(str) """Emitted when clicked a link to open repository""" def __init__(self, repoagent, parent=None, bundle=None): QWidget.__init__(self, parent, acceptDrops=True) self._repoagent = repoagent self.bundlesource = None # source URL of incoming bundle [unicode] self.outgoingMode = False self._busyIconNames = [] self._namedTabs = {} self.destroyed.connect(self.repo.thginvalidate) self.currentMessage = '' self.setupUi() self.createActions() self.loadSettings() self._initModel() self._lastTaskTabVisible = self.isTaskTabVisible() self.repotabs_splitter.splitterMoved.connect(self._onSplitterMoved) if bundle: self.setBundle(bundle) self._dialogs = qtlib.DialogKeeper( lambda self, dlgmeth, *args: dlgmeth(self, *args), parent=self) # listen to change notification after initial settings are loaded repoagent.repositoryChanged.connect(self.repositoryChanged) repoagent.configChanged.connect(self.configChanged) QTimer.singleShot(0, self._initView) def setupUi(self): self.repotabs_splitter = QSplitter(orientation=Qt.Vertical) self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) # placeholder to shift repoview while infobar is overlaid self._repoviewFrame = infobar.InfoBarPlaceholder(self._repoagent, self) self._repoviewFrame.linkActivated.connect(self._openLink) self.filterbar = RepoFilterBar(self._repoagent, self) self.layout().addWidget(self.filterbar) self.filterbar.branchChanged.connect(self.setBranch) self.filterbar.showHiddenChanged.connect(self.setShowHidden) self.filterbar.showGraftSourceChanged.connect(self.setShowGraftSource) self.filterbar.setRevisionSet.connect(self.setRevisionSet) self.filterbar.filterToggled.connect(self.filterToggled) self.filterbar.visibilityChanged.connect(self.toolbarVisibilityChanged) self.filterbar.hide() self.layout().addWidget(self.repotabs_splitter) cs = ('Workbench', _('Workbench Log Columns')) self.repoview = view = HgRepoView(self._repoagent, 'repoWidget', cs, self) view.clicked.connect(self._clearInfoMessage) view.revisionSelected.connect(self.onRevisionSelected) view.revisionActivated.connect(self.onRevisionActivated) view.showMessage.connect(self.showMessage) view.menuRequested.connect(self.viewMenuRequest) self._repoviewFrame.setView(view) self.repotabs_splitter.addWidget(self._repoviewFrame) self.repotabs_splitter.setCollapsible(0, True) self.repotabs_splitter.setStretchFactor(0, 1) self.taskTabsWidget = tt = QTabWidget() self.repotabs_splitter.addWidget(self.taskTabsWidget) self.repotabs_splitter.setStretchFactor(1, 1) tt.setDocumentMode(True) self.updateTaskTabs() tt.currentChanged.connect(self.currentTaskTabChanged) w = revdetails.RevDetailsWidget(self._repoagent, self) self.revDetailsWidget = w self.revDetailsWidget.filelisttbar.setStyleSheet(qtlib.tbstylesheet) w.linkActivated.connect(self._openLink) w.revisionSelected.connect(self.repoview.goto) w.grepRequested.connect(self.grep) w.showMessage.connect(self.showMessage) w.revsetFilterRequested.connect(self.setFilter) w.runCustomCommandRequested.connect( self.handleRunCustomCommandRequest) idx = tt.addTab(w, qtlib.geticon('hg-log'), '') self._namedTabs['log'] = idx tt.setTabToolTip(idx, _("Revision details", "tab tooltip")) self.commitDemand = w = DemandWidget('createCommitWidget', self) idx = tt.addTab(w, qtlib.geticon('hg-commit'), '') self._namedTabs['commit'] = idx tt.setTabToolTip(idx, _("Commit", "tab tooltip")) self.grepDemand = w = DemandWidget('createGrepWidget', self) idx = tt.addTab(w, qtlib.geticon('hg-grep'), '') self._namedTabs['grep'] = idx tt.setTabToolTip(idx, _("Search", "tab tooltip")) w = ConsoleWidget(self._repoagent, self) self.consoleWidget = w w.closeRequested.connect(self.switchToPreferredTaskTab) idx = tt.addTab(w, qtlib.geticon('thg-console'), '') self._namedTabs['console'] = idx tt.setTabToolTip(idx, _("Console log", "tab tooltip")) self.syncDemand = w = DemandWidget('createSyncWidget', self) idx = tt.addTab(w, qtlib.geticon('thg-sync'), '') self._namedTabs['sync'] = idx tt.setTabToolTip(idx, _("Synchronize", "tab tooltip")) @pyqtSlot() def _initView(self): self._updateRepoViewForModel() # 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) # select the widget chosen by the user name = self._repoagent.configString('tortoisehg', 'defaultwidget') if name: name = {'revdetails': 'log', 'search': 'grep'}.get(name, name) self.taskTabsWidget.setCurrentIndex(self._namedTabs.get(name, 0)) def currentTaskTabName(self): indexmap = dict((idx, name) for name, idx in self._namedTabs.items()) return indexmap.get(self.taskTabsWidget.currentIndex()) @pyqtSlot(str) def switchToNamedTaskTab(self, tabname): tabname = str(tabname) if tabname in self._namedTabs: idx = self._namedTabs[tabname] # refresh status even if current widget is already a 'commit' if (tabname == 'commit' and self.taskTabsWidget.currentIndex() == idx): self._refreshCommitTabIfNeeded() self.taskTabsWidget.setCurrentIndex(idx) # restore default splitter position if task tab is invisible 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]) 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) @property def repo(self): return self._repoagent.rawRepo() def repoRootPath(self): return self._repoagent.rootPath() def repoDisplayName(self): return self._repoagent.displayName() def title(self): """Returns the expected title for this widget [unicode]""" name = self._repoagent.shortName() if self._repoagent.overlayUrl(): return _('%s ') % name elif self.repomodel.branch(): return u'%s [%s]' % (name, self.repomodel.branch()) else: return name def busyIcon(self): if self._busyIconNames: return qtlib.geticon(self._busyIconNames[-1]) else: return QIcon() def filterBar(self): return self.filterbar def filterBarVisible(self): return self.filterbar.isVisible() @pyqtSlot(bool) def toggleFilterBar(self, checked): """Toggle display repowidget filter bar""" if self.filterbar.isVisibleTo(self) == checked: return self.filterbar.setVisible(checked) if checked: self.filterbar.setFocus() 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)) @pyqtSlot(str) def _openLink(self, link): link = pycompat.unicode(link) handlers = {'cset': self.goto, 'log': lambda a: self.makeLogVisible.emit(True), 'repo': self._openRepoLink, 'shelve' : self.shelve} if ':' in link: scheme, param = link.split(':', 1) hdr = handlers.get(scheme) if hdr: return hdr(param) if os.path.isabs(link): qtlib.openlocalurl(link) else: QDesktopServices.openUrl(QUrl(link)) def setInfoBar(self, cls, *args, **kwargs): return self._repoviewFrame.setInfoBar(cls, *args, **kwargs) def clearInfoBar(self, priority=None): return self._repoviewFrame.clearInfoBar(priority) def createCommitWidget(self): pats, opts = {}, {} cw = CommitWidget(self._repoagent, pats, opts, self, rev=self.rev) cw.buttonHBox.addWidget(cw.commitSetupButton()) cw.loadSettings(QSettings(), 'Workbench') cw.progress.connect(self.progress) cw.linkActivated.connect(self._openLink) cw.showMessage.connect(self.showMessage) cw.grepRequested.connect(self.grep) cw.runCustomCommandRequested.connect( self.handleRunCustomCommandRequest) QTimer.singleShot(0, self._initCommitWidgetLate) return cw @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) def createSyncWidget(self): sw = SyncWidget(self._repoagent, self) sw.newCommand.connect(self._handleNewSyncCommand) sw.outgoingNodes.connect(self.setOutgoingNodes) sw.showMessage.connect(self.showMessage) sw.showMessage.connect(self._repoviewFrame.showMessage) sw.incomingBundle.connect(self.setBundle) sw.pullCompleted.connect(self.onPullCompleted) sw.pushCompleted.connect(self.clearRevisionSet) sw.refreshTargets(self.rev) sw.switchToRequest.connect(self.switchToNamedTaskTab) return sw @pyqtSlot(cmdcore.CmdSession) def _handleNewSyncCommand(self, sess): self._handleNewCommand(sess) if sess.isFinished(): return sess.commandFinished.connect(self._onSyncCommandFinished) self._setBusyIcon('thg-sync') @pyqtSlot() def _onSyncCommandFinished(self): self._clearBusyIcon('thg-sync') def _setBusyIcon(self, iconname): self._busyIconNames.append(iconname) self.busyIconChanged.emit() def _clearBusyIcon(self, iconname): if iconname in self._busyIconNames: self._busyIconNames.remove(iconname) self.busyIconChanged.emit() @pyqtSlot(str) def setFilter(self, filter): self.filterbar.setQuery(filter) self.filterbar.setVisible(True) self.filterbar.runQuery() @pyqtSlot(str, str) def setBundle(self, bfile, bsource=None): if self._repoagent.overlayUrl(): self.clearBundle() self.bundlesource = bsource and pycompat.unicode(bsource) or None oldlen = len(self.repo) # no "bundle:" because bfile may contain "+" separator self._repoagent.setOverlay(bfile) self.filterbar.setQuery('bundle()') self.filterbar.runQuery() self.titleChanged.emit(self.title()) newlen = len(self.repo) w = self.setInfoBar(infobar.ConfirmInfoBar, _('Found %d incoming changesets') % (newlen - oldlen)) assert w w.acceptButton.setText(_('Pull')) w.acceptButton.setToolTip(_('Pull incoming changesets into ' 'your repository')) w.rejectButton.setText(_('Cancel')) w.rejectButton.setToolTip(_('Reject incoming changesets')) w.accepted.connect(self.acceptBundle) w.rejected.connect(self.clearBundle) @pyqtSlot() def clearBundle(self): self.clearRevisionSet() self.bundlesource = None self._repoagent.clearOverlay() self.titleChanged.emit(self.title()) @pyqtSlot() def onPullCompleted(self): if self._repoagent.overlayUrl(): self.clearBundle() @pyqtSlot() def acceptBundle(self): bundle = self._repoagent.overlayUrl() if bundle: w = self.syncDemand.get() w.pullBundle(bundle, None, self.bundlesource) @pyqtSlot() def pullBundleToRev(self): bundle = self._repoagent.overlayUrl() if bundle: # manually remove infobar to work around unwanted clearBundle # during pull operation (issue #2596) self._repoviewFrame.discardInfoBar() w = self.syncDemand.get() w.pullBundle(bundle, self.repo[self.rev].hex(), self.bundlesource) @pyqtSlot() def clearRevisionSet(self): self.filterbar.setQuery('') self.setRevisionSet('') def setRevisionSet(self, revspec): self.repomodel.setRevset(revspec) if not revspec: self.outgoingMode = False @pyqtSlot(bool) def filterToggled(self, checked): self.repomodel.setFilterByRevset(checked) def setOutgoingNodes(self, nodes): self.filterbar.setQuery('outgoing()') revs = [self.repo[n].rev() for n in nodes] self.setRevisionSet(hglib.compactrevs(revs)) self.outgoingMode = True numnodes = len(nodes) numoutgoing = numnodes 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 defaultpush = self._repoagent.configString( 'tortoisehg', 'defaultpush') rev = None branch = None pushall = False # note that we assume that none of the revisions # on the nodes/revs lists is secret if defaultpush == 'branch': branch = self.repo[b'.'].branch() ubranch = hglib.tounicode(branch) # Get the list of revs that will be actually pushed outgoingrevs = self.repo.revs(b'%ld and branch(.)', revs) numoutgoing = len(outgoingrevs) elif defaultpush == 'revision': rev = self.repo[b'.'].rev() # Get the list of revs that will be actually pushed # excluding (potentially) the current rev outgoingrevs = self.repo.revs(b'%ld and ::.', revs) numoutgoing = len(outgoingrevs) maxrev = rev if numoutgoing > 0: maxrev = max(outgoingrevs) else: pushall = True # 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 w = self.setInfoBar(infobar.ConfirmInfoBar, msg.strip()) assert w if numoutgoing == 0: acceptbuttontext = _('Nothing to push') w.acceptButton.setEnabled(False) w.acceptButton.setText(acceptbuttontext) w.accepted.connect(lambda: self.push(False, rev=rev, branch=branch, pushall=pushall)) # TODO: to the same URL w.rejected.connect(self.clearRevisionSet) def createGrepWidget(self): upats = {} gw = SearchWidget(self._repoagent, upats, self) gw.setRevision(self.repoview.current_rev) gw.showMessage.connect(self.showMessage) gw.progress.connect(self.progress) gw.revisionSelected.connect(self.goto) return gw @property def rev(self): """Returns the current active revision""" return self.repoview.current_rev def gotoRev(self, revspec): """Select and scroll to the specified revision""" try: # try instant look up if scmutil.isrevsymbol(self.repo, hglib.fromunicode(revspec)): 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) @pyqtSlot(int) 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 output = bytes(sess.readAll()) 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) def showMessage(self, msg): self.currentMessage = msg if self.isVisible(): self.showMessageSignal.emit(msg) def keyPressEvent(self, event): if self._repoviewFrame.activeInfoBar() and event.key() == Qt.Key_Escape: self.clearInfoBar(infobar.INFO) else: QWidget.keyPressEvent(self, event) def showEvent(self, event): QWidget.showEvent(self, event) self.showMessageSignal.emit(self.currentMessage) 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() def createActions(self): self._mqActions = None if b'mq' in self.repo.extensions(): self._mqActions = mq.PatchQueueActions(self) self._mqActions.setRepoAgent(self._repoagent) self.generateUnappliedPatchMenu() self.generateSingleMenu() self.generatePairMenu() self.generateMultipleSelectionMenu() self.generateBundleMenu() self.generateOutgoingMenu() def detectPatches(self, paths): filepaths = [] for p in paths: if not os.path.isfile(p): continue try: pf = open(p, 'rb') earlybytes = pf.read(4096) if b'\0' in earlybytes: continue pf.seek(0) with hglib.extractpatch(self.repo.ui, pf) as data: if data.get('filename'): filepaths.append(p) except EnvironmentError: pass return filepaths def dragEnterEvent(self, event): paths = [pycompat.unicode(u.toLocalFile()) for u in event.mimeData().urls()] if self.detectPatches(paths): event.setDropAction(Qt.CopyAction) event.accept() def dropEvent(self, event): paths = [pycompat.unicode(u.toLocalFile()) for u in event.mimeData().urls()] patches = self.detectPatches(paths) if not patches: return event.setDropAction(Qt.CopyAction) event.accept() self.thgimport(patches) ## Begin Workbench event forwards def back(self): self.repoview.back() def forward(self): self.repoview.forward() def bisect(self): self._dialogs.open(RepoWidget._createBisectDialog) def _createBisectDialog(self): dlg = bisect.BisectDialog(self._repoagent, self) dlg.newCandidate.connect(self.gotoParent) return dlg def resolve(self): dlg = resolve.ResolveDialog(self._repoagent, self) dlg.exec_() def thgimport(self, paths=None): dlg = thgimport.ImportDialog(self._repoagent, self) if paths: dlg.setfilepaths(paths) if dlg.exec_() == 0: self.gotoTip() def unbundle(self): w = self.syncDemand.get() w.unbundle() def shelve(self, arg=None): self._dialogs.open(RepoWidget._createShelveDialog) def _createShelveDialog(self): dlg = shelve.ShelveDialog(self._repoagent) dlg.finished.connect(self._refreshCommitTabIfNeeded) return dlg def verify(self): cmdline = ['verify', '--verbose'] dlg = cmdui.CmdSessionDialog(self) dlg.setWindowIcon(qtlib.geticon('hg-verify')) dlg.setWindowTitle(_('%s - verify repository') % self.repoDisplayName()) dlg.setWindowFlags(dlg.windowFlags() | Qt.WindowMaximizeButtonHint) dlg.setSession(self._repoagent.runCommand(cmdline, self)) dlg.exec_() def recover(self): cmdline = ['recover', '--verbose'] dlg = cmdui.CmdSessionDialog(self) dlg.setWindowIcon(qtlib.geticon('hg-recover')) dlg.setWindowTitle(_('%s - recover repository') % self.repoDisplayName()) dlg.setWindowFlags(dlg.windowFlags() | Qt.WindowMaximizeButtonHint) dlg.setSession(self._repoagent.runCommand(cmdline, self)) dlg.exec_() def rollback(self): desc, oldlen = hglib.readundodesc(self.repo) if not desc: InfoMsgBox(_('No transaction available'), _('There is no rollback transaction available')) return elif desc == 'commit': if not QuestionMsgBox(_('Undo last commit?'), _('Undo most recent commit (%d), preserving file changes?') % oldlen): return else: if not QuestionMsgBox(_('Undo last transaction?'), _('Rollback to revision %d (undo %s)?') % (oldlen - 1, desc)): return try: rev = self.repo[b'.'].rev() except error.LookupError as e: InfoMsgBox(_('Repository Error'), _('Unable to determine working copy revision\n') + hglib.tounicode(bytes(e))) return if rev >= oldlen and not QuestionMsgBox( _('Remove current working revision?'), _('Your current working revision (%d) will be removed ' 'by this rollback, leaving uncommitted changes.\n ' 'Continue?') % rev): return cmdline = ['rollback', '--verbose'] sess = self._runCommand(cmdline) sess.commandFinished.connect(self._notifyWorkingDirChanges) def purge(self): dlg = purge.PurgeDialog(self._repoagent, self) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) dlg.showMessage.connect(self.showMessage) dlg.progress.connect(self.progress) dlg.exec_() # ignores result code of PurgeDialog because it's unreliable self._refreshCommitTabIfNeeded() ## End workbench event forwards @pyqtSlot(str, dict) def grep(self, pattern='', opts=None): """Open grep task tab""" if opts is None: opts = {} opts = dict((str(k), str(v)) for k, v in opts.items()) self.taskTabsWidget.setCurrentIndex(self._namedTabs['grep']) self.grepDemand.setSearch(pattern, **opts) self.grepDemand.runSearch() def _initModel(self): self.repomodel = repomodel.HgRepoListModel(self._repoagent, self) self.repomodel.setBranch(self.filterbar.branch(), self.filterbar.branchAncestorsIncluded()) self.repomodel.setFilterByRevset(self.filterbar.filtercb.isChecked()) self.repomodel.setShowGraftSource(self.filterbar.getShowGraftSource()) self.repomodel.showMessage.connect(self.showMessage) self.repomodel.showMessage.connect(self._repoviewFrame.showMessage) self.repoview.setModel(self.repomodel) self.repomodel.revsUpdated.connect(self._updateRepoViewForModel) @pyqtSlot() def _updateRepoViewForModel(self): model = self.repoview.model() selmodel = self.repoview.selectionModel() index = selmodel.currentIndex() if not (index.flags() & Qt.ItemIsEnabled): index = model.defaultIndex() f = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows selmodel.setCurrentIndex(index, f) self.repoview.scrollTo(index) self.repoview.enablefilterpalette(bool(model.revset())) self.clearInfoBar(infobar.INFO) # clear progress message @pyqtSlot() def _clearInfoMessage(self): self.clearInfoBar(infobar.INFO) @pyqtSlot() def switchToPreferredTaskTab(self): tw = self.taskTabsWidget rev = self.rev ctx = self.repo[rev] if rev is None or (b'mq' in self.repo.extensions() and b'qtip' in ctx.tags() and self.repo[b'.'].rev() == rev): # Clicking on working copy or on the topmost applied patch # (_if_ it is also the working copy parent) switches to the commit tab tw.setCurrentIndex(self._namedTabs['commit']) else: # Clicking on a normal revision switches from commit tab tw.setCurrentIndex(self._namedTabs['log']) def onRevisionSelected(self, rev): 'View selection changed, could be a reload' self.showMessage('') try: self.revDetailsWidget.onRevisionSelected(rev) self.revisionSelected.emit(rev) if not isinstance(rev, str): # Regular patch or working directory self.grepDemand.forward('setRevision', rev) self.syncDemand.forward('refreshTargets', rev) self.commitDemand.forward('setRev', rev) except (IndexError, error.RevlogError, error.Abort) as e: self.showMessage(hglib.tounicode(str(e))) cw = self.taskTabsWidget.currentWidget() if cw.canswitch(): self.switchToPreferredTaskTab() @pyqtSlot() def gotoParent(self): self.goto('.') def gotoTip(self): self.repoview.clearSelection() self.goto('tip') def goto(self, rev): self.repoview.goto(rev) def onRevisionActivated(self, rev): qgoto = False if hglib.isbasestring(rev): qgoto = True else: ctx = self.repo[rev] if b'qparent' in ctx.tags() or ctx.thgmqappliedpatch(): qgoto = True if b'qtip' in ctx.tags(): qgoto = False if qgoto: self.qgotoSelectedRevision() else: self.visualDiffRevision() def reload(self, invalidate=True): 'Initiate a refresh of the repo model, rebuild graph' try: if invalidate: self.repo.thginvalidate() self.rebuildGraph() self.reloadTaskTab() except EnvironmentError as e: self.showMessage(hglib.tounicode(str(e))) def rebuildGraph(self): 'Called by repositoryChanged signals, and during reload' self.showMessage('') self.filterbar.refresh() self.repoview.saveSettings() def reloadTaskTab(self): w = self.taskTabsWidget.currentWidget() w.reload() @pyqtSlot() def repositoryChanged(self): 'Repository has detected a changelog / dirstate change' try: self.rebuildGraph() except (error.RevlogError, error.RepoError) as e: self.showMessage(hglib.tounicode(str(e))) @pyqtSlot() def configChanged(self): 'Repository is reporting its config files have changed' self.revDetailsWidget.reload() self.titleChanged.emit(self.title()) self.updateTaskTabs() def updateTaskTabs(self): val = self._repoagent.configString('tortoisehg', 'tasktabs').lower() if val == 'east': self.taskTabsWidget.setTabPosition(QTabWidget.East) self.taskTabsWidget.tabBar().show() elif val == 'west': self.taskTabsWidget.setTabPosition(QTabWidget.West) self.taskTabsWidget.tabBar().show() else: self.taskTabsWidget.tabBar().hide() @pyqtSlot(str, bool) def setBranch(self, branch, allparents): self.repomodel.setBranch(branch, allparents=allparents) self.titleChanged.emit(self.title()) @pyqtSlot(bool) def setShowHidden(self, showhidden): self._repoagent.setHiddenRevsIncluded(showhidden) @pyqtSlot(bool) def setShowGraftSource(self, showgraftsource): self.repomodel.setShowGraftSource(showgraftsource) ## ## Workbench methods ## def canGoBack(self): return self.repoview.canGoBack() def canGoForward(self): return self.repoview.canGoForward() def loadSettings(self): s = QSettings() repoid = hglib.shortrepoid(self.repo) self.revDetailsWidget.loadSettings(s) self.filterbar.loadSettings(s) self._repoagent.setHiddenRevsIncluded(self.filterbar.getShowHidden()) self.repotabs_splitter.restoreState( qtlib.readByteArray(s, 'repoWidget/splitter-' + repoid)) def okToContinue(self): if self._repoagent.isBusy(): r = QMessageBox.question(self, _('Confirm Exit'), _('Mercurial command is still running.\n' 'Are you sure you want to terminate?'), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if r == QMessageBox.Yes: self._repoagent.abortCommands() return False for i in pycompat.xrange(self.taskTabsWidget.count()): w = self.taskTabsWidget.widget(i) if w.canExit(): continue self.taskTabsWidget.setCurrentWidget(w) self.showMessage(_('Tab cannot exit')) return False return True def closeRepoWidget(self): '''returns False if close should be aborted''' if not self.okToContinue(): return False s = QSettings() if self.isVisible(): try: repoid = hglib.shortrepoid(self.repo) s.setValue('repoWidget/splitter-' + repoid, self.repotabs_splitter.saveState()) except EnvironmentError: pass self.revDetailsWidget.saveSettings(s) self.commitDemand.forward('saveSettings', s, 'workbench') self.grepDemand.forward('saveSettings', s) self.filterbar.saveSettings(s) self.repoview.saveSettings(s) return True def setSyncUrl(self, url): """Change the current peer-repo url of the sync widget; url may be a symbolic name defined in [paths] section""" self.syncDemand.get().setUrl(url) def incoming(self): self.syncDemand.get().incoming() def pull(self): self.syncDemand.get().pull() def outgoing(self): self.syncDemand.get().outgoing() def push(self, confirm=None, **kwargs): """Call sync push. If confirm is False, the user will not be prompted for confirmation. If confirm is True, the prompt might be used. """ self.syncDemand.get().push(confirm, **kwargs) self.outgoingMode = False def syncBookmark(self): self.syncDemand.get().syncBookmark() ## ## Repoview context menu ## def viewMenuRequest(self, point, selection): 'User requested a context menu in repo view widget' # selection is a list of the currently selected revisions. # Integers for changelog revisions, None for the working copy, # or strings for unapplied patches. if len(selection) == 0: return self.menuselection = selection if self._repoagent.overlayUrl(): if len(selection) == 1: self.bundlemenu.exec_(point) return if self.outgoingMode: if len(selection) == 1: self.outgoingcmenu.exec_(point) return allunapp = False if b'mq' in self.repo.extensions(): for rev in selection: if not self.repo[rev].thgmqunappliedpatch(): break else: allunapp = True if allunapp: self.unappliedPatchMenu(point, selection) elif len(selection) == 1: self.singleSelectionMenu(point, selection) elif len(selection) == 2: self.doubleSelectionMenu(point, selection) else: self.multipleSelectionMenu(point, selection) def _selectionMenuExec(self, point, selection, menu, menuitems): ctxs = [self.repo[rev] for rev in selection] applied = [ctx.thgmqappliedpatch() for ctx in ctxs] working = [ctx.rev() is None for ctx in ctxs] tags = [ctx.tags() for ctx in ctxs] phases = [ctx.phase() for ctx in ctxs] for item in menuitems: enabled = all(item.enableFunc(*args) for args in zip(applied, working, tags, phases)) item.setEnabled(enabled) menu.exec_(point) def singleSelectionMenu(self, point, selection): self._selectionMenuExec(point, selection, self.singlecmenu, self.singlecmenuitems) def doubleSelectionMenu(self, point, selection): for r in selection: # No pair menu if working directory or unapplied patch if not isinstance(r, int): return self._selectionMenuExec(point, selection, self.paircmenu, self.paircmenuitems) def multipleSelectionMenu(self, point, selection): for r in selection: # No multi menu if working directory or unapplied patch if not isinstance(r, int): return self._selectionMenuExec(point, selection, self.multicmenu, self.multicmenuitems) def unappliedPatchMenu(self, point, selection): q = self.repo.mq ispushable = False unapplied = 0 for i in pycompat.xrange(q.seriesend(), len(q.series)): pushable, reason = q.pushable(i) if pushable: if unapplied == 0: qnext = q.series[i] if self.rev == q.series[i]: ispushable = True unapplied += 1 self.unappacts[0].setEnabled(ispushable and len(selection) == 1) self.unappacts[1].setEnabled(ispushable and len(selection) == 1) self.unappacts[2].setEnabled(ispushable and len(selection) == 1 and \ self.rev != qnext) self.unappacts[3].setEnabled(b'qtip' in self.repo.tags()) self.unappacts[4].setEnabled(True) self.unappacts[5].setEnabled(len(selection) == 1) self.unappcmenu.exec_(point) def _createMenuEntry(self, items, menu, ext=None, func=None, desc=None, icon=None, cb=None): if ext: ext = pycompat.sysbytes(ext) if ext not in self.repo.extensions(): return if desc is None: return menu.addSeparator() act = QAction(desc, self) if cb: act.triggered.connect(cb) if icon: act.setIcon(qtlib.geticon(icon)) act.enableFunc = func menu.addAction(act) items.append(act) return act def _setupCustomSubmenu(self, items, menu, location): tools, toollist = hglib.tortoisehgtools(self.repo.ui, selectedlocation=location) if not tools: return self._createMenuEntry(items, menu) submenu = menu.addMenu(_('Custom Tools')) submenu.triggered.connect(self._runCustomCommandByMenu) for name in toollist: if name == '|': self._createMenuEntry(items, submenu) continue info = tools.get(name, None) if info is None: continue command = info.get('command', None) if not command: continue workingdir = info.get('workingdir', '') showoutput = info.get('showoutput', False) label = info.get('label', name) icon = info.get('icon', 'tools-spanner-hammer') enable = info.get('enable', 'istrue').lower() if enable in _ENABLE_MENU_FUNCS: enable = _ENABLE_MENU_FUNCS[enable] else: continue a = self._createMenuEntry(items, submenu, None, enable, label, icon) a.setData((command, showoutput, workingdir)) def generateSingleMenu(self, mode=None): items = [] # This menu will never be opened for an unapplied patch, they # have their own menu. entry = self._createMenuEntry enablefuncs = _ENABLE_MENU_FUNCS exs = self.repo.extensions() menu = QMenu(self) if mode == 'outgoing': pushtypeicon = {'all': None, 'branch': None, 'revision': None} defaultpush = self._repoagent.configString( 'tortoisehg', 'defaultpush') pushtypeicon[defaultpush] = 'hg-push' submenu = menu.addMenu(_('Pus&h')) entry(items, submenu, None, enablefuncs['isrev'], _('Push to &Here'), pushtypeicon['revision'], self.pushToRevision) entry(items, submenu, None, enablefuncs['isrev'], _('Push Selected &Branch'), pushtypeicon['branch'], self.pushBranch) entry(items, submenu, None, enablefuncs['isrev'], _('Push &All'), pushtypeicon['all'], self.pushAll) entry(items, menu) entry(items, menu, None, enablefuncs['isrev'], _('&Update...'), 'hg-update', self.updateToRevision) entry(items, menu) entry(items, menu, None, enablefuncs['isctx'], _('&Diff to Parent'), 'visualdiff', self.visualDiffRevision) entry(items, menu, None, enablefuncs['isrev'], _('Diff to &Local'), 'ldiff', self.visualDiffToLocal) entry(items, menu, None, enablefuncs['isctx'], _('Bro&wse at Revision'), 'hg-annotate', self.manifestRevision) act = self._createFilterBySelectedRevisionsMenu() act.enableFunc = enablefuncs['isrev'] menu.addAction(act) items.append(act) entry(items, menu) entry(items, menu, None, enablefuncs['fixed'], _('&Merge with Local...'), 'hg-merge', self.mergeWithRevision) if b'closehead' in exs: entry(items, menu, None, enablefuncs['isrev'], _('&Close Branch'), 'hg-close-head', self.closeRevision) entry(items, menu) entry(items, menu, None, enablefuncs['fixed'], _('&Tag...'), 'hg-tag', self.tagToRevision) entry(items, menu, None, enablefuncs['isrev'], _('Boo&kmark...'), 'hg-bookmarks', self.bookmarkRevision) entry(items, menu, 'topic', enablefuncs['isdraftorwd'], _('Top&ic...'), 'topic', self.topicsRevision) entry(items, menu, 'gpg', enablefuncs['fixed'], _('Sig&n...'), 'hg-sign', self.signRevision) entry(items, menu) entry(items, menu, None, enablefuncs['fixed'], _('&Backout...'), 'hg-revert', self.backoutToRevision) entry(items, menu, None, enablefuncs['isctx'], _('Revert &All Files...'), 'hg-revert', self.revertToRevision) entry(items, menu) entry(items, menu, None, enablefuncs['isrev'], _('Copy &Hash'), 'copy-hash', self.copyHash) entry(items, menu) submenu = menu.addMenu(_('E&xport')) entry(items, submenu, None, enablefuncs['isrev'], _('E&xport Patch...'), 'hg-export', self.exportRevisions) entry(items, submenu, None, enablefuncs['isrev'], _('&Email Patch...'), 'mail-forward', self.emailSelectedRevisions) entry(items, submenu, None, enablefuncs['isrev'], _('&Archive...'), 'hg-archive', self.archiveRevision) entry(items, submenu, None, enablefuncs['isrev'], _('&Bundle Rev and Descendants...'), 'hg-bundle', self.bundleRevisions) entry(items, submenu, None, enablefuncs['isctx'], _('&Copy Patch'), 'copy-patch', self.copyPatch) entry(items, menu) submenu = menu.addMenu(_('Change &Phase to')) submenu.triggered.connect(self._changePhaseByMenu) # TODO: filter out hidden names better for pnum, pname in enumerate(phases.phasenames[:3]): a = entry(items, submenu, None, enablefuncs['isrev'], pycompat.sysstr(pname)) assert a is not None # help pytype a.setData(pnum) entry(items, menu) entry(items, menu, None, enablefuncs['isrev'], _('&Graft to Local...'), 'hg-transplant', self.graftRevisions) if (b'mq' in exs or b'rebase' in exs or b'strip' in exs or b'evolve' in exs): submenu = menu.addMenu(_('Modi&fy History')) entry(items, submenu, 'mq', enablefuncs['applied'], _('&Unapply Patch'), 'hg-qgoto', self.qgotoParentRevision) entry(items, submenu, 'mq', enablefuncs['fixed'], _('Import to &MQ'), 'qimport', self.qimportRevision) entry(items, submenu, 'mq', enablefuncs['applied'], _('&Finish Patch'), 'qfinish', self.qfinishRevision) entry(items, submenu, 'mq', enablefuncs['applied'], _('Re&name Patch...'), None, self.qrename) entry(items, submenu, 'mq') if self._mqActions: entry(items, submenu, 'mq', enablefuncs['isctx'], _('MQ &Options'), None, self._mqActions.launchOptionsDialog) entry(items, submenu, 'mq') entry(items, submenu, 'evolve', enablefuncs['isrev'], _('Pick...'), None, self._pickRevision) entry(items, submenu, 'rebase', enablefuncs['isrev'], _('&Rebase...'), 'hg-rebase', self.rebaseRevision) entry(items, submenu, 'rebase') entry(items, submenu, 'evolve', enablefuncs['fixed'], _('&Prune...'), 'edit-cut', self._pruneSelected) if b'mq' in exs or b'strip' in exs: entry(items, submenu, None, enablefuncs['fixed'], _('&Strip...'), 'hg-strip', self.stripRevision) entry(items, menu, 'reviewboard', enablefuncs['isrev'], _('Post to Re&view Board...'), 'reviewboard', self.sendToReviewBoard) entry(items, menu, 'phabricator', enablefuncs['isrev'], _('Post to Phabricator...'), 'phabricator', self.sendToPhabricator) self._setupCustomSubmenu(items, menu, 'workbench.revdetails.custom-menu') if mode == 'outgoing': self.outgoingcmenu = menu self.outgoingcmenuitems = items else: self.singlecmenu = menu self.singlecmenuitems = items def _gotoAncestor(self): ancestor = self.repo[self.menuselection[0]] for rev in self.menuselection[1:]: ctx = self.repo[rev] ancestor = ancestor.ancestor(ctx) self.goto(ancestor.rev()) def generatePairMenu(self): def dagrange(): revA, revB = self.menuselection if revA > revB: B, A = self.menuselection else: A, B = self.menuselection # simply disable lazy evaluation as we won't handle slow query return list(self.repo.revs(b'%d::%d', A, B)) def exportPair(): self.exportRevisions(self.menuselection) def exportDiff(): root = self.repo.root filename = b'%s_%d_to_%d.diff' % (os.path.basename(root), self.menuselection[0], self.menuselection[1]) file, _filter = QFileDialog.getSaveFileName( self, _('Write diff file'), hglib.tounicode(os.path.join(root, filename))) if not file: return f = QFile(file) if not f.open(QIODevice.WriteOnly | QIODevice.Truncate): WarningMsgBox(_('Repository Error'), _('Unable to write diff file')) return sess = self._buildPatch('diff') sess.setOutputDevice(f) def archiveDagRange(): l = dagrange() if l: self.archiveRevisions(l) def exportDagRange(): l = dagrange() if l: self.exportRevisions(l) def diffPair(): revA, revB = self.menuselection dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], {'rev':(str(revA), str(revB))}) if dlg: dlg.exec_() def emailPair(): self._emailRevisions(self.menuselection) def emailDagRange(): l = dagrange() if l: self._emailRevisions(l) def bundleDagRange(): l = dagrange() if l: self.bundleRevisions(base=l[0], tip=l[-1]) def bisectNormal(): revA, revB = self.menuselection dlg = self._dialogs.open(RepoWidget._createBisectDialog) dlg.restart(str(revA), str(revB)) def bisectReverse(): revA, revB = self.menuselection dlg = self._dialogs.open(RepoWidget._createBisectDialog) dlg.restart(str(revB), str(revA)) def compressDlg(): ctxa, ctxb = map(self.repo.hgchangectx, self.menuselection) if ctxa.ancestor(ctxb).rev() == ctxb.rev(): revs = self.menuselection[:] elif ctxa.ancestor(ctxb).rev() == ctxa.rev(): revs = [self.menuselection[1], self.menuselection[0]] else: InfoMsgBox(_('Unable to compress history'), _('Selected changeset pair not related')) return dlg = compress.CompressDialog(self._repoagent, revs, self) dlg.exec_() def rebaseDlg(): opts = {'source': self.menuselection[0], 'dest': self.menuselection[1]} dlg = rebase.RebaseDialog(self._repoagent, self, **opts) dlg.exec_() items = [] entry = self._createMenuEntry enablefuncs = _ENABLE_MENU_FUNCS menu = QMenu(self) entry(items, menu, None, enablefuncs['istrue'], _('Visual Diff...'), 'visualdiff', diffPair) entry(items, menu, None, enablefuncs['istrue'], _('Export Diff...'), 'hg-export', exportDiff) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Export Selected...'), 'hg-export', exportPair) entry(items, menu, None, enablefuncs['istrue'], _('Email Selected...'), 'mail-forward', emailPair) entry(items, menu, None, enablefuncs['istrue'], _('Copy Selected as Patch'), 'copy-patch', self.copyPatch) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Archive DAG Range...'), 'hg-archive', archiveDagRange) entry(items, menu, None, enablefuncs['istrue'], _('Export DAG Range...'), 'hg-export', exportDagRange) entry(items, menu, None, enablefuncs['istrue'], _('Email DAG Range...'), 'mail-forward', emailDagRange) entry(items, menu, None, enablefuncs['istrue'], _('Bundle DAG Range...'), 'hg-bundle', bundleDagRange) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Bisect - Good, Bad...'), 'hg-bisect-good-bad', bisectNormal) entry(items, menu, None, enablefuncs['istrue'], _('Bisect - Bad, Good...'), 'hg-bisect-bad-good', bisectReverse) entry(items, menu, None, enablefuncs['istrue'], _('Compress History...'), 'hg-compress', compressDlg) entry(items, menu, 'rebase', enablefuncs['istrue'], _('Rebase...'), 'hg-rebase', rebaseDlg) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Goto common ancestor'), 'hg-merge', self._gotoAncestor) menu.addAction(self._createFilterBySelectedRevisionsMenu()) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Graft Selected to local...'), 'hg-transplant', self.graftRevisions) entry(items, menu) entry(items, menu, 'evolve', enablefuncs['istrue'], _('&Prune Selected...'), 'edit-cut', self._pruneSelected) if b'reviewboard' in self.repo.extensions(): menu.addSeparator() a = QAction(_('Post Selected to Review Board...'), self) a.triggered.connect(self.sendToReviewBoard) menu.addAction(a) if b'phabricator' in self.repo.extensions(): menu.addSeparator() a = QAction(_('Post Selected to Phabricator...'), self) a.triggered.connect(self.sendToPhabricator) menu.addAction(a) self._setupCustomSubmenu(items, menu, 'workbench.pairselection.custom-menu') self.paircmenu = menu self.paircmenuitems = items def generateUnappliedPatchMenu(self): def qdeleteact(): """Delete unapplied patch(es)""" patches = pycompat.maplist(hglib.tounicode, self.menuselection) self._mqActions.deletePatches(patches) def qfoldact(): patches = pycompat.maplist(hglib.tounicode, self.menuselection) self._mqActions.foldPatches(patches) menu = QMenu(self) acts = [] for name, cb, icon in ( (_('Apply patch'), self.qpushRevision, 'hg-qpush'), (_('Apply onto original parent'), self.qpushExactRevision, None), (_('Apply only this patch'), self.qpushMoveRevision, None), (_('Fold patches...'), qfoldact, 'hg-qfold'), (_('Delete patches...'), qdeleteact, 'hg-qdelete'), (_('Rename patch...'), self.qrename, None)): act = QAction(name, self) act.triggered.connect(cb) if icon: act.setIcon(qtlib.geticon(icon)) acts.append(act) menu.addAction(act) menu.addSeparator() acts.append(menu.addAction(_('MQ &Options'), self._mqActions.launchOptionsDialog)) self.unappcmenu = menu self.unappacts = acts def generateMultipleSelectionMenu(self): def exportSel(): self.exportRevisions(self.menuselection) def emailSel(): self._emailRevisions(self.menuselection) entry = self._createMenuEntry enablefuncs = _ENABLE_MENU_FUNCS items = [] menu = QMenu(self) entry(items, menu, None, enablefuncs['istrue'], _('Export Selected...'), 'hg-export', exportSel) entry(items, menu, None, enablefuncs['istrue'], _('Email Selected...'), 'mail-forward', emailSel) entry(items, menu, None, enablefuncs['istrue'], _('Copy Selected as Patch'), 'copy-patch', self.copyPatch) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Goto common ancestor'), 'hg-merge', self._gotoAncestor) menu.addAction(self._createFilterBySelectedRevisionsMenu()) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Graft Selected to local...'), 'hg-transplant', self.graftRevisions) if b'evolve' in self.repo.extensions(): menu.addSeparator() entry(items, menu, None, enablefuncs['istrue'], _('&Prune Selected...'), 'edit-cut', self._pruneSelected) entry(items, menu, 'reviewboard', enablefuncs['istrue'], _('Post Selected to Review Board...'), None, self.sendToReviewBoard) entry(items, menu, 'phabricator', enablefuncs['istrue'], _('Post Selected to Phabricator...'), None, self.sendToPhabricator) self._setupCustomSubmenu(items, menu, 'workbench.multipleselection.custom-menu') self.multicmenu = menu self.multicmenuitems = items def generateBundleMenu(self): menu = QMenu(self) for name, cb, icon in ( (_('Pull to here...'), self.pullBundleToRev, 'hg-pull-to-here'), (_('Visual diff...'), self.visualDiffRevision, 'visualdiff'), ): a = QAction(name, self) a.triggered.connect(cb) if icon: a.setIcon(qtlib.geticon(icon)) menu.addAction(a) self.bundlemenu = menu def generateOutgoingMenu(self): self.generateSingleMenu(mode='outgoing') def exportRevisions(self, revisions): if not revisions: revisions = [self.rev] if len(revisions) == 1: if isinstance(self.rev, int): defaultpath = os.path.join(self.repoRootPath(), '%d.patch' % self.rev) else: defaultpath = self.repoRootPath() ret, _filter = QFileDialog.getSaveFileName( self, _('Export patch'), defaultpath, _('Patch Files (*.patch)')) if not ret: return epath = pycompat.unicode(ret) udir = os.path.dirname(epath) custompath = True else: udir = QFileDialog.getExistingDirectory(self, _('Export patch'), hglib.tounicode(self.repo.root)) if not udir: return udir = pycompat.unicode(udir) ename = self._repoagent.shortName() + '_%r.patch' epath = os.path.join(udir, ename) custompath = False cmdline = hglib.buildcmdargs('export', verbose=True, output=epath, rev=hglib.compactrevs(sorted(revisions))) existingRevisions = [] for rev in revisions: if custompath: path = epath else: path = epath % rev if os.path.exists(path): if os.path.isfile(path): existingRevisions.append(rev) else: QMessageBox.warning(self, _('Cannot export revision'), (_('Cannot export revision %s into the file named:' '\n\n%s\n') % (rev, epath % rev)) + \ _('There is already an existing folder ' 'with that same name.')) return if existingRevisions: buttonNames = [_("Replace"), _("Append"), _("Abort")] warningMessage = \ _('There are existing patch files for %d revisions (%s) ' 'in the selected location (%s).\n\n') \ % (len(existingRevisions), " ,".join([str(rev) for rev in existingRevisions]), udir) warningMessage += \ _('What do you want to do?\n') + u'\n' + \ u'- ' + _('Replace the existing patch files.\n') + \ u'- ' + _('Append the changes to the existing patch files.\n') + \ u'- ' + _('Abort the export operation.\n') res = qtlib.CustomPrompt(_('Patch files already exist'), warningMessage, self, buttonNames, 0, 2).run() if buttonNames[res] == _("Replace"): # Remove the existing patch files for rev in existingRevisions: if custompath: os.remove(epath) else: os.remove(epath % rev) elif buttonNames[res] == _("Abort"): return self._runCommand(cmdline) if len(revisions) == 1: # Show a message box with a link to the export folder and to the # exported file rev = revisions[0] patchfilename = os.path.normpath(epath) patchdirname = os.path.normpath(os.path.dirname(epath)) patchshortname = os.path.basename(patchfilename) if patchdirname.endswith(os.path.sep): patchdirname = patchdirname[:-1] qtlib.InfoMsgBox(_('Patch exported'), _('Revision #%d (%s) was exported to:

' '%s%s' '%s') \ % (rev, str(self.repo[rev]), patchdirname, patchdirname, os.path.sep, patchfilename, patchshortname)) else: # Show a message box with a link to the export folder qtlib.InfoMsgBox(_('Patches exported'), _('%d patches were exported to:

' '%s') \ % (len(revisions), udir, udir)) def visualDiffRevision(self): opts = dict(change=self.rev) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() def visualDiffToLocal(self): if self.rev is None: return opts = dict(rev=['rev(%d)' % self.rev]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() @pyqtSlot() def updateToRevision(self): rev = None if isinstance(self.rev, int): rev = hglib.tounicode(hglib.getrevisionlabel(self.repo, self.rev)) dlg = update.UpdateDialog(self._repoagent, rev, self) r = dlg.exec_() if r in (0, 1): self.gotoParent() @pyqtSlot() def lockTool(self): from .locktool import LockDialog dlg = LockDialog(self._repoagent, self) if dlg: dlg.exec_() @pyqtSlot() def revertToRevision(self): if not qtlib.QuestionMsgBox( _('Confirm Revert'), _('Reverting all files will discard changes and ' 'leave affected files in a modified state.
' '
Are you sure you want to use revert?

' '(use update to checkout another revision)'), parent=self): return cmdline = hglib.buildcmdargs('revert', all=True, rev=self.rev) sess = self._runCommand(cmdline) sess.commandFinished.connect(self._refreshCommitTabIfNeeded) def _createFilterBySelectedRevisionsMenu(self): menu = QMenu(_('Filter b&y'), self) menu.setIcon(qtlib.geticon('view-filter')) menu.triggered.connect(self._filterBySelectedRevisions) for t, r in [(_('&Ancestors and Descendants'), "ancestors({revs}) or descendants({revs})"), (_('A&uthor'), "matching({revs}, 'author')"), (_('&Branch'), "branch({revs})"), ]: a = menu.addAction(t) a.setData(r) menu.addSeparator() menu.addAction(_('&More Options...')) return menu.menuAction() @pyqtSlot(QAction) def _filterBySelectedRevisions(self, action): revs = hglib.compactrevs(sorted(self.repoview.selectedRevisions())) expr = action.data() if not expr: self._filterByMatchDialog(revs) return self.setFilter(expr.format(revs=revs)) def _filterByMatchDialog(self, revlist): dlg = matching.MatchDialog(self._repoagent, revlist, self) if dlg.exec_(): self.setFilter(dlg.revsetexpression) def pushAll(self): self.syncDemand.forward('push', False, pushall=True) def pushToRevision(self): # Do not ask for confirmation self.syncDemand.forward('push', False, rev=self.rev) def pushBranch(self): # Do not ask for confirmation self.syncDemand.forward('push', False, branch=self.repo[self.rev].branch()) def manifestRevision(self): if QApplication.keyboardModifiers() & Qt.ShiftModifier: self._dialogs.openNew(RepoWidget._createManifestDialog) else: dlg = self._dialogs.open(RepoWidget._createManifestDialog) dlg.setRev(self.rev) def _createManifestDialog(self): return revdetails.createManifestDialog(self._repoagent, self.rev) def mergeWithOtherHead(self): """Open dialog to merge with the other head of the current branch""" cmdline = hglib.buildcmdargs('merge', preview=True, config=r'ui.logtemplate={rev}\n') sess = self._runCommand(cmdline) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onMergePreviewFinished) @pyqtSlot(int) def _onMergePreviewFinished(self, ret): sess = self.sender() if ret == 255 and 'hg heads' in sess.errorString(): # multiple heads self.filterbar.setQuery('head() - .') self.filterbar.runQuery() msg = '\n'.join(sess.errorString().splitlines()[:-1]) # drop hint w = self.setInfoBar(infobar.ConfirmInfoBar, msg) assert w w.acceptButton.setText(_('Merge')) w.accepted.connect(self.mergeWithRevision) w.finished.connect(self.clearRevisionSet) return if ret != 0: return revs = pycompat.maplist(int, bytes(sess.readAll()).splitlines()) if not revs: return self._dialogs.open(RepoWidget._createMergeDialog, revs[-1]) @pyqtSlot() def mergeWithRevision(self): # Don't use self.rev (i.e. the current revision.) This is a context # menu handler, and the menu is open for the selected rows, not for # the current row. revisions = self.repoview.selectedRevisions() if len(revisions) != 1: QMessageBox.warning(self, _('Unable to merge'), _('Please select a revision to merge.')) return rev = revisions[0] if not isinstance(rev, int): QMessageBox.warning(self, _('Unable to merge'), _('Cannot merge with a pseudo revision %r.') % rev) return pctx = self.repo[b'.'] octx = self.repo[rev] if pctx == octx: QMessageBox.warning(self, _('Unable to merge'), _('You cannot merge a revision with itself')) return self._dialogs.open(RepoWidget._createMergeDialog, rev) def _createMergeDialog(self, rev): return merge.MergeDialog(self._repoagent, rev, self) def tagToRevision(self): dlg = tag.TagDialog(self._repoagent, rev=str(self.rev), parent=self) dlg.exec_() def closeRevision(self): dlg = close_branch.createCloseBranchDialog(self._repoagent, self.rev, parent=self) dlg.exec_() def bookmarkRevision(self): dlg = bookmark.BookmarkDialog(self._repoagent, self.rev, self) dlg.exec_() def topicsRevision(self): dlg = topics.TopicsDialog(self._repoagent, self.rev, self) dlg.exec_() def signRevision(self): dlg = sign.SignDialog(self._repoagent, self.rev, self) dlg.exec_() def graftRevisions(self): """Graft selected revision on top of working directory parent""" revlist = [] for rev in sorted(self.repoview.selectedRevisions()): revlist.append(str(rev)) if not revlist: revlist = [self.rev] dlg = graft.GraftDialog(self._repoagent, self, source=revlist) if dlg.valid: dlg.exec_() def backoutToRevision(self): msg = backout.checkrev(self._repoagent.rawRepo(), self.rev) if msg: qtlib.InfoMsgBox(_('Unable to backout'), msg, parent=self) return dlg = backout.BackoutDialog(self._repoagent, self.rev, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() @pyqtSlot() def _pruneSelected(self): revspec = hglib.compactrevs(sorted(self.repoview.selectedRevisions())) dlg = prune.createPruneDialog(self._repoagent, revspec, self) dlg.exec_() def stripRevision(self): 'Strip the selected revision and all descendants' dlg = thgstrip.createStripDialog(self._repoagent, rev=str(self.rev), parent=self) dlg.exec_() def sendToReviewBoard(self): self._dialogs.open(RepoWidget._createPostReviewDialog, tuple(self.repoview.selectedRevisions())) def _createPostReviewDialog(self, revs): return postreview.PostReviewDialog(self.repo.ui, self._repoagent, revs) @pyqtSlot() def sendToPhabricator(self): self._dialogs.open(RepoWidget._createPhabReviewDialog, tuple(self.repoview.selectedRevisions())) def _createPhabReviewDialog(self, revs): return phabreview.PhabReviewDialog(self._repoagent, revs) @pyqtSlot() def emailSelectedRevisions(self): self._emailRevisions(self.repoview.selectedRevisions()) def _emailRevisions(self, revs): self._dialogs.open(RepoWidget._createEmailDialog, tuple(revs)) def _createEmailDialog(self, revs): return hgemail.EmailDialog(self._repoagent, revs) def archiveRevision(self): rev = hglib.getrevisionlabel(self.repo, self.rev) dlg = archive.createArchiveDialog(self._repoagent, rev, self) dlg.exec_() def archiveRevisions(self, revs): rev = max(revs) minrev = min(revs) dlg = archive.createArchiveDialog(self._repoagent, rev=rev, minrev=minrev, parent=self) dlg.exec_() def bundleRevisions(self, base=None, tip=None): root = self.repoRootPath() if base is None or base is False: base = self.rev data = dict(name=os.path.basename(root), base=base) if tip is None: filename = '%(name)s_%(base)s_and_descendants.hg' % data else: data.update(rev=tip) filename = '%(name)s_%(base)s_to_%(rev)s.hg' % data file, _filter = QFileDialog.getSaveFileName( self, _('Write bundle'), os.path.join(root, filename)) if not file: return cmdline = ['bundle', '--verbose'] parents = [hglib.escaperev(r.rev()) for r in self.repo[base].parents()] for p in parents: cmdline.extend(['--base', p]) if tip: cmdline.extend(['--rev', str(tip)]) else: cmdline.extend(['--rev', 'heads(descendants(%s))' % base]) cmdline.append(pycompat.unicode(file)) self._runCommand(cmdline) def _buildPatch(self, command=None): if not command: # workingdir revision cannot be exported if self.rev is None: command = 'diff' else: command = 'export' assert command in ('export', 'diff'), command if command == 'export': # patches should be in chronological order revs = sorted(self.menuselection) cmdline = hglib.buildcmdargs('export', rev=hglib.compactrevs(revs)) else: revs = self.rev and self.menuselection or None cmdline = hglib.buildcmdargs('diff', rev=revs) return self._runCommand(cmdline) @pyqtSlot() def copyPatch(self): sess = self._buildPatch() sess.setCaptureOutput(True) sess.commandFinished.connect(self._copyPatchOutputToClipboard) @pyqtSlot(int) def _copyPatchOutputToClipboard(self, ret): if ret == 0: sess = self.sender() output = sess.readAll() mdata = QMimeData() mdata.setData('text/x-diff', output) # for lossless import mdata.setText(hglib.tounicode(str(output))) QApplication.clipboard().setMimeData(mdata) def copyHash(self): clip = QApplication.clipboard() clip.setText( hglib.tounicode(binascii.hexlify(self.repo[self.rev].node()))) def changePhase(self, phase): currentphase = self.repo[self.rev].phase() if currentphase == phase: # There is nothing to do, we are already in the target phase return phasestr = pycompat.sysstr(phases.phasenames[phase]) cmdline = ['phase', '--rev', '%s' % self.rev, '--%s' % phasestr] if currentphase < phase: # Ask the user if he wants to force the transition title = _('Backwards phase change requested') if currentphase == phases.draft and phase == phases.secret: # Here we are sure that the current phase is draft and the target phase is secret # Nevertheless we will not hard-code those phase names on the dialog strings to # make sure that the proper phase name translations are used main = _('Do you really want to make this revision secret?') text = _('Making a "draft" revision "secret" ' 'is generally a safe operation.\n\n' 'However, there are a few caveats:\n\n' '- "secret" revisions are not pushed. ' 'This can cause you trouble if you\n' 'refer to a secret subrepo revision.\n\n' '- If you pulled this revision from ' 'a non publishing server it may be\n' 'moved back to "draft" if you pull ' 'again from that particular server.\n\n' 'Please be careful!') labels = ((QMessageBox.Yes, _('&Make secret')), (QMessageBox.No, _('&Cancel'))) else: currentphasestr = pycompat.sysstr( phases.phasenames[currentphase]) main = _('Do you really want to force a backwards phase transition?') text = _('You are trying to move the phase of revision %d backwards,\n' 'from "%s" to "%s".\n\n' 'However, "%s" is a lower phase level than "%s".\n\n' 'Moving the phase backwards is not recommended.\n' 'For example, it may result in having multiple heads\nif you ' 'modify a revision that you have already pushed\nto a server.\n\n' 'Please be careful!') % (self.rev, currentphasestr, phasestr, phasestr, currentphasestr) labels = ((QMessageBox.Yes, _('&Force')), (QMessageBox.No, _('&Cancel'))) if not qtlib.QuestionMsgBox(title, main, text, labels=labels, parent=self): return cmdline.append('--force') self._runCommand(cmdline) @pyqtSlot(QAction) def _changePhaseByMenu(self, action): phasenum = action.data() self.changePhase(phasenum) def _pickRevision(self): """Pick selected revision on top of working directory parent""" opts = {'rev': self.rev} dlg = pick.PickDialog(self._repoagent, self, **opts) dlg.exec_() def rebaseRevision(self): """Rebase selected revision on top of working directory parent""" opts = {'source' : self.rev, 'dest': self.repo[b'.'].rev()} dlg = rebase.RebaseDialog(self._repoagent, self, **opts) dlg.exec_() def qimportRevision(self): """QImport revision and all descendents to MQ""" if b'qparent' in self.repo.tags(): endrev = b'qparent' else: endrev = b'' # Check whether there are existing patches in the MQ queue whose name # collides with the revisions that are going to be imported revList = self.repo.revs(b'%s::%s and not hidden()' % (self.rev, endrev)) if endrev and not revList: # There is a qparent but the revision list is empty # This means that the qparent is not a descendant of the # selected revision QMessageBox.warning(self, _('Cannot import selected revision'), _('The selected revision (rev #%d) cannot be imported ' 'because it is not a descendant of ''qparent'' (rev #%d)') \ % (self.rev, scmutil.revsymbol(self.repo, b'qparent').rev())) return patchdir = self.repo.vfs.join(b'patches') def patchExists(p): return os.path.exists(os.path.join(patchdir, p)) # Note that the following two arrays are both ordered by "rev" defaultPatchNames = ['%d.diff' % rev for rev in revList] defaultPatchesExist = [patchExists(p) for p in defaultPatchNames] if any(defaultPatchesExist): # We will qimport each revision one by one, starting from the newest # To do so, we will find a valid and unique patch name for each # revision that we must qimport (i.e. a filename that does not # already exist) # and then we will import them one by one starting from the newest # one, using these unique names def getUniquePatchName(baseName): maxRetries = 99 for n in range(1, maxRetries): patchName = baseName + '_%02d.diff' % n if not patchExists(patchName): return patchName return baseName patchNames = {} for n, rev in enumerate(revList): if defaultPatchesExist[n]: patchNames[rev] = getUniquePatchName(str(rev)) else: # The default name is safe patchNames[rev] = defaultPatchNames[n] # qimport each revision individually, starting from the topmost one revList.reverse() cmdlines = [] for rev in revList: cmdlines.append(['qimport', '--rev', '%s' % rev, '--name', patchNames[rev]]) self._runCommandSequence(cmdlines) else: # There were no collisions with existing patch names, we can # simply qimport the whole revision set in a single go cmdline = ['qimport', '--rev', '%s::%s' % (self.rev, endrev)] self._runCommand(cmdline) def qfinishRevision(self): """Finish applied patches up to and including selected revision""" self._mqActions.finishRevision(hglib.tounicode(str(self.rev))) @pyqtSlot() def qgotoParentRevision(self): """Apply an unapplied patch, or qgoto the parent of an applied patch""" self.qgotoRevision(self.repo[self.rev].p1().rev()) @pyqtSlot() def qgotoSelectedRevision(self): self.qgotoRevision(self.rev) def qgotoRevision(self, rev): """Make REV the top applied patch""" mqw = self._mqActions ctx = self.repo[rev] if b'qparent' in ctx.tags(): mqw.popAllPatches() else: mqw.gotoPatch(hglib.tounicode(ctx.thgmqpatchname())) def qrename(self): sel = self.menuselection[0] if not isinstance(sel, str): sel = self.repo[sel].thgmqpatchname() self._mqActions.renamePatch(hglib.tounicode(sel)) def _qpushRevision(self, move=False, exact=False): """QPush REV with the selected options""" ctx = self.repo[self.rev] patchname = hglib.tounicode(ctx.thgmqpatchname()) self._mqActions.pushPatch(patchname, move=move, exact=exact) def qpushRevision(self): """Call qpush with no options""" self._qpushRevision(move=False, exact=False) def qpushExactRevision(self): """Call qpush using the exact flag""" self._qpushRevision(exact=True) def qpushMoveRevision(self): """Make REV the top applied patch""" self._qpushRevision(move=True) def runCustomCommand(self, command, showoutput=False, workingdir='', files=None): """Execute 'custom commands', on the selected repository""" # Perform variable expansion # This is done in two steps: # 1. Expand environment variables command = os.path.expandvars(command).strip() if not command: InfoMsgBox(_('Invalid command'), _('The selected command is empty')) return if workingdir: workingdir = os.path.expandvars(workingdir).strip() # 2. Expand internal workbench variables def filelist2str(filelist): return ' '.join(procutil.shellquote( os.path.normpath(self.repo.wjoin(filename))) for filename in filelist) if files is None: files = [] selection = self.repoview.selectedRevisions() def selectionfiles2str(source): files = set() for rev in selection: files.update(f for f in getattr(self.repo[rev], source)()) return filelist2str(sorted(files)) vars = { 'ROOT': lambda: self.repo.root, 'REVID': lambda: '+'.join(str(self.repo[rev]) for rev in selection), 'REV': lambda: '+'.join(str(rev) for rev in selection), 'FILES': lambda: selectionfiles2str('files'), 'ALLFILES': lambda: selectionfiles2str('manifest'), 'SELECTEDFILES': lambda: filelist2str(files), } if len(selection) == 2: pairvars = { 'REV_A': lambda: selection[0], 'REV_B': lambda: selection[1], 'REVID_A': lambda: str(self.repo[selection[0]]), 'REVID_B': lambda: str(self.repo[selection[1]]), } vars.update(pairvars) for var in vars: bracedvar = '{%s}' % var if bracedvar in command: command = command.replace(bracedvar, str(vars[var]())) if workingdir and bracedvar in workingdir: workingdir = workingdir.replace(bracedvar, str(vars[var]())) if not workingdir: workingdir = self.repo.root # Show the Output Log if configured to do so if showoutput: self.makeLogVisible.emit(True) # If the user wants to run mercurial, # do so via our usual runCommand method cmd = shlex.split(command) cmdtype = cmd[0].lower() if cmdtype == 'hg': sess = self._runCommand(pycompat.maplist(hglib.tounicode, cmd[1:])) sess.commandFinished.connect(self._notifyWorkingDirChanges) return elif cmdtype == 'thg': cmd = cmd[1:] if '--repository' in cmd: _ui = hglib.loadui() else: cmd += ['--repository', self.repo.root] _ui = self.repo.ui.copy() _ui.ferr = pycompat.bytesio() # avoid circular import of hgqt.run by importing it inplace from . import run res = run.dispatch(cmd, u=_ui) if res: errormsg = _ui.ferr.getvalue().strip() if errormsg: errormsg = \ _('The following error message was returned:' '\n\n%s') % hglib.tounicode(errormsg) errormsg +=\ _('\n\nPlease check that the "thg" command is valid.') qtlib.ErrorMsgBox( _('Failed to execute custom TortoiseHg command'), _('The command "%s" failed (code %d).') % (hglib.tounicode(command), res), errormsg) return res # Otherwise, run the selected command in the background try: res = subprocess.Popen(command, cwd=workingdir, shell=True) except OSError as ex: res = 1 qtlib.ErrorMsgBox(_('Failed to execute custom command'), _('The command "%s" could not be executed.') % hglib.tounicode(command), _('The following error message was returned:\n\n"%s"\n\n' 'Please check that the command path is valid and ' 'that it is a valid application') % hglib.tounicode(ex.strerror)) return res @pyqtSlot(QAction) def _runCustomCommandByMenu(self, action): command, showoutput, workingdir = action.data() self.runCustomCommand(command, showoutput, workingdir) @pyqtSlot(str, list) def handleRunCustomCommandRequest(self, toolname, files): tools, toollist = hglib.tortoisehgtools(self.repo.ui) if not tools or toolname not in toollist: return toolname = str(toolname) command = tools[toolname].get('command', '') showoutput = tools[toolname].get('showoutput', False) workingdir = tools[toolname].get('workingdir', '') self.runCustomCommand(command, showoutput, workingdir, files) def _runCommand(self, cmdline): sess = self._repoagent.runCommand(cmdline, self) self._handleNewCommand(sess) return sess def _runCommandSequence(self, cmdlines): sess = self._repoagent.runCommandSequence(cmdlines, self) self._handleNewCommand(sess) return sess def _handleNewCommand(self, sess): self.clearInfoBar() sess.outputReceived.connect(self._repoviewFrame.showOutput) @pyqtSlot() def _notifyWorkingDirChanges(self): shlib.shell_notify([self.repo.root]) @pyqtSlot() def _refreshCommitTabIfNeeded(self): """Refresh the Commit tab if the user settings require it""" if self.taskTabsWidget.currentIndex() != self._namedTabs['commit']: return refreshwd = self._repoagent.configString( 'tortoisehg', 'refreshwdstatus') # Valid refreshwd values are 'auto', 'always' and 'alwayslocal' if refreshwd != 'auto': if refreshwd == 'always' \ or paths.is_on_fixed_drive(self.repo.root): self.commitDemand.forward('refreshWctx') class LightRepoWindow(QMainWindow): def __init__(self, repoagent): super(LightRepoWindow, self).__init__() self._repoagent = repoagent self.setIconSize(qtlib.smallIconSize()) repo = repoagent.rawRepo() val = repo.ui.config(b'tortoisehg', b'tasktabs').lower() if val not in (b'east', b'west'): repo.ui.setconfig(b'tortoisehg', b'tasktabs', b'east') rw = RepoWidget(repoagent, self) self.setCentralWidget(rw) self._edittbar = tbar = self.addToolBar(_('&Edit Toolbar')) tbar.setObjectName('edittbar') a = tbar.addAction(qtlib.geticon('view-refresh'), _('&Refresh')) a.setShortcuts(QKeySequence.Refresh) a.triggered.connect(self.refresh) tbar = rw.filterBar() tbar.setObjectName('filterbar') tbar.setWindowTitle(_('&Filter Toolbar')) self.addToolBar(tbar) stbar = cmdui.ThgStatusBar(self) repoagent.progressReceived.connect(stbar.setProgress) rw.showMessageSignal.connect(stbar.showMessage) rw.progress.connect(stbar.progress) self.setStatusBar(stbar) s = QSettings() s.beginGroup('LightRepoWindow') self.restoreGeometry(qtlib.readByteArray(s, 'geometry')) self.restoreState(qtlib.readByteArray(s, 'windowState')) stbar.setVisible(qtlib.readBool(s, 'statusBar', True)) s.endGroup() self.setWindowTitle(_('TortoiseHg: %s') % repoagent.displayName()) def createPopupMenu(self): menu = super(LightRepoWindow, self).createPopupMenu() assert menu # should have toolbar stbar = self.statusBar() a = menu.addAction(_('S&tatus Bar')) a.setCheckable(True) a.setChecked(stbar.isVisibleTo(self)) a.triggered.connect(stbar.setVisible) menu.addSeparator() menu.addAction(_('&Settings'), self._editSettings) return menu def closeEvent(self, event): rw = self.centralWidget() if not rw.closeRepoWidget(): event.ignore() return s = QSettings() s.beginGroup('LightRepoWindow') s.setValue('geometry', self.saveGeometry()) s.setValue('windowState', self.saveState()) s.setValue('statusBar', self.statusBar().isVisibleTo(self)) s.endGroup() event.accept() @pyqtSlot() def refresh(self): self._repoagent.pollStatus() rw = self.centralWidget() rw.reload() def setSyncUrl(self, url): rw = self.centralWidget() rw.setSyncUrl(url) @pyqtSlot() def _editSettings(self): dlg = settings.SettingsDialog(parent=self) dlg.exec_()