1# repowidget.py - TortoiseHg repository widget 2# 3# Copyright (C) 2007-2010 Logilab. All rights reserved. 4# Copyright (C) 2010 Adrian Buehlmann <adrian@cadifra.com> 5# 6# This software may be used and distributed according to the terms 7# of the GNU General Public License, incorporated herein by reference. 8 9from __future__ import absolute_import 10 11import binascii 12import os 13import shlex # used by runCustomCommand 14import subprocess # used by runCustomCommand 15 16from .qtcore import ( 17 QFile, 18 QIODevice, 19 QItemSelectionModel, 20 QMimeData, 21 QPoint, 22 QSettings, 23 QTimer, 24 QUrl, 25 Qt, 26 pyqtSignal, 27 pyqtSlot, 28) 29from .qtgui import ( 30 QAction, 31 QApplication, 32 QDesktopServices, 33 QFileDialog, 34 QIcon, 35 QKeySequence, 36 QMainWindow, 37 QMenu, 38 QMessageBox, 39 QSplitter, 40 QTabWidget, 41 QVBoxLayout, 42 QWidget, 43) 44 45from mercurial import ( 46 error, 47 node as nodemod, 48 phases, 49 pycompat, 50 scmutil, 51) 52from mercurial.utils import ( 53 procutil, 54) 55 56from ..util import ( 57 hglib, 58 paths, 59 shlib, 60) 61from ..util.i18n import _ 62from . import ( 63 archive, 64 backout, 65 bisect, 66 bookmark, 67 close_branch, 68 cmdcore, 69 cmdui, 70 compress, 71 graft, 72 hgemail, 73 infobar, 74 matching, 75 merge, 76 mq, 77 pick, 78 phabreview, 79 postreview, 80 prune, 81 purge, 82 qtlib, 83 rebase, 84 repomodel, 85 resolve, 86 revdetails, 87 settings, 88 shelve, 89 sign, 90 tag, 91 thgimport, 92 thgstrip, 93 topic, 94 update, 95 visdiff, 96) 97from .commit import CommitWidget 98from .docklog import ConsoleWidget 99from .grep import SearchWidget 100from .qtlib import ( 101 DemandWidget, 102 InfoMsgBox, 103 QuestionMsgBox, 104 WarningMsgBox, 105) 106from .repofilter import RepoFilterBar 107from .repoview import HgRepoView 108from .sync import SyncWidget 109 110if hglib.TYPE_CHECKING: 111 from typing import ( 112 Callable, 113 Dict, 114 List, 115 Optional, 116 Sequence, 117 Set, 118 Text, 119 Tuple, 120 Union, 121 ) 122 123 124_SELECTION_SINGLE = 'single' 125_SELECTION_PAIR = 'pair' 126_SELECTION_SOME = 'some' 127 128_SELECTION_INCOMING = 'incoming' 129_SELECTION_OUTGOING = 'outgoing' 130 131# iswd = working directory 132# isrev = the changeset has an integer revision number 133# isctx = changectx or workingctx 134# ispatch = applied revision or unapplied patch 135# fixed = the changeset is considered permanent 136# applied = an applied patch 137# unapplied = unapplied patch 138# qfold = unapplied patch and at least one applied patch exists 139# qgoto = applied patch or qparent 140# qpush = unapplied patch and can qpush 141# qpushmove = unapplied patch and can qpush --move to reorder patches 142# isdraftorwd = working directory or changset is draft 143_SELECTION_ISREV = 'isrev' 144_SELECTION_ISWD = 'iswd' 145_SELECTION_ISCTX = 'isctx' 146_SELECTION_ISPATCH = 'ispatch' 147_SELECTION_FIXED = 'fixed' 148_SELECTION_APPLIED = 'applied' 149_SELECTION_UNAPPLIED = 'unapplied' 150_SELECTION_QFOLD = 'qfold' 151_SELECTION_QGOTO = 'qgoto' 152_SELECTION_QPUSH = 'qpush' 153_SELECTION_QPUSHMOVE = 'qpushmove' 154_SELECTION_ISTRUE = 'istrue' 155_SELECTION_ISDRAFTORWD = 'isdraftorwd' 156 157_KNOWN_SELECTION_ATTRS = { 158 _SELECTION_SINGLE, 159 _SELECTION_PAIR, 160 _SELECTION_SOME, 161 162 _SELECTION_INCOMING, 163 _SELECTION_OUTGOING, 164 165 _SELECTION_ISREV, 166 _SELECTION_ISWD, 167 _SELECTION_ISCTX, 168 _SELECTION_ISPATCH, 169 _SELECTION_FIXED, 170 _SELECTION_APPLIED, 171 _SELECTION_UNAPPLIED, 172 _SELECTION_QFOLD, 173 _SELECTION_QGOTO, 174 _SELECTION_QPUSH, 175 _SELECTION_QPUSHMOVE, 176 _SELECTION_ISTRUE, 177 _SELECTION_ISDRAFTORWD, 178} # type: Set[Text] 179 180# selection attributes which may be specified by user 181_CUSTOM_TOOLS_SELECTION_ATTRS = { 182 _SELECTION_ISREV, 183 _SELECTION_ISWD, 184 _SELECTION_ISCTX, 185 _SELECTION_FIXED, 186 _SELECTION_APPLIED, 187 _SELECTION_QGOTO, 188 _SELECTION_ISTRUE, 189 _SELECTION_ISDRAFTORWD, 190} # type: Set[Text] 191 192 193class RepoWidget(QWidget): 194 195 currentTaskTabChanged = pyqtSignal() 196 showMessageSignal = pyqtSignal(str) 197 taskTabVisibilityChanged = pyqtSignal(bool) 198 toolbarVisibilityChanged = pyqtSignal(bool) 199 200 # TODO: progress can be removed if all actions are run as hg command 201 progress = pyqtSignal(str, object, str, str, object) 202 makeLogVisible = pyqtSignal(bool) 203 204 revisionSelected = pyqtSignal(object) 205 206 titleChanged = pyqtSignal(str) 207 """Emitted when changed the expected title for the RepoWidget tab""" 208 209 busyIconChanged = pyqtSignal() 210 211 repoLinkClicked = pyqtSignal(str) 212 """Emitted when clicked a link to open repository""" 213 214 def __init__(self, actionregistry, repoagent, parent=None, bundle=None): 215 QWidget.__init__(self, parent, acceptDrops=True) 216 217 self._actionregistry = actionregistry 218 self._repoagent = repoagent 219 self.bundlesource = None # source URL of incoming bundle [unicode] 220 self.outgoingMode = False 221 self._busyIconNames = [] 222 self._namedTabs = {} 223 self.destroyed.connect(self.repo.thginvalidate) 224 225 self.currentMessage = '' 226 227 self.setupUi() 228 self._actions = {} # type: Dict[Text, Tuple[QAction, Set[Text], Set[Text]]] 229 self.createActions() 230 self.loadSettings() 231 self._initModel() 232 233 self._lastTaskTabVisible = self.isTaskTabVisible() 234 self.repotabs_splitter.splitterMoved.connect(self._onSplitterMoved) 235 236 if bundle: 237 self.setBundle(bundle) 238 239 self._dialogs = qtlib.DialogKeeper( 240 lambda self, dlgmeth, *args: dlgmeth(self, *args), parent=self) 241 242 # listen to change notification after initial settings are loaded 243 repoagent.repositoryChanged.connect(self.repositoryChanged) 244 repoagent.configChanged.connect(self.configChanged) 245 246 self._updateNamedActions() 247 QTimer.singleShot(0, self._initView) 248 249 def setupUi(self): 250 self.repotabs_splitter = QSplitter(orientation=Qt.Vertical) 251 self.setLayout(QVBoxLayout()) 252 self.layout().setContentsMargins(0, 0, 0, 0) 253 self.layout().setSpacing(0) 254 255 # placeholder to shift repoview while infobar is overlaid 256 self._repoviewFrame = infobar.InfoBarPlaceholder(self._repoagent, self) 257 self._repoviewFrame.linkActivated.connect(self._openLink) 258 259 self.filterbar = RepoFilterBar(self._repoagent, self) 260 self.layout().addWidget(self.filterbar) 261 262 self.filterbar.branchChanged.connect(self.setBranch) 263 self.filterbar.showHiddenChanged.connect(self.setShowHidden) 264 self.filterbar.showGraftSourceChanged.connect(self.setShowGraftSource) 265 self.filterbar.setRevisionSet.connect(self.setRevisionSet) 266 self.filterbar.filterToggled.connect(self.filterToggled) 267 self.filterbar.visibilityChanged.connect(self.toolbarVisibilityChanged) 268 self.filterbar.hide() 269 270 self.layout().addWidget(self.repotabs_splitter) 271 272 cs = ('Workbench', _('Workbench Log Columns')) 273 self.repoview = view = HgRepoView(self._repoagent, 'repoWidget', cs, 274 self) 275 view.clicked.connect(self._clearInfoMessage) 276 view.revisionSelected.connect(self.onRevisionSelected) 277 view.revisionActivated.connect(self.onRevisionActivated) 278 view.showMessage.connect(self.showMessage) 279 view.menuRequested.connect(self._popupSelectionMenu) 280 self._repoviewFrame.setView(view) 281 282 self.repotabs_splitter.addWidget(self._repoviewFrame) 283 self.repotabs_splitter.setCollapsible(0, True) 284 self.repotabs_splitter.setStretchFactor(0, 1) 285 286 self.taskTabsWidget = tt = QTabWidget() 287 self.repotabs_splitter.addWidget(self.taskTabsWidget) 288 self.repotabs_splitter.setStretchFactor(1, 1) 289 tt.setDocumentMode(True) 290 self.updateTaskTabs() 291 tt.currentChanged.connect(self.currentTaskTabChanged) 292 293 w = revdetails.RevDetailsWidget(self._repoagent, self) 294 self.revDetailsWidget = w 295 self.revDetailsWidget.filelisttbar.setStyleSheet(qtlib.tbstylesheet) 296 w.linkActivated.connect(self._openLink) 297 w.revisionSelected.connect(self.repoview.goto) 298 w.grepRequested.connect(self.grep) 299 w.showMessage.connect(self.showMessage) 300 w.revsetFilterRequested.connect(self.setFilter) 301 w.runCustomCommandRequested.connect( 302 self.handleRunCustomCommandRequest) 303 idx = tt.addTab(w, qtlib.geticon('hg-log'), '') 304 self._namedTabs['log'] = idx 305 tt.setTabToolTip(idx, _("Revision details", "tab tooltip")) 306 307 self.commitDemand = w = DemandWidget('createCommitWidget', self) 308 idx = tt.addTab(w, qtlib.geticon('hg-commit'), '') 309 self._namedTabs['commit'] = idx 310 tt.setTabToolTip(idx, _("Commit", "tab tooltip")) 311 312 self.grepDemand = w = DemandWidget('createGrepWidget', self) 313 idx = tt.addTab(w, qtlib.geticon('hg-grep'), '') 314 self._namedTabs['grep'] = idx 315 tt.setTabToolTip(idx, _("Search", "tab tooltip")) 316 317 w = ConsoleWidget(self._repoagent, self) 318 self.consoleWidget = w 319 w.closeRequested.connect(self.switchToPreferredTaskTab) 320 idx = tt.addTab(w, qtlib.geticon('thg-console'), '') 321 self._namedTabs['console'] = idx 322 tt.setTabToolTip(idx, _("Console log", "tab tooltip")) 323 324 self.syncDemand = w = DemandWidget('createSyncWidget', self) 325 idx = tt.addTab(w, qtlib.geticon('thg-sync'), '') 326 self._namedTabs['sync'] = idx 327 tt.setTabToolTip(idx, _("Synchronize", "tab tooltip")) 328 329 @pyqtSlot() 330 def _initView(self): 331 self._updateRepoViewForModel() 332 # restore column widths when model is initially loaded. For some 333 # reason, this needs to be deferred after updating the view. Otherwise 334 # repoview.HgRepoView.resizeEvent() fires as the vertical scrollbar is 335 # added, which causes the last column to grow by the scrollbar width on 336 # each restart (and steal from the description width). 337 QTimer.singleShot(0, self.repoview.resizeColumns) 338 339 # select the widget chosen by the user 340 name = self._repoagent.configString('tortoisehg', 'defaultwidget') 341 if name: 342 name = {'revdetails': 'log', 'search': 'grep'}.get(name, name) 343 self.taskTabsWidget.setCurrentIndex(self._namedTabs.get(name, 0)) 344 345 def currentTaskTabName(self): 346 indexmap = dict((idx, name) 347 for name, idx in self._namedTabs.items()) 348 return indexmap.get(self.taskTabsWidget.currentIndex()) 349 350 @pyqtSlot(str) 351 def switchToNamedTaskTab(self, tabname): 352 tabname = str(tabname) 353 if tabname in self._namedTabs: 354 idx = self._namedTabs[tabname] 355 # refresh status even if current widget is already a 'commit' 356 if (tabname == 'commit' 357 and self.taskTabsWidget.currentIndex() == idx): 358 self._refreshCommitTabIfNeeded() 359 self.taskTabsWidget.setCurrentIndex(idx) 360 361 # restore default splitter position if task tab is invisible 362 self.setTaskTabVisible(True) 363 364 def isTaskTabVisible(self): 365 return self.repotabs_splitter.sizes()[1] > 0 366 367 def setTaskTabVisible(self, visible): 368 if visible == self.isTaskTabVisible(): 369 return 370 if visible: 371 self.repotabs_splitter.setSizes([1, 1]) 372 else: 373 self.repotabs_splitter.setSizes([1, 0]) 374 self._updateLastTaskTabState(visible) 375 376 @pyqtSlot() 377 def _onSplitterMoved(self): 378 visible = self.isTaskTabVisible() 379 if self._lastTaskTabVisible == visible: 380 return 381 self._updateLastTaskTabState(visible) 382 383 def _updateLastTaskTabState(self, visible): 384 self._lastTaskTabVisible = visible 385 self.taskTabVisibilityChanged.emit(visible) 386 387 @property 388 def repo(self): 389 return self._repoagent.rawRepo() 390 391 def repoRootPath(self): 392 return self._repoagent.rootPath() 393 394 def repoDisplayName(self): 395 return self._repoagent.displayName() 396 397 def title(self): 398 """Returns the expected title for this widget [unicode]""" 399 name = self._repoagent.shortName() 400 if self._repoagent.overlayUrl(): 401 return _('%s <incoming>') % name 402 elif self.repomodel.branch(): 403 return u'%s [%s]' % (name, self.repomodel.branch()) 404 else: 405 return name 406 407 def busyIcon(self): 408 if self._busyIconNames: 409 return qtlib.geticon(self._busyIconNames[-1]) 410 else: 411 return QIcon() 412 413 def filterBar(self): 414 return self.filterbar 415 416 def filterBarVisible(self): 417 return self.filterbar.isVisible() 418 419 @pyqtSlot(bool) 420 def toggleFilterBar(self, checked): 421 """Toggle display repowidget filter bar""" 422 if self.filterbar.isVisibleTo(self) == checked: 423 return 424 self.filterbar.setVisible(checked) 425 if checked: 426 self.filterbar.setFocus() 427 428 def _openRepoLink(self, upath): 429 path = hglib.fromunicode(upath) 430 if not os.path.isabs(path): 431 path = self.repo.wjoin(path) 432 self.repoLinkClicked.emit(hglib.tounicode(path)) 433 434 @pyqtSlot(str) 435 def _openLink(self, link): 436 link = pycompat.unicode(link) 437 handlers = {'cset': self.goto, 438 'log': lambda a: self.makeLogVisible.emit(True), 439 'repo': self._openRepoLink, 440 'shelve' : self.shelve} 441 if ':' in link: 442 scheme, param = link.split(':', 1) 443 hdr = handlers.get(scheme) 444 if hdr: 445 return hdr(param) 446 if os.path.isabs(link): 447 qtlib.openlocalurl(link) 448 else: 449 QDesktopServices.openUrl(QUrl(link)) 450 451 def setInfoBar(self, cls, *args, **kwargs): 452 return self._repoviewFrame.setInfoBar(cls, *args, **kwargs) 453 454 def clearInfoBar(self, priority=None): 455 return self._repoviewFrame.clearInfoBar(priority) 456 457 def createCommitWidget(self): 458 pats = [] 459 opts = {} 460 cw = CommitWidget(self._repoagent, pats, opts, self, rev=self.rev) 461 cw.buttonHBox.addWidget(cw.commitSetupButton()) 462 cw.loadSettings(QSettings(), 'Workbench') 463 464 cw.progress.connect(self.progress) 465 cw.linkActivated.connect(self._openLink) 466 cw.showMessage.connect(self.showMessage) 467 cw.grepRequested.connect(self.grep) 468 cw.runCustomCommandRequested.connect( 469 self.handleRunCustomCommandRequest) 470 QTimer.singleShot(0, self._initCommitWidgetLate) 471 return cw 472 473 @pyqtSlot() 474 def _initCommitWidgetLate(self): 475 cw = self.commitDemand.get() 476 cw.reload() 477 # auto-refresh should be enabled after initial reload(); otherwise 478 # refreshWctx() can be doubled 479 self.taskTabsWidget.currentChanged.connect( 480 self._refreshCommitTabIfNeeded) 481 482 def createSyncWidget(self): 483 sw = SyncWidget(self._repoagent, self) 484 sw.newCommand.connect(self._handleNewSyncCommand) 485 sw.outgoingNodes.connect(self.setOutgoingNodes) 486 sw.showMessage.connect(self.showMessage) 487 sw.showMessage.connect(self._repoviewFrame.showMessage) 488 sw.incomingBundle.connect(self.setBundle) 489 sw.pullCompleted.connect(self.onPullCompleted) 490 sw.pushCompleted.connect(self.clearRevisionSet) 491 sw.refreshTargets(self.rev) 492 sw.switchToRequest.connect(self.switchToNamedTaskTab) 493 return sw 494 495 @pyqtSlot(cmdcore.CmdSession) 496 def _handleNewSyncCommand(self, sess): 497 self._handleNewCommand(sess) 498 if sess.isFinished(): 499 return 500 sess.commandFinished.connect(self._onSyncCommandFinished) 501 self._setBusyIcon('thg-sync') 502 503 @pyqtSlot() 504 def _onSyncCommandFinished(self): 505 self._clearBusyIcon('thg-sync') 506 507 def _setBusyIcon(self, iconname): 508 self._busyIconNames.append(iconname) 509 self.busyIconChanged.emit() 510 511 def _clearBusyIcon(self, iconname): 512 if iconname in self._busyIconNames: 513 self._busyIconNames.remove(iconname) 514 self.busyIconChanged.emit() 515 516 @pyqtSlot(str) 517 def setFilter(self, filter): 518 self.filterbar.setQuery(filter) 519 self.filterbar.setVisible(True) 520 self.filterbar.runQuery() 521 522 def isBundleSet(self): 523 # type: () -> bool 524 return (bool(self._repoagent.overlayUrl()) 525 and self.repomodel.revset() == 'bundle()') 526 527 @pyqtSlot(str, str) 528 def setBundle(self, bfile, bsource=None): 529 if self._repoagent.overlayUrl(): 530 self.clearBundle() 531 self.bundlesource = bsource and pycompat.unicode(bsource) or None 532 oldlen = len(self.repo) 533 # no "bundle:<bfile>" because bfile may contain "+" separator 534 self._repoagent.setOverlay(bfile) 535 self.filterbar.setQuery('bundle()') 536 self.filterbar.runQuery() 537 self.titleChanged.emit(self.title()) 538 newlen = len(self.repo) 539 540 w = self.setInfoBar(infobar.ConfirmInfoBar, 541 _('Found %d incoming changesets') % (newlen - oldlen)) 542 assert w 543 w.acceptButton.setText(_('Pull')) 544 w.acceptButton.setToolTip(_('Pull incoming changesets into ' 545 'your repository')) 546 w.rejectButton.setText(_('Cancel')) 547 w.rejectButton.setToolTip(_('Reject incoming changesets')) 548 w.accepted.connect(self.acceptBundle) 549 w.rejected.connect(self.clearBundle) 550 551 @pyqtSlot() 552 def clearBundle(self): 553 self.clearRevisionSet() 554 self.bundlesource = None 555 self._repoagent.clearOverlay() 556 self.titleChanged.emit(self.title()) 557 558 @pyqtSlot() 559 def onPullCompleted(self): 560 if self._repoagent.overlayUrl(): 561 self.clearBundle() 562 563 @pyqtSlot() 564 def acceptBundle(self): 565 bundle = self._repoagent.overlayUrl() 566 if bundle: 567 w = self.syncDemand.get() 568 w.pullBundle(bundle, None, self.bundlesource) 569 570 @pyqtSlot() 571 def pullBundleToRev(self): 572 bundle = self._repoagent.overlayUrl() 573 if bundle: 574 # manually remove infobar to work around unwanted clearBundle 575 # during pull operation (issue #2596) 576 self._repoviewFrame.discardInfoBar() 577 578 w = self.syncDemand.get() 579 w.pullBundle(bundle, self.repo[self.rev].hex(), self.bundlesource) 580 581 @pyqtSlot() 582 def clearRevisionSet(self): 583 self.filterbar.setQuery('') 584 self.setRevisionSet('') 585 586 def setRevisionSet(self, revspec): 587 self.repomodel.setRevset(revspec) 588 if not revspec and self.outgoingMode: 589 self.outgoingMode = False 590 self._updateNamedActions() 591 592 @pyqtSlot(bool) 593 def filterToggled(self, checked): 594 self.repomodel.setFilterByRevset(checked) 595 596 def setOutgoingNodes(self, nodes): 597 self.filterbar.setQuery('outgoing()') 598 revs = [self.repo[n].rev() for n in nodes] 599 self.setRevisionSet(hglib.compactrevs(revs)) 600 self.outgoingMode = True 601 numnodes = len(nodes) 602 numoutgoing = numnodes 603 604 if self.syncDemand.get().isTargetSelected(): 605 # Outgoing preview is already filtered by target selection 606 defaultpush = None 607 else: 608 # Read the tortoisehg.defaultpush setting to determine what to push 609 # by default, and set the button label and action accordingly 610 defaultpush = self._repoagent.configString( 611 'tortoisehg', 'defaultpush') 612 rev = None 613 branch = None 614 pushall = False 615 # note that we assume that none of the revisions 616 # on the nodes/revs lists is secret 617 if defaultpush == 'branch': 618 branch = self.repo[b'.'].branch() 619 ubranch = hglib.tounicode(branch) 620 # Get the list of revs that will be actually pushed 621 outgoingrevs = self.repo.revs(b'%ld and branch(.)', revs) 622 numoutgoing = len(outgoingrevs) 623 elif defaultpush == 'revision': 624 rev = self.repo[b'.'].rev() 625 # Get the list of revs that will be actually pushed 626 # excluding (potentially) the current rev 627 outgoingrevs = self.repo.revs(b'%ld and ::.', revs) 628 numoutgoing = len(outgoingrevs) 629 maxrev = rev 630 if numoutgoing > 0: 631 maxrev = max(outgoingrevs) 632 else: 633 pushall = True 634 635 # Set the default acceptbuttontext 636 # Note that the pushall case uses the default accept button text 637 if branch is not None: 638 acceptbuttontext = _('Push current branch (%s)') % ubranch 639 elif rev is not None: 640 if maxrev == rev: 641 acceptbuttontext = _('Push up to current revision (#%d)') % rev 642 else: 643 acceptbuttontext = _('Push up to revision #%d') % maxrev 644 else: 645 acceptbuttontext = _('Push all') 646 647 if numnodes == 0: 648 msg = _('no outgoing changesets') 649 elif numoutgoing == 0: 650 if branch: 651 msg = _('no outgoing changesets in current branch (%s) ' 652 '/ %d in total') % (ubranch, numnodes) 653 elif rev is not None: 654 if maxrev == rev: 655 msg = _('no outgoing changesets up to current revision ' 656 '(#%d) / %d in total') % (rev, numnodes) 657 else: 658 msg = _('no outgoing changesets up to revision #%d ' 659 '/ %d in total') % (maxrev, numnodes) 660 elif numoutgoing == numnodes: 661 # This case includes 'Push all' among others 662 msg = _('%d outgoing changesets') % numoutgoing 663 elif branch: 664 msg = _('%d outgoing changesets in current branch (%s) ' 665 '/ %d in total') % (numoutgoing, ubranch, numnodes) 666 elif rev: 667 if maxrev == rev: 668 msg = _('%d outgoing changesets up to current revision (#%d) ' 669 '/ %d in total') % (numoutgoing, rev, numnodes) 670 else: 671 msg = _('%d outgoing changesets up to revision #%d ' 672 '/ %d in total') % (numoutgoing, maxrev, numnodes) 673 else: 674 # This should never happen but we leave this else clause 675 # in case there is a flaw in the logic above (e.g. due to 676 # a future change in the code) 677 msg = _('%d outgoing changesets') % numoutgoing 678 679 w = self.setInfoBar(infobar.ConfirmInfoBar, msg.strip()) 680 assert w 681 682 if numoutgoing == 0: 683 acceptbuttontext = _('Nothing to push') 684 w.acceptButton.setEnabled(False) 685 w.acceptButton.setText(acceptbuttontext) 686 w.accepted.connect(lambda: self.push(False, 687 rev=rev, branch=branch, pushall=pushall)) # TODO: to the same URL 688 w.rejected.connect(self.clearRevisionSet) 689 self._updateNamedActions() 690 691 def createGrepWidget(self): 692 upats = {} 693 gw = SearchWidget(self._repoagent, upats, self) 694 gw.setRevision(self.repoview.current_rev) 695 gw.showMessage.connect(self.showMessage) 696 gw.progress.connect(self.progress) 697 gw.revisionSelected.connect(self.goto) 698 return gw 699 700 @property 701 def rev(self): 702 """Returns the current active revision""" 703 return self.repoview.current_rev 704 705 def gotoRev(self, revspec): 706 """Select and scroll to the specified revision""" 707 try: 708 # try instant look up 709 if scmutil.isrevsymbol(self.repo, hglib.fromunicode(revspec)): 710 self.repoview.goto(revspec) 711 return 712 except error.LookupError: 713 pass # ambiguous node 714 715 cmdline = hglib.buildcmdargs('log', rev=revspec, template='{rev}\n') 716 sess = self._runCommand(cmdline) 717 sess.setCaptureOutput(True) 718 sess.commandFinished.connect(self._onGotoRevQueryFinished) 719 720 @pyqtSlot(int) 721 def _onGotoRevQueryFinished(self, ret): 722 sess = self.sender() 723 if ret != 0: 724 return 725 output = bytes(sess.readAll()) 726 if not output: 727 # TODO: maybe this should be a warning bar since there would be no 728 # information in log window. 729 self.setInfoBar(infobar.CommandErrorInfoBar, _('No revision found')) 730 return 731 rev = int(output.splitlines()[-1]) # pick last rev as "hg update" does 732 self.repoview.goto(rev) 733 734 def showMessage(self, msg): 735 self.currentMessage = msg 736 if self.isVisible(): 737 self.showMessageSignal.emit(msg) 738 739 def keyPressEvent(self, event): 740 if self._repoviewFrame.activeInfoBar() and event.key() == Qt.Key_Escape: 741 self.clearInfoBar(infobar.INFO) 742 else: 743 QWidget.keyPressEvent(self, event) 744 745 def showEvent(self, event): 746 QWidget.showEvent(self, event) 747 self.showMessageSignal.emit(self.currentMessage) 748 if not event.spontaneous(): 749 # RepoWidget must be the main widget in any window, so grab focus 750 # when it gets visible at start-up or by switching tabs. 751 self.repoview.setFocus() 752 753 def createActions(self): 754 self._mqActions = None 755 if b'mq' in self.repo.extensions(): 756 self._mqActions = mq.PatchQueueActions(self) 757 self._mqActions.setRepoAgent(self._repoagent) 758 759 self._setUpNamedActions() 760 761 def detectPatches(self, paths): 762 filepaths = [] 763 for p in paths: 764 if not os.path.isfile(p): 765 continue 766 try: 767 pf = open(p, 'rb') 768 earlybytes = pf.read(4096) 769 if b'\0' in earlybytes: 770 continue 771 pf.seek(0) 772 with hglib.extractpatch(self.repo.ui, pf) as data: 773 if data.get('filename'): 774 filepaths.append(p) 775 except EnvironmentError: 776 pass 777 return filepaths 778 779 def dragEnterEvent(self, event): 780 paths = [pycompat.unicode(u.toLocalFile()) for u in event.mimeData().urls()] 781 if self.detectPatches(paths): 782 event.setDropAction(Qt.CopyAction) 783 event.accept() 784 785 def dropEvent(self, event): 786 paths = [pycompat.unicode(u.toLocalFile()) for u in event.mimeData().urls()] 787 patches = self.detectPatches(paths) 788 if not patches: 789 return 790 event.setDropAction(Qt.CopyAction) 791 event.accept() 792 self.thgimport(patches) 793 794 ## Begin Workbench event forwards 795 796 def back(self): 797 self.repoview.back() 798 799 def forward(self): 800 self.repoview.forward() 801 802 def bisect(self): 803 self._dialogs.open(RepoWidget._createBisectDialog) 804 805 @pyqtSlot() 806 def bisectGoodBadRevisionsPair(self): 807 revA, revB = self._selectedIntRevisionsPair() 808 dlg = self._dialogs.open(RepoWidget._createBisectDialog) 809 dlg.restart(str(revA), str(revB)) 810 811 @pyqtSlot() 812 def bisectBadGoodRevisionsPair(self): 813 revA, revB = self._selectedIntRevisionsPair() 814 dlg = self._dialogs.open(RepoWidget._createBisectDialog) 815 dlg.restart(str(revB), str(revA)) 816 817 def _createBisectDialog(self): 818 dlg = bisect.BisectDialog(self._repoagent, self) 819 dlg.newCandidate.connect(self.gotoParent) 820 return dlg 821 822 def resolve(self): 823 dlg = resolve.ResolveDialog(self._repoagent, self) 824 dlg.exec_() 825 826 def thgimport(self, paths=None): 827 dlg = thgimport.ImportDialog(self._repoagent, self) 828 if paths: 829 dlg.setfilepaths(paths) 830 if dlg.exec_() == 0: 831 self.gotoTip() 832 833 def unbundle(self): 834 w = self.syncDemand.get() 835 w.unbundle() 836 837 def shelve(self, arg=None): 838 self._dialogs.open(RepoWidget._createShelveDialog) 839 840 def _createShelveDialog(self): 841 dlg = shelve.ShelveDialog(self._repoagent) 842 dlg.finished.connect(self._refreshCommitTabIfNeeded) 843 return dlg 844 845 def verify(self): 846 cmdline = ['verify', '--verbose'] 847 dlg = cmdui.CmdSessionDialog(self) 848 dlg.setWindowIcon(qtlib.geticon('hg-verify')) 849 dlg.setWindowTitle(_('%s - verify repository') % self.repoDisplayName()) 850 dlg.setWindowFlags(dlg.windowFlags() | Qt.WindowMaximizeButtonHint) 851 dlg.setSession(self._repoagent.runCommand(cmdline, self)) 852 dlg.exec_() 853 854 def recover(self): 855 cmdline = ['recover', '--verbose'] 856 dlg = cmdui.CmdSessionDialog(self) 857 dlg.setWindowIcon(qtlib.geticon('hg-recover')) 858 dlg.setWindowTitle(_('%s - recover repository') 859 % self.repoDisplayName()) 860 dlg.setWindowFlags(dlg.windowFlags() | Qt.WindowMaximizeButtonHint) 861 dlg.setSession(self._repoagent.runCommand(cmdline, self)) 862 dlg.exec_() 863 864 def rollback(self): 865 desc, oldlen = hglib.readundodesc(self.repo) 866 if not desc: 867 InfoMsgBox(_('No transaction available'), 868 _('There is no rollback transaction available')) 869 return 870 elif desc == 'commit': 871 if not QuestionMsgBox(_('Undo last commit?'), 872 _('Undo most recent commit (%d), preserving file changes?') % 873 oldlen): 874 return 875 else: 876 if not QuestionMsgBox(_('Undo last transaction?'), 877 _('Rollback to revision %d (undo %s)?') % 878 (oldlen - 1, desc)): 879 return 880 try: 881 rev = self.repo[b'.'].rev() 882 except error.LookupError as e: 883 InfoMsgBox(_('Repository Error'), 884 _('Unable to determine working copy revision\n') + 885 hglib.tounicode(bytes(e))) 886 return 887 if rev >= oldlen and not QuestionMsgBox( 888 _('Remove current working revision?'), 889 _('Your current working revision (%d) will be removed ' 890 'by this rollback, leaving uncommitted changes.\n ' 891 'Continue?') % rev): 892 return 893 cmdline = ['rollback', '--verbose'] 894 sess = self._runCommand(cmdline) 895 sess.commandFinished.connect(self._notifyWorkingDirChanges) 896 897 def purge(self): 898 dlg = purge.PurgeDialog(self._repoagent, self) 899 dlg.setWindowFlags(Qt.Sheet) 900 dlg.setWindowModality(Qt.WindowModal) 901 dlg.showMessage.connect(self.showMessage) 902 dlg.progress.connect(self.progress) 903 dlg.exec_() 904 # ignores result code of PurgeDialog because it's unreliable 905 self._refreshCommitTabIfNeeded() 906 907 ## End workbench event forwards 908 909 @pyqtSlot(str, dict) 910 def grep(self, pattern='', opts=None): 911 """Open grep task tab""" 912 if opts is None: 913 opts = {} 914 opts = dict((str(k), str(v)) for k, v in opts.items()) 915 self.taskTabsWidget.setCurrentIndex(self._namedTabs['grep']) 916 self.grepDemand.setSearch(pattern, **opts) 917 self.grepDemand.runSearch() 918 919 def _initModel(self): 920 self.repomodel = repomodel.HgRepoListModel(self._repoagent, self) 921 self.repomodel.setBranch(self.filterbar.branch(), 922 self.filterbar.branchAncestorsIncluded()) 923 self.repomodel.setFilterByRevset(self.filterbar.filtercb.isChecked()) 924 self.repomodel.setShowGraftSource(self.filterbar.getShowGraftSource()) 925 self.repomodel.showMessage.connect(self.showMessage) 926 self.repomodel.showMessage.connect(self._repoviewFrame.showMessage) 927 self.repoview.setModel(self.repomodel) 928 self.repomodel.revsUpdated.connect(self._updateRepoViewForModel) 929 930 selmodel = self.repoview.selectionModel() 931 assert selmodel is not None 932 selmodel.selectionChanged.connect(self._onSelectedRevisionsChanged) 933 934 @pyqtSlot() 935 def _updateRepoViewForModel(self): 936 model = self.repoview.model() 937 selmodel = self.repoview.selectionModel() 938 assert model is not None 939 assert selmodel is not None 940 index = selmodel.currentIndex() 941 if not (index.flags() & Qt.ItemIsEnabled): 942 index = model.defaultIndex() 943 f = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows 944 selmodel.setCurrentIndex(index, f) 945 self.repoview.scrollTo(index) 946 self.repoview.enablefilterpalette(bool(model.revset())) 947 self.clearInfoBar(infobar.INFO) # clear progress message 948 949 @pyqtSlot() 950 def _clearInfoMessage(self): 951 self.clearInfoBar(infobar.INFO) 952 953 @pyqtSlot() 954 def switchToPreferredTaskTab(self): 955 tw = self.taskTabsWidget 956 rev = self.rev 957 ctx = self.repo[rev] 958 if rev is None or (b'mq' in self.repo.extensions() 959 and b'qtip' in ctx.tags() 960 and self.repo[b'.'].rev() == rev): 961 # Clicking on working copy or on the topmost applied patch 962 # (_if_ it is also the working copy parent) switches to the commit tab 963 tw.setCurrentIndex(self._namedTabs['commit']) 964 else: 965 # Clicking on a normal revision switches from commit tab 966 tw.setCurrentIndex(self._namedTabs['log']) 967 968 def onRevisionSelected(self, rev): 969 'View selection changed, could be a reload' 970 self.showMessage('') 971 try: 972 self.revDetailsWidget.onRevisionSelected(rev) 973 self.revisionSelected.emit(rev) 974 if not isinstance(rev, str): 975 # Regular patch or working directory 976 self.grepDemand.forward('setRevision', rev) 977 self.syncDemand.forward('refreshTargets', rev) 978 self.commitDemand.forward('setRev', rev) 979 except (IndexError, error.RevlogError, error.Abort) as e: 980 self.showMessage(hglib.tounicode(str(e))) 981 982 cw = self.taskTabsWidget.currentWidget() 983 if cw.canswitch(): 984 self.switchToPreferredTaskTab() 985 986 @pyqtSlot() 987 def _onSelectedRevisionsChanged(self): 988 self._updateNamedActions() 989 990 @pyqtSlot() 991 def gotoParent(self): 992 self.goto('.') 993 994 def gotoTip(self): 995 self.repoview.clearSelection() 996 self.goto('tip') 997 998 def _gotoAncestor(self): 999 revs = self._selectedIntRevisions() 1000 if not revs: 1001 return 1002 ancestor = self.repo[revs[0]] 1003 for rev in revs[1:]: 1004 ctx = self.repo[rev] 1005 ancestor = ancestor.ancestor(ctx) 1006 self.goto(ancestor.rev()) 1007 1008 def goto(self, rev): 1009 self.repoview.goto(rev) 1010 1011 def onRevisionActivated(self, rev): 1012 qgoto = False 1013 if hglib.isbasestring(rev): 1014 qgoto = True 1015 else: 1016 ctx = self.repo[rev] 1017 if b'qparent' in ctx.tags() or ctx.thgmqappliedpatch(): 1018 qgoto = True 1019 if b'qtip' in ctx.tags(): 1020 qgoto = False 1021 if qgoto: 1022 self.qgotoSelectedRevision() 1023 else: 1024 self.visualDiffRevision() 1025 1026 def reload(self, invalidate=True): 1027 'Initiate a refresh of the repo model, rebuild graph' 1028 try: 1029 if invalidate: 1030 self.repo.thginvalidate() 1031 self.rebuildGraph() 1032 self.reloadTaskTab() 1033 except EnvironmentError as e: 1034 self.showMessage(hglib.tounicode(str(e))) 1035 1036 def rebuildGraph(self): 1037 'Called by repositoryChanged signals, and during reload' 1038 self.showMessage('') 1039 self.filterbar.refresh() 1040 self.repoview.saveSettings() 1041 1042 def reloadTaskTab(self): 1043 w = self.taskTabsWidget.currentWidget() 1044 w.reload() 1045 1046 @pyqtSlot() 1047 def repositoryChanged(self): 1048 'Repository has detected a changelog / dirstate change' 1049 try: 1050 self.rebuildGraph() 1051 except (error.RevlogError, error.RepoError) as e: 1052 self.showMessage(hglib.tounicode(str(e))) 1053 self._updateNamedActions() 1054 1055 @pyqtSlot() 1056 def configChanged(self): 1057 'Repository is reporting its config files have changed' 1058 self.revDetailsWidget.reload() 1059 self.titleChanged.emit(self.title()) 1060 self.updateTaskTabs() 1061 1062 def updateTaskTabs(self): 1063 val = self._repoagent.configString('tortoisehg', 'tasktabs').lower() 1064 if val == 'east': 1065 self.taskTabsWidget.setTabPosition(QTabWidget.East) 1066 self.taskTabsWidget.tabBar().show() 1067 elif val == 'west': 1068 self.taskTabsWidget.setTabPosition(QTabWidget.West) 1069 self.taskTabsWidget.tabBar().show() 1070 else: 1071 self.taskTabsWidget.tabBar().hide() 1072 1073 @pyqtSlot(str, bool) 1074 def setBranch(self, branch, allparents): 1075 self.repomodel.setBranch(branch, allparents=allparents) 1076 self.titleChanged.emit(self.title()) 1077 1078 @pyqtSlot(bool) 1079 def setShowHidden(self, showhidden): 1080 self._repoagent.setHiddenRevsIncluded(showhidden) 1081 1082 @pyqtSlot(bool) 1083 def setShowGraftSource(self, showgraftsource): 1084 self.repomodel.setShowGraftSource(showgraftsource) 1085 1086 ## 1087 ## Workbench methods 1088 ## 1089 1090 def canGoBack(self): 1091 return self.repoview.canGoBack() 1092 1093 def canGoForward(self): 1094 return self.repoview.canGoForward() 1095 1096 def loadSettings(self): 1097 s = QSettings() 1098 repoid = hglib.shortrepoid(self.repo) 1099 self.revDetailsWidget.loadSettings(s) 1100 self.filterbar.loadSettings(s) 1101 self._repoagent.setHiddenRevsIncluded(self.filterbar.getShowHidden()) 1102 self.repotabs_splitter.restoreState( 1103 qtlib.readByteArray(s, 'repoWidget/splitter-' + repoid)) 1104 1105 def okToContinue(self): 1106 if self._repoagent.isBusy(): 1107 r = QMessageBox.question(self, _('Confirm Exit'), 1108 _('Mercurial command is still running.\n' 1109 'Are you sure you want to terminate?'), 1110 QMessageBox.Yes | QMessageBox.No, 1111 QMessageBox.No) 1112 if r == QMessageBox.Yes: 1113 self._repoagent.abortCommands() 1114 return False 1115 for i in pycompat.xrange(self.taskTabsWidget.count()): 1116 w = self.taskTabsWidget.widget(i) 1117 if w.canExit(): 1118 continue 1119 self.taskTabsWidget.setCurrentWidget(w) 1120 self.showMessage(_('Tab cannot exit')) 1121 return False 1122 return True 1123 1124 def closeRepoWidget(self): 1125 '''returns False if close should be aborted''' 1126 if not self.okToContinue(): 1127 return False 1128 s = QSettings() 1129 if self.isVisible(): 1130 try: 1131 repoid = hglib.shortrepoid(self.repo) 1132 s.setValue('repoWidget/splitter-' + repoid, 1133 self.repotabs_splitter.saveState()) 1134 except EnvironmentError: 1135 pass 1136 self.revDetailsWidget.saveSettings(s) 1137 self.commitDemand.forward('saveSettings', s, 'workbench') 1138 self.grepDemand.forward('saveSettings', s) 1139 self.filterbar.saveSettings(s) 1140 self.repoview.saveSettings(s) 1141 return True 1142 1143 def setSyncUrl(self, url): 1144 """Change the current peer-repo url of the sync widget; url may be 1145 a symbolic name defined in [paths] section""" 1146 self.syncDemand.get().setUrl(url) 1147 1148 def incoming(self): 1149 self.syncDemand.get().incoming() 1150 1151 def pull(self): 1152 self.syncDemand.get().pull() 1153 def outgoing(self): 1154 self.syncDemand.get().outgoing() 1155 def push(self, confirm=None, **kwargs): 1156 """Call sync push. 1157 1158 If confirm is False, the user will not be prompted for 1159 confirmation. If confirm is True, the prompt might be used. 1160 """ 1161 self.syncDemand.get().push(confirm, **kwargs) 1162 self.outgoingMode = False 1163 self._updateNamedActions() 1164 1165 def syncBookmark(self): 1166 self.syncDemand.get().syncBookmark() 1167 1168 ## 1169 ## Repoview context menu 1170 ## 1171 1172 def _isRevisionSelected(self): 1173 # type: () -> bool 1174 """True if the selection includes change/workingctx revision""" 1175 return any(r is None or isinstance(r, int) 1176 for r in self.repoview.selectedRevisions()) 1177 1178 def _isUnappliedPatchSelected(self): 1179 # type: () -> bool 1180 """True if the selection includes unapplied patch""" 1181 return any(r is not None and not isinstance(r, int) 1182 for r in self.repoview.selectedRevisions()) 1183 1184 def _isRevisionsPairSelected(self): 1185 # type: () -> bool 1186 """True if exactly two change/workingctx revisions are selected""" 1187 return (len(self.repoview.selectedRevisions()) == 2 1188 and not self._isUnappliedPatchSelected()) 1189 1190 def _selectionAttributes(self): 1191 # type: () -> Set[Text] 1192 """Returns a set of keywords that describe the selected revisions""" 1193 attributes = {_SELECTION_ISTRUE} 1194 1195 revisions, patches = self._selectedIntRevisionsAndUnappliedPatches() 1196 1197 if len(revisions) + len(patches) == 1: 1198 attributes.add(_SELECTION_SINGLE) 1199 if len(revisions) + len(patches) == 2: 1200 attributes.add(_SELECTION_PAIR) 1201 if revisions or patches: 1202 attributes.add(_SELECTION_SOME) 1203 1204 # In incoming/outgoing mode, unrelated revisions and patches are 1205 # filtered out. So we don't have to test each selected revision. 1206 if self.isBundleSet(): 1207 attributes.add(_SELECTION_INCOMING) 1208 if self.outgoingMode: 1209 attributes.add(_SELECTION_OUTGOING) 1210 1211 if not patches and revisions: 1212 attributes.add(_SELECTION_ISCTX) 1213 1214 haswdir = max(revisions) == nodemod.wdirrev 1215 if not haswdir: 1216 attributes.add(_SELECTION_ISREV) 1217 if len(revisions) == 1 and haswdir: 1218 attributes.add(_SELECTION_ISWD) 1219 1220 ctxs = [self.repo[rev] for rev in revisions] 1221 if all(c.phase() >= phases.draft or c.rev() is None for c in ctxs): 1222 attributes.add(_SELECTION_ISDRAFTORWD) 1223 if not haswdir and not any(c.thgmqappliedpatch() for c in ctxs): 1224 attributes.add(_SELECTION_FIXED) 1225 if all(c.thgmqappliedpatch() for c in ctxs): 1226 attributes.add(_SELECTION_ISPATCH) 1227 attributes.add(_SELECTION_APPLIED) 1228 if all(c.thgmqappliedpatch() or b'qparent' in c.tags() 1229 for c in ctxs): 1230 attributes.add(_SELECTION_QGOTO) 1231 1232 if not revisions and patches: 1233 attributes.add(_SELECTION_ISPATCH) 1234 attributes.add(_SELECTION_UNAPPLIED) 1235 if b'qtip' in self.repo.tags(): 1236 attributes.add(_SELECTION_QFOLD) 1237 1238 # TODO: maybe better to not scan all patches and test selection? 1239 q = self.repo.mq 1240 ispushable = False 1241 qnext = '' 1242 unapplied = 0 1243 for i in pycompat.xrange(q.seriesend(), len(q.series)): 1244 pushable, reason = q.pushable(i) 1245 if pushable: 1246 if unapplied == 0: 1247 qnext = hglib.tounicode(q.series[i]) 1248 if self.rev == q.series[i]: 1249 ispushable = True 1250 unapplied += 1 1251 1252 if ispushable: 1253 attributes.add(_SELECTION_QPUSH) 1254 if ispushable and len(patches) == 1 and patches[0] != qnext: 1255 attributes.add(_SELECTION_QPUSHMOVE) 1256 1257 return attributes 1258 1259 def _selectedIntRevisionsAndUnappliedPatches(self): 1260 # type: () -> Tuple[List[int], List[Text]] 1261 """Returns lists of selected change/workingctx revisions and unapplied 1262 patches""" 1263 revisions = [] 1264 patches = [] 1265 for r in self.repoview.selectedRevisions(): 1266 if r is None: 1267 revisions.append(nodemod.wdirrev) 1268 elif isinstance(r, int): 1269 revisions.append(r) 1270 else: 1271 assert isinstance(r, bytes) 1272 patches.append(hglib.tounicode(r)) 1273 return revisions, patches 1274 1275 def _selectedIntRevisions(self): 1276 # type: () -> List[int] 1277 """Returns a list of selected change/workingctx revisions 1278 1279 Unapplied patches are excluded. 1280 """ 1281 revisions, _patches = self._selectedIntRevisionsAndUnappliedPatches() 1282 return revisions 1283 1284 def _selectedIntRevisionsPair(self): 1285 # type: () -> Tuple[int, int] 1286 """Returns a pair of change/workingctx revisions if exactly two 1287 revisions are selected 1288 1289 Otherwise returns (nullrev, nullrev) for convenience. Use 1290 _isRevisionsPairSelected() if you need to check it strictly. 1291 """ 1292 if not self._isRevisionsPairSelected(): 1293 return nodemod.nullrev, nodemod.nullrev 1294 rev0, rev1 = self._selectedIntRevisions() 1295 return rev0, rev1 1296 1297 def _selectedDagRangeRevisions(self): 1298 # type: () -> List[int] 1299 """Returns a list of revisions in the DAG range specified by the 1300 selected revisions pair 1301 1302 If no revisions pair selected, returns an empty list. 1303 """ 1304 if not self._isRevisionsPairSelected(): 1305 return [] 1306 rev0, rev1 = sorted(self._selectedIntRevisions()) 1307 # simply disable lazy evaluation as we won't handle slow query 1308 return list(self.repo.revs(b'%d::%d', rev0, rev1)) 1309 1310 def _selectedUnappliedPatches(self): 1311 # type: () -> List[Text] 1312 """Returns a list of selected unapplied patches""" 1313 _revisions, patches = self._selectedIntRevisionsAndUnappliedPatches() 1314 return patches 1315 1316 @pyqtSlot(QPoint) 1317 def _popupSelectionMenu(self, point): 1318 'User requested a context menu in repo view widget' 1319 1320 selection = self.repoview.selectedRevisions() 1321 if not selection: 1322 return 1323 1324 if self.isBundleSet(): 1325 self._popupIncomingBundleMenu(point) 1326 elif not self._isRevisionSelected(): 1327 self._popupUnappliedPatchMenu(point) 1328 elif len(selection) == 1: 1329 self._popupSingleSelectionMenu(point) 1330 elif len(selection) == 2: 1331 self._popupPairSelectionMenu(point) 1332 else: 1333 self._popupMultipleSelectionMenu(point) 1334 1335 def _popupSingleSelectionMenu(self, point): 1336 menu = QMenu(self) 1337 1338 if self.outgoingMode: 1339 submenu = menu.addMenu(_('Pus&h')) 1340 self._addNamedActionsToMenu(submenu, [ 1341 'Repository.pushToRevision', 1342 'Repository.pushBranch', 1343 'Repository.pushAll', 1344 ]) 1345 menu.addSeparator() 1346 1347 self._addNamedActionsToMenu(menu, [ 1348 'Repository.updateToRevision', 1349 None, 1350 'Repository.visualDiff', 1351 'Repository.visualDiffToLocal', 1352 'Repository.browseRevision', 1353 'RepoView.filterByRevisionsMenu', 1354 None, 1355 'Repository.mergeWithRevision', 1356 'Repository.closeRevision', 1357 'Repository.tagRevision', 1358 'Repository.bookmarkRevision', 1359 'Repository.topicRevision', 1360 'Repository.signRevision', 1361 None, 1362 'Repository.backoutToRevision', 1363 'Repository.revertToRevision', 1364 None, 1365 ]) 1366 1367 submenu = menu.addMenu(_('Copy &Hash')) 1368 self._addNamedActionsToMenu(submenu, [ 1369 'Repository.copyHash', 1370 'Repository.copyShortHash', 1371 None, 1372 'Repository.copyGitHash', 1373 'Repository.copyShortGitHash', 1374 ]) 1375 menu.addSeparator() 1376 1377 submenu = menu.addMenu(_('E&xport')) 1378 self._addNamedActionsToMenu(submenu, [ 1379 'Repository.exportRevisions', 1380 'Repository.emailRevisions', 1381 'Repository.archiveRevision', 1382 'Repository.bundleRevisions', 1383 'Repository.copyPatch', 1384 ]) 1385 menu.addSeparator() 1386 1387 self._addNamedActionsToMenu(menu, [ 1388 'RepoView.changePhaseMenu', 1389 None, 1390 'Repository.graftRevisions', 1391 ]) 1392 1393 submenu = menu.addMenu(_('Modi&fy History')) 1394 self._addNamedActionsToMenu(submenu, [ 1395 'PatchQueue.popPatch', 1396 'PatchQueue.importRevision', 1397 'PatchQueue.finishRevision', 1398 'PatchQueue.renamePatch', 1399 None, 1400 'PatchQueue.launchOptionsDialog', 1401 None, 1402 'Repository.pickRevision', 1403 'Repository.rebaseRevision', 1404 None, 1405 'Repository.pruneRevisions', 1406 'Repository.stripRevision' 1407 ]) 1408 submenu.menuAction().setVisible(not submenu.isEmpty()) 1409 1410 self._addNamedActionsToMenu(menu, [ 1411 'Repository.sendToReviewBoard', 1412 'Repository.sendToPhabricator', 1413 ]) 1414 1415 self._addCustomToolsSubMenu(menu, 'workbench.revdetails.custom-menu') 1416 1417 menu.setAttribute(Qt.WA_DeleteOnClose) 1418 menu.popup(point) 1419 1420 def _popupPairSelectionMenu(self, point): 1421 menu = QMenu(self) 1422 1423 self._addNamedActionsToMenu(menu, [ 1424 'Repository.visualDiffRevisionsPair', 1425 'Repository.exportDiff', 1426 None, 1427 'Repository.exportRevisions', 1428 'Repository.emailRevisions', 1429 'Repository.copyPatch', 1430 None, 1431 'Repository.archiveDagRangeRevisions', 1432 'Repository.exportDagRangeRevisions', 1433 'Repository.emailDagRangeRevisions', 1434 'Repository.bundleDagRangeRevisions', 1435 None, 1436 'Repository.bisectGoodBadRevisionsPair', 1437 'Repository.bisectBadGoodRevisionsPair', 1438 'Repository.compressRevisionsPair', 1439 'Repository.rebaseSourceDestRevisionsPair', 1440 None, 1441 'RepoView.goToCommonAncestor', 1442 'RepoView.filterByRevisionsMenu', 1443 None, 1444 'Repository.graftRevisions', 1445 None, 1446 'Repository.pruneRevisions', 1447 None, 1448 'Repository.sendToReviewBoard', 1449 None, 1450 'Repository.sendToPhabricator', 1451 ]) 1452 1453 self._addCustomToolsSubMenu(menu, 'workbench.pairselection.custom-menu') 1454 1455 menu.setAttribute(Qt.WA_DeleteOnClose) 1456 menu.popup(point) 1457 1458 def _popupMultipleSelectionMenu(self, point): 1459 menu = QMenu(self) 1460 1461 self._addNamedActionsToMenu(menu, [ 1462 'Repository.exportRevisions', 1463 'Repository.emailRevisions', 1464 'Repository.copyPatch', 1465 None, 1466 'RepoView.goToCommonAncestor', 1467 'RepoView.filterByRevisionsMenu', 1468 None, 1469 'Repository.graftRevisions', 1470 None, 1471 'Repository.pruneRevisions', 1472 'Repository.sendToReviewBoard', 1473 'Repository.sendToPhabricator', 1474 ]) 1475 1476 self._addCustomToolsSubMenu(menu, 1477 'workbench.multipleselection.custom-menu') 1478 1479 menu.setAttribute(Qt.WA_DeleteOnClose) 1480 menu.popup(point) 1481 1482 def _popupIncomingBundleMenu(self, point): 1483 menu = QMenu(self) 1484 1485 self._addNamedActionsToMenu(menu, [ 1486 'Repository.pullToRevision', 1487 'Repository.visualDiff', 1488 ]) 1489 1490 menu.setAttribute(Qt.WA_DeleteOnClose) 1491 menu.popup(point) 1492 1493 def _popupUnappliedPatchMenu(self, point): 1494 menu = QMenu(self) 1495 1496 self._addNamedActionsToMenu(menu, [ 1497 'PatchQueue.pushPatch', 1498 'PatchQueue.pushExactPatch', 1499 'PatchQueue.pushMovePatch', 1500 'PatchQueue.foldPatches', 1501 'PatchQueue.deletePatches', 1502 'PatchQueue.renamePatch', 1503 None, 1504 'PatchQueue.launchOptionsDialog', 1505 ]) 1506 1507 menu.setAttribute(Qt.WA_DeleteOnClose) 1508 menu.popup(point) 1509 1510 def _createNamedAction(self, name, attrs, exts=None, icon=None, cb=None): 1511 # type: (Text, Set[Text], Optional[Set[Text]], Optional[Text], Optional[Callable]) -> QAction 1512 act = QAction(self) 1513 act.setShortcutContext(Qt.WidgetWithChildrenShortcut) 1514 if icon: 1515 act.setIcon(qtlib.geticon(icon)) 1516 if cb: 1517 act.triggered.connect(cb) 1518 self._addNamedAction(name, act, attrs, exts) 1519 return act 1520 1521 def _addNamedAction(self, name, act, attrs, exts=None): 1522 # type: (Text, QAction, Set[Text], Optional[Set[Text]]) -> None 1523 assert name not in self._actions, name 1524 assert attrs.issubset(_KNOWN_SELECTION_ATTRS), attrs 1525 # RepoWidget actions act on revisions selected in the graph view, so 1526 # the shortcuts should not be enabled for task tabs. 1527 self.repoview.addAction(act) 1528 self._actionregistry.registerAction(name, act) 1529 self._actions[name] = (act, attrs, exts or set()) 1530 1531 def _addNamedActionsToMenu(self, menu, names): 1532 # type: (QMenu, List[Optional[Text]]) -> None 1533 for n in names: 1534 if n: 1535 menu.addAction(self._actions[n][0]) 1536 else: 1537 menu.addSeparator() 1538 1539 def _updateNamedActions(self): 1540 selattrs = self._selectionAttributes() 1541 enabledexts = set(map(pycompat.sysstr, self.repo.extensions())) 1542 1543 for act, attrs, exts in self._actions.values(): 1544 act.setEnabled(attrs.issubset(selattrs)) 1545 act.setVisible(not exts or bool(exts & enabledexts)) 1546 1547 def _addCustomToolsSubMenu(self, menu, location): 1548 # type: (QMenu, Text) -> None 1549 tools, toollist = hglib.tortoisehgtools(self.repo.ui, 1550 selectedlocation=location) 1551 1552 if not tools: 1553 return 1554 1555 selattrs = self._selectionAttributes() 1556 1557 menu.addSeparator() 1558 submenu = menu.addMenu(_('Custom Tools')) 1559 submenu.triggered.connect(self._runCustomCommandByMenu) 1560 for name in toollist: 1561 if name == '|': 1562 submenu.addSeparator() 1563 continue 1564 info = tools.get(name, None) 1565 if info is None: 1566 continue 1567 command = info.get('command', None) 1568 if not command: 1569 continue 1570 workingdir = info.get('workingdir', '') 1571 showoutput = info.get('showoutput', False) 1572 label = info.get('label', name) 1573 icon = info.get('icon', 'tools-spanner-hammer') 1574 enable = info.get('enable', 'istrue').lower() # pytype: disable=attribute-error 1575 if enable not in _CUSTOM_TOOLS_SELECTION_ATTRS: 1576 continue 1577 a = submenu.addAction(label) 1578 if icon: 1579 a.setIcon(qtlib.geticon(icon)) 1580 a.setData((command, showoutput, workingdir)) 1581 a.setEnabled(enable in selattrs) 1582 1583 def _setUpNamedActions(self): 1584 entry = self._createNamedAction 1585 1586 SINGLE = _SELECTION_SINGLE 1587 PAIR = _SELECTION_PAIR 1588 SOME = _SELECTION_SOME 1589 1590 INCOMING = _SELECTION_INCOMING 1591 OUTGOING = _SELECTION_OUTGOING 1592 1593 ISREV = _SELECTION_ISREV 1594 ISCTX = _SELECTION_ISCTX 1595 ISPATCH = _SELECTION_ISPATCH 1596 FIXED = _SELECTION_FIXED 1597 APPLIED = _SELECTION_APPLIED 1598 UNAPPLIED = _SELECTION_UNAPPLIED 1599 QFOLD = _SELECTION_QFOLD 1600 QPUSH = _SELECTION_QPUSH 1601 QPUSHMOVE = _SELECTION_QPUSHMOVE 1602 ISDRAFTORWD = _SELECTION_ISDRAFTORWD 1603 1604 entry('Repository.pullToRevision', {SINGLE, INCOMING, ISREV}, None, 1605 'hg-pull-to-here', self.pullBundleToRev) 1606 1607 pushtypeicon = {'all': None, 'branch': None, 'revision': None} 1608 defaultpush = self._repoagent.configString('tortoisehg', 'defaultpush') 1609 pushtypeicon[defaultpush] = 'hg-push' 1610 entry('Repository.pushToRevision', {SINGLE, OUTGOING, ISREV}, None, 1611 pushtypeicon['revision'], self.pushToRevision) 1612 entry('Repository.pushBranch', {SINGLE, OUTGOING, ISREV}, None, 1613 pushtypeicon['branch'], self.pushBranch) 1614 entry('Repository.pushAll', {SINGLE, OUTGOING, ISREV}, None, 1615 pushtypeicon['all'], self.pushAll) 1616 1617 # TODO: unify to Repository.update action of Workbench? 1618 entry('Repository.updateToRevision', {SINGLE, ISREV}, None, 1619 'hg-update', self.updateToRevision) 1620 1621 entry('Repository.visualDiff', {SINGLE, ISCTX}, None, 1622 'visualdiff', self.visualDiffRevision) 1623 entry('Repository.visualDiffToLocal', {SINGLE, ISREV}, None, 1624 'ldiff', self.visualDiffToLocal) 1625 # TODO: visdiff can't handle wdir dest 1626 entry('Repository.visualDiffRevisionsPair', {PAIR, ISREV}, None, 1627 'visualdiff', self.visualDiffRevisionsPair) 1628 1629 entry('Repository.browseRevision', {SINGLE, ISCTX}, None, 1630 'hg-annotate', self.manifestRevision) 1631 1632 self._addNamedAction('RepoView.filterByRevisionsMenu', 1633 self._createFilterBySelectedRevisionsMenu(), 1634 {SOME, ISREV}) 1635 1636 entry('Repository.mergeWithRevision', {SINGLE, FIXED}, None, 1637 'hg-merge', self.mergeWithRevision) 1638 entry('Repository.closeRevision', {SINGLE, ISREV}, {'closehead'}, 1639 'hg-close-head', self.closeRevision) 1640 1641 entry('Repository.tagRevision', {SINGLE, FIXED}, None, 1642 'hg-tag', self.tagToRevision) 1643 entry('Repository.bookmarkRevision', {SINGLE, ISREV}, None, 1644 'hg-bookmarks', self.bookmarkRevision) 1645 entry('Repository.topicRevision', {SINGLE, ISDRAFTORWD}, {'topic'}, 1646 'topic', self.topicRevision) 1647 entry('Repository.signRevision', {SINGLE, FIXED}, {'gpg'}, 1648 'hg-sign', self.signRevision) 1649 1650 entry('Repository.backoutToRevision', {SINGLE, FIXED}, None, 1651 'hg-revert', self.backoutToRevision) 1652 entry('Repository.revertToRevision', {SINGLE, ISCTX}, None, 1653 'hg-revert', self.revertToRevision) 1654 1655 entry('Repository.copyHash', {SINGLE, ISREV}, None, 1656 'copy-hash', self.copyHash) 1657 entry('Repository.copyShortHash', {SINGLE, ISREV}, None, 1658 None, self.copyShortHash) 1659 entry('Repository.copyGitHash', {SINGLE, ISREV}, {'hggit'}, None, 1660 self.copyGitHash) 1661 entry('Repository.copyShortGitHash', {SINGLE, ISREV}, {'hggit'}, None, 1662 self.copyShortGitHash) 1663 1664 entry('Repository.exportDiff', {PAIR, ISCTX}, None, 1665 'hg-export', self.exportDiff) 1666 entry('Repository.exportRevisions', {SOME, ISREV}, None, 1667 'hg-export', self.exportSelectedRevisions) 1668 entry('Repository.exportDagRangeRevisions', {PAIR, ISREV}, None, 1669 'hg-export', self.exportDagRangeRevisions) 1670 entry('Repository.emailRevisions', {SOME, ISREV}, None, 1671 'mail-forward', self.emailSelectedRevisions) 1672 entry('Repository.emailDagRangeRevisions', {PAIR, ISREV}, None, 1673 'mail-forward', self.emailDagRangeRevisions) 1674 entry('Repository.archiveRevision', {SINGLE, ISREV}, None, 1675 'hg-archive', self.archiveRevision) 1676 entry('Repository.archiveDagRangeRevisions', {PAIR, ISREV}, None, 1677 'hg-archive', self.archiveDagRangeRevisions) 1678 entry('Repository.bundleRevisions', {SINGLE, ISREV}, None, 1679 'hg-bundle', self.bundleRevisions) 1680 entry('Repository.bundleDagRangeRevisions', {PAIR, ISREV}, None, 1681 'hg-bundle', self.bundleDagRangeRevisions) 1682 entry('Repository.copyPatch', {SOME, ISCTX}, None, 1683 'copy-patch', self.copyPatch) 1684 1685 entry('Repository.bisectGoodBadRevisionsPair', {PAIR, ISREV}, None, 1686 'hg-bisect-good-bad', self.bisectGoodBadRevisionsPair) 1687 entry('Repository.bisectBadGoodRevisionsPair', {PAIR, ISREV}, None, 1688 'hg-bisect-bad-good', self.bisectBadGoodRevisionsPair) 1689 1690 entry('RepoView.goToCommonAncestor', {SOME, ISCTX}, None, 1691 'hg-merge', self._gotoAncestor) 1692 1693 submenu = QMenu(self) 1694 submenu.triggered.connect(self._changePhaseByMenu) 1695 # TODO: filter out hidden names better 1696 for pnum, pname in enumerate(phases.cmdphasenames): 1697 a = submenu.addAction(pycompat.sysstr(pname)) 1698 a.setData(pnum) 1699 self._addNamedAction('RepoView.changePhaseMenu', submenu.menuAction(), 1700 {SINGLE, ISREV}) 1701 1702 entry('Repository.compressRevisionsPair', {PAIR, ISREV}, None, 1703 'hg-compress', self.compressRevisionsPair) 1704 entry('Repository.graftRevisions', {SOME, ISREV}, None, 1705 'hg-transplant', self.graftRevisions) 1706 1707 entry('PatchQueue.popPatch', {SINGLE, APPLIED}, {'mq'}, 1708 'hg-qgoto', self.qgotoParentRevision) 1709 entry('PatchQueue.importRevision', {SINGLE, FIXED}, {'mq'}, 1710 'qimport', self.qimportRevision) 1711 entry('PatchQueue.finishRevision', {SINGLE, APPLIED}, {'mq'}, 1712 'qfinish', self.qfinishRevision) 1713 entry('PatchQueue.renamePatch', {SINGLE, ISPATCH}, {'mq'}, 1714 None, self.qrename) 1715 1716 entry('PatchQueue.pushPatch', {SINGLE, QPUSH}, {'mq'}, 1717 'hg-qpush', self.qpushRevision) 1718 entry('PatchQueue.pushExactPatch', {SINGLE, QPUSH}, {'mq'}, 1719 None, self.qpushExactRevision) 1720 entry('PatchQueue.pushMovePatch', {SINGLE, QPUSHMOVE}, {'mq'}, 1721 None, self.qpushMoveRevision) 1722 entry('PatchQueue.foldPatches', {SOME, QFOLD}, {'mq'}, 1723 'hg-qfold', self.qfoldPatches) 1724 entry('PatchQueue.deletePatches', {SOME, UNAPPLIED}, {'mq'}, 1725 'hg-qdelete', self.qdeletePatches) 1726 1727 a = entry('PatchQueue.launchOptionsDialog', set(), {'mq'}) 1728 if self._mqActions: 1729 a.triggered.connect(self._mqActions.launchOptionsDialog) 1730 1731 entry('Repository.pickRevision', {SINGLE, ISREV}, {'evolve'}, 1732 None, self._pickRevision) 1733 entry('Repository.rebaseRevision', {SINGLE, ISREV}, {'rebase'}, 1734 'hg-rebase', self.rebaseRevision) 1735 entry('Repository.rebaseSourceDestRevisionsPair', {PAIR, ISREV}, 1736 {'rebase'}, 'hg-rebase', self.rebaseSourceDestRevisionsPair) 1737 1738 entry('Repository.pruneRevisions', {SOME, FIXED}, {'evolve'}, 1739 'edit-cut', self._pruneSelected) 1740 entry('Repository.stripRevision', {SINGLE, FIXED}, {'mq', 'strip'}, 1741 'hg-strip', self.stripRevision) 1742 1743 entry('Repository.sendToReviewBoard', {SOME, ISREV}, {'reviewboard'}, 1744 'reviewboard', self.sendToReviewBoard) 1745 entry('Repository.sendToPhabricator', {SOME, ISREV}, {'phabricator'}, 1746 'phabricator', self.sendToPhabricator) 1747 1748 @pyqtSlot() 1749 def exportDiff(self): 1750 rev0, rev1 = self._selectedIntRevisionsPair() 1751 root = self.repo.root 1752 filename = b'%s_%d_to_%d.diff' % (os.path.basename(root), rev0, rev1) 1753 file, _filter = QFileDialog.getSaveFileName( 1754 self, _('Write diff file'), 1755 hglib.tounicode(os.path.join(root, filename))) 1756 if not file: 1757 return 1758 f = QFile(file) 1759 if not f.open(QIODevice.WriteOnly | QIODevice.Truncate): 1760 WarningMsgBox(_('Repository Error'), 1761 _('Unable to write diff file')) 1762 return 1763 cmdline = hglib.buildcmdargs('diff', rev=[rev0, rev1]) 1764 sess = self._runCommand(cmdline) 1765 sess.setOutputDevice(f) 1766 1767 @pyqtSlot() 1768 def exportSelectedRevisions(self): 1769 self._exportRevisions(self.repoview.selectedRevisions()) 1770 1771 @pyqtSlot() 1772 def exportDagRangeRevisions(self): 1773 l = self._selectedDagRangeRevisions() 1774 if l: 1775 self._exportRevisions(l) 1776 1777 def _exportRevisions(self, revisions): 1778 if not revisions: 1779 return 1780 if len(revisions) == 1: 1781 if isinstance(self.rev, int): 1782 defaultpath = os.path.join(self.repoRootPath(), 1783 '%d.patch' % self.rev) 1784 else: 1785 defaultpath = self.repoRootPath() 1786 1787 ret, _filter = QFileDialog.getSaveFileName( 1788 self, _('Export patch'), defaultpath, 1789 _('Patch Files (*.patch)')) 1790 if not ret: 1791 return 1792 epath = pycompat.unicode(ret) 1793 udir = os.path.dirname(epath) 1794 custompath = True 1795 else: 1796 udir = QFileDialog.getExistingDirectory(self, _('Export patch'), 1797 hglib.tounicode(self.repo.root)) 1798 if not udir: 1799 return 1800 udir = pycompat.unicode(udir) 1801 ename = self._repoagent.shortName() + '_%r.patch' 1802 epath = os.path.join(udir, ename) 1803 custompath = False 1804 1805 cmdline = hglib.buildcmdargs('export', verbose=True, output=epath, 1806 rev=hglib.compactrevs(sorted(revisions))) 1807 1808 existingRevisions = [] 1809 for rev in revisions: 1810 if custompath: 1811 path = epath 1812 else: 1813 path = epath % rev 1814 if os.path.exists(path): 1815 if os.path.isfile(path): 1816 existingRevisions.append(rev) 1817 else: 1818 QMessageBox.warning(self, 1819 _('Cannot export revision'), 1820 (_('Cannot export revision %s into the file named:' 1821 '\n\n%s\n') % (rev, epath % rev)) + \ 1822 _('There is already an existing folder ' 1823 'with that same name.')) 1824 return 1825 1826 if existingRevisions: 1827 buttonNames = [_("Replace"), _("Append"), _("Abort")] 1828 1829 warningMessage = \ 1830 _('There are existing patch files for %d revisions (%s) ' 1831 'in the selected location (%s).\n\n') \ 1832 % (len(existingRevisions), 1833 " ,".join([str(rev) for rev in existingRevisions]), 1834 udir) 1835 1836 warningMessage += \ 1837 _('What do you want to do?\n') + u'\n' + \ 1838 u'- ' + _('Replace the existing patch files.\n') + \ 1839 u'- ' + _('Append the changes to the existing patch files.\n') + \ 1840 u'- ' + _('Abort the export operation.\n') 1841 1842 res = qtlib.CustomPrompt(_('Patch files already exist'), 1843 warningMessage, 1844 self, 1845 buttonNames, 0, 2).run() 1846 1847 if buttonNames[res] == _("Replace"): 1848 # Remove the existing patch files 1849 for rev in existingRevisions: 1850 if custompath: 1851 os.remove(epath) 1852 else: 1853 os.remove(epath % rev) 1854 elif buttonNames[res] == _("Abort"): 1855 return 1856 1857 self._runCommand(cmdline) 1858 1859 if len(revisions) == 1: 1860 # Show a message box with a link to the export folder and to the 1861 # exported file 1862 rev = revisions[0] 1863 patchfilename = os.path.normpath(epath) 1864 patchdirname = os.path.normpath(os.path.dirname(epath)) 1865 patchshortname = os.path.basename(patchfilename) 1866 if patchdirname.endswith(os.path.sep): 1867 patchdirname = patchdirname[:-1] 1868 qtlib.InfoMsgBox(_('Patch exported'), 1869 _('Revision #%d (%s) was exported to:<p>' 1870 '<a href="file:///%s">%s</a>%s' 1871 '<a href="file:///%s">%s</a>') \ 1872 % (rev, str(self.repo[rev]), 1873 patchdirname, patchdirname, os.path.sep, 1874 patchfilename, patchshortname)) 1875 else: 1876 # Show a message box with a link to the export folder 1877 qtlib.InfoMsgBox(_('Patches exported'), 1878 _('%d patches were exported to:<p>' 1879 '<a href="file:///%s">%s</a>') \ 1880 % (len(revisions), udir, udir)) 1881 1882 def visualDiffRevision(self): 1883 opts = dict(change=self.rev) 1884 dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) 1885 if dlg: 1886 dlg.exec_() 1887 1888 def visualDiffToLocal(self): 1889 if self.rev is None: 1890 return 1891 opts = dict(rev=['rev(%d)' % self.rev]) 1892 dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) 1893 if dlg: 1894 dlg.exec_() 1895 1896 @pyqtSlot() 1897 def visualDiffRevisionsPair(self): 1898 revA, revB = self._selectedIntRevisionsPair() 1899 dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], 1900 {'rev': (str(revA), str(revB))}) 1901 if dlg: 1902 dlg.exec_() 1903 1904 @pyqtSlot() 1905 def updateToRevision(self): 1906 rev = None 1907 if isinstance(self.rev, int): 1908 rev = hglib.getrevisionlabel(self.repo, self.rev) 1909 dlg = update.UpdateDialog(self._repoagent, rev, self) 1910 r = dlg.exec_() 1911 if r in (0, 1): 1912 self.gotoParent() 1913 1914 @pyqtSlot() 1915 def lockTool(self): 1916 from .locktool import LockDialog 1917 dlg = LockDialog(self._repoagent, self) 1918 if dlg: 1919 dlg.exec_() 1920 1921 @pyqtSlot() 1922 def revertToRevision(self): 1923 if not qtlib.QuestionMsgBox( 1924 _('Confirm Revert'), 1925 _('Reverting all files will discard changes and ' 1926 'leave affected files in a modified state.<br>' 1927 '<br>Are you sure you want to use revert?<br><br>' 1928 '(use update to checkout another revision)'), 1929 parent=self): 1930 return 1931 cmdline = hglib.buildcmdargs('revert', all=True, rev=self.rev) 1932 sess = self._runCommand(cmdline) 1933 sess.commandFinished.connect(self._refreshCommitTabIfNeeded) 1934 1935 def _createFilterBySelectedRevisionsMenu(self): 1936 menu = QMenu(_('Filter b&y'), self) 1937 menu.setIcon(qtlib.geticon('view-filter')) 1938 menu.triggered.connect(self._filterBySelectedRevisions) 1939 for t, r in [(_('&Ancestors and Descendants'), 1940 "ancestors({revs}) or descendants({revs})"), 1941 (_('A&uthor'), "matching({revs}, 'author')"), 1942 (_('&Branch'), "branch({revs})"), 1943 ]: 1944 a = menu.addAction(t) 1945 a.setData(r) 1946 menu.addSeparator() 1947 menu.addAction(_('&More Options...')) 1948 return menu.menuAction() 1949 1950 @pyqtSlot(QAction) 1951 def _filterBySelectedRevisions(self, action): 1952 revs = hglib.compactrevs(sorted(self.repoview.selectedRevisions())) 1953 expr = action.data() 1954 if not expr: 1955 self._filterByMatchDialog(revs) 1956 return 1957 self.setFilter(expr.format(revs=revs)) 1958 1959 def _filterByMatchDialog(self, revlist): 1960 dlg = matching.MatchDialog(self._repoagent, revlist, self) 1961 if dlg.exec_(): 1962 self.setFilter(dlg.revsetexpression) 1963 1964 def pushAll(self): 1965 self.syncDemand.forward('push', False, pushall=True) 1966 1967 def pushToRevision(self): 1968 # Do not ask for confirmation 1969 self.syncDemand.forward('push', False, rev=self.rev) 1970 1971 def pushBranch(self): 1972 # Do not ask for confirmation 1973 self.syncDemand.forward('push', False, 1974 branch=self.repo[self.rev].branch()) 1975 1976 def manifestRevision(self): 1977 if QApplication.keyboardModifiers() & Qt.ShiftModifier: 1978 self._dialogs.openNew(RepoWidget._createManifestDialog) 1979 else: 1980 dlg = self._dialogs.open(RepoWidget._createManifestDialog) 1981 dlg.setRev(self.rev) 1982 1983 def _createManifestDialog(self): 1984 return revdetails.createManifestDialog(self._repoagent, self.rev) 1985 1986 def mergeWithOtherHead(self): 1987 """Open dialog to merge with the other head of the current branch""" 1988 cmdline = hglib.buildcmdargs('merge', preview=True, 1989 config=r'ui.logtemplate={rev}\n') 1990 sess = self._runCommand(cmdline) 1991 sess.setCaptureOutput(True) 1992 sess.commandFinished.connect(self._onMergePreviewFinished) 1993 1994 @pyqtSlot(int) 1995 def _onMergePreviewFinished(self, ret): 1996 sess = self.sender() 1997 if ret == 255 and 'hg heads' in sess.errorString(): 1998 # multiple heads 1999 self.filterbar.setQuery('head() - .') 2000 self.filterbar.runQuery() 2001 msg = '\n'.join(sess.errorString().splitlines()[:-1]) # drop hint 2002 w = self.setInfoBar(infobar.ConfirmInfoBar, msg) 2003 assert w 2004 w.acceptButton.setText(_('Merge')) 2005 w.accepted.connect(self.mergeWithRevision) 2006 w.finished.connect(self.clearRevisionSet) 2007 return 2008 if ret != 0: 2009 return 2010 revs = pycompat.maplist(int, bytes(sess.readAll()).splitlines()) 2011 if not revs: 2012 return 2013 self._dialogs.open(RepoWidget._createMergeDialog, revs[-1]) 2014 2015 @pyqtSlot() 2016 def mergeWithRevision(self): 2017 # Don't use self.rev (i.e. the current revision.) This is a context 2018 # menu handler, and the menu is open for the selected rows, not for 2019 # the current row. 2020 revisions = self.repoview.selectedRevisions() 2021 if len(revisions) != 1: 2022 QMessageBox.warning(self, _('Unable to merge'), 2023 _('Please select a revision to merge.')) 2024 return 2025 rev = revisions[0] 2026 if not isinstance(rev, int): 2027 QMessageBox.warning(self, _('Unable to merge'), 2028 _('Cannot merge with a pseudo revision %r.') 2029 % rev) 2030 return 2031 pctx = self.repo[b'.'] 2032 octx = self.repo[rev] 2033 if pctx == octx: 2034 QMessageBox.warning(self, _('Unable to merge'), 2035 _('You cannot merge a revision with itself')) 2036 return 2037 self._dialogs.open(RepoWidget._createMergeDialog, rev) 2038 2039 def _createMergeDialog(self, rev): 2040 return merge.MergeDialog(self._repoagent, rev, self) 2041 2042 def tagToRevision(self): 2043 dlg = tag.TagDialog(self._repoagent, rev=str(self.rev), parent=self) 2044 dlg.exec_() 2045 2046 def closeRevision(self): 2047 dlg = close_branch.createCloseBranchDialog(self._repoagent, self.rev, 2048 parent=self) 2049 dlg.exec_() 2050 2051 def bookmarkRevision(self): 2052 dlg = bookmark.BookmarkDialog(self._repoagent, self.rev, self) 2053 dlg.exec_() 2054 2055 def topicRevision(self): 2056 dlg = topic.TopicDialog(self._repoagent, self.rev, self) 2057 dlg.exec_() 2058 2059 def signRevision(self): 2060 dlg = sign.SignDialog(self._repoagent, self.rev, self) 2061 dlg.exec_() 2062 2063 def graftRevisions(self): 2064 """Graft selected revision on top of working directory parent""" 2065 revlist = [] 2066 for rev in sorted(self.repoview.selectedRevisions()): 2067 revlist.append(str(rev)) 2068 if not revlist: 2069 revlist = [self.rev] 2070 dlg = graft.GraftDialog(self._repoagent, self, source=revlist) 2071 if dlg.valid: 2072 dlg.exec_() 2073 2074 def backoutToRevision(self): 2075 msg = backout.checkrev(self._repoagent.rawRepo(), self.rev) 2076 if msg: 2077 qtlib.InfoMsgBox(_('Unable to backout'), msg, parent=self) 2078 return 2079 dlg = backout.BackoutDialog(self._repoagent, self.rev, self) 2080 dlg.finished.connect(dlg.deleteLater) 2081 dlg.exec_() 2082 2083 @pyqtSlot() 2084 def _pruneSelected(self): 2085 revspec = hglib.compactrevs(sorted(self.repoview.selectedRevisions())) 2086 dlg = prune.createPruneDialog(self._repoagent, revspec, self) 2087 dlg.exec_() 2088 2089 def stripRevision(self): 2090 'Strip the selected revision and all descendants' 2091 dlg = thgstrip.createStripDialog(self._repoagent, rev=str(self.rev), 2092 parent=self) 2093 dlg.exec_() 2094 2095 def sendToReviewBoard(self): 2096 self._dialogs.open(RepoWidget._createPostReviewDialog, 2097 tuple(self.repoview.selectedRevisions())) 2098 2099 def _createPostReviewDialog(self, revs): 2100 # type: (Sequence[int]) -> postreview.PostReviewDialog 2101 return postreview.PostReviewDialog(self.repo.ui, self._repoagent, revs) 2102 2103 @pyqtSlot() 2104 def sendToPhabricator(self): 2105 self._dialogs.open(RepoWidget._createPhabReviewDialog, 2106 tuple(self.repoview.selectedRevisions())) 2107 2108 def _createPhabReviewDialog(self, revs): 2109 return phabreview.PhabReviewDialog(self._repoagent, revs) 2110 2111 @pyqtSlot() 2112 def emailSelectedRevisions(self): 2113 self._emailRevisions(self.repoview.selectedRevisions()) 2114 2115 @pyqtSlot() 2116 def emailDagRangeRevisions(self): 2117 l = self._selectedDagRangeRevisions() 2118 if l: 2119 self._emailRevisions(l) 2120 2121 def _emailRevisions(self, revs): 2122 self._dialogs.open(RepoWidget._createEmailDialog, tuple(revs)) 2123 2124 def _createEmailDialog(self, revs): 2125 return hgemail.EmailDialog(self._repoagent, revs) 2126 2127 def archiveRevision(self): 2128 rev = hglib.getrevisionlabel(self.repo, self.rev) 2129 dlg = archive.createArchiveDialog(self._repoagent, rev, self) 2130 dlg.exec_() 2131 2132 @pyqtSlot() 2133 def archiveDagRangeRevisions(self): 2134 l = self._selectedDagRangeRevisions() 2135 if l: 2136 self.archiveRevisions(l) 2137 2138 def archiveRevisions(self, revs): 2139 rev = hglib.getrevisionlabel(self.repo, max(revs)) 2140 minrev = '%d' % min(revs) 2141 dlg = archive.createArchiveDialog(self._repoagent, rev=rev, minrev=minrev, 2142 parent=self) 2143 dlg.exec_() 2144 2145 @pyqtSlot() 2146 def bundleDagRangeRevisions(self): 2147 l = self._selectedDagRangeRevisions() 2148 if l: 2149 self.bundleRevisions(base=l[0], tip=l[-1]) 2150 2151 def bundleRevisions(self, base=None, tip=None): 2152 root = self.repoRootPath() 2153 if base is None or base is False: 2154 base = self.rev 2155 data = dict(name=os.path.basename(root), base=base) 2156 if tip is None: 2157 filename = '%(name)s_%(base)s_and_descendants.hg' % data 2158 else: 2159 data.update(rev=tip) 2160 filename = '%(name)s_%(base)s_to_%(rev)s.hg' % data 2161 2162 file, _filter = QFileDialog.getSaveFileName( 2163 self, _('Write bundle'), os.path.join(root, filename)) 2164 if not file: 2165 return 2166 2167 cmdline = ['bundle', '--verbose'] 2168 parents = [hglib.escaperev(r.rev()) for r in self.repo[base].parents()] 2169 for p in parents: 2170 cmdline.extend(['--base', p]) 2171 if tip: 2172 cmdline.extend(['--rev', str(tip)]) 2173 else: 2174 cmdline.extend(['--rev', 'heads(descendants(%s))' % base]) 2175 cmdline.append(pycompat.unicode(file)) 2176 self._runCommand(cmdline) 2177 2178 @pyqtSlot() 2179 def copyPatch(self): 2180 # patches should be in chronological order 2181 revs = sorted(self._selectedIntRevisions()) 2182 cmdline = hglib.buildcmdargs('export', rev=hglib.compactrevs(revs)) 2183 sess = self._runCommand(cmdline) 2184 sess.setCaptureOutput(True) 2185 sess.commandFinished.connect(self._copyPatchOutputToClipboard) 2186 2187 @pyqtSlot(int) 2188 def _copyPatchOutputToClipboard(self, ret): 2189 if ret == 0: 2190 sess = self.sender() 2191 output = sess.readAll() 2192 mdata = QMimeData() 2193 mdata.setData('text/x-diff', output) # for lossless import 2194 mdata.setText(hglib.tounicode(bytes(output))) 2195 QApplication.clipboard().setMimeData(mdata) 2196 2197 def copyHash(self): 2198 clip = QApplication.clipboard() 2199 clip.setText( 2200 hglib.tounicode(binascii.hexlify(self.repo[self.rev].node()))) 2201 2202 def copyShortHash(self): 2203 clip = QApplication.clipboard() 2204 clip.setText( 2205 hglib.tounicode(nodemod.short(self.repo[self.rev].node()))) 2206 2207 @pyqtSlot() 2208 def copyGitHash(self): 2209 fullGitHash = hglib.gitcommit_full(self.repo[self.rev]) 2210 if fullGitHash is None: 2211 return 2212 clip = QApplication.clipboard() 2213 clip.setText(fullGitHash) 2214 2215 @pyqtSlot() 2216 def copyShortGitHash(self): 2217 shortGitHash = hglib.gitcommit_short(self.repo[self.rev]) 2218 if shortGitHash is None: 2219 return 2220 clip = QApplication.clipboard() 2221 clip.setText(shortGitHash) 2222 2223 def changePhase(self, phase): 2224 currentphase = self.repo[self.rev].phase() 2225 if currentphase == phase: 2226 # There is nothing to do, we are already in the target phase 2227 return 2228 phasestr = pycompat.sysstr(phases.phasenames[phase]) 2229 cmdline = ['phase', '--rev', '%s' % self.rev, '--%s' % phasestr] 2230 if currentphase < phase: 2231 # Ask the user if he wants to force the transition 2232 title = _('Backwards phase change requested') 2233 if currentphase == phases.draft and phase == phases.secret: 2234 # Here we are sure that the current phase is draft and the target phase is secret 2235 # Nevertheless we will not hard-code those phase names on the dialog strings to 2236 # make sure that the proper phase name translations are used 2237 main = _('Do you really want to make this revision <i>secret</i>?') 2238 text = _('Making a "<i>draft</i>" revision "<i>secret</i>" ' 2239 'is generally a safe operation.\n\n' 2240 'However, there are a few caveats:\n\n' 2241 '- "secret" revisions are not pushed. ' 2242 'This can cause you trouble if you\n' 2243 'refer to a secret subrepo revision.\n\n' 2244 '- If you pulled this revision from ' 2245 'a non publishing server it may be\n' 2246 'moved back to "<i>draft</i>" if you pull ' 2247 'again from that particular server.\n\n' 2248 'Please be careful!') 2249 labels = ((QMessageBox.Yes, _('&Make secret')), 2250 (QMessageBox.No, _('&Cancel'))) 2251 else: 2252 currentphasestr = pycompat.sysstr( 2253 phases.phasenames[currentphase]) 2254 main = _('Do you really want to <i>force</i> a backwards phase transition?') 2255 text = _('You are trying to move the phase of revision %d backwards,\n' 2256 'from "<i>%s</i>" to "<i>%s</i>".\n\n' 2257 'However, "<i>%s</i>" is a lower phase level than "<i>%s</i>".\n\n' 2258 'Moving the phase backwards is not recommended.\n' 2259 'For example, it may result in having multiple heads\nif you ' 2260 'modify a revision that you have already pushed\nto a server.\n\n' 2261 'Please be careful!') % (self.rev, currentphasestr, 2262 phasestr, phasestr, 2263 currentphasestr) 2264 labels = ((QMessageBox.Yes, _('&Force')), 2265 (QMessageBox.No, _('&Cancel'))) 2266 if not qtlib.QuestionMsgBox(title, main, text, 2267 labels=labels, parent=self): 2268 return 2269 cmdline.append('--force') 2270 self._runCommand(cmdline) 2271 2272 @pyqtSlot(QAction) 2273 def _changePhaseByMenu(self, action): 2274 phasenum = action.data() 2275 self.changePhase(phasenum) 2276 2277 @pyqtSlot() 2278 def compressRevisionsPair(self): 2279 reva, revb = self._selectedIntRevisionsPair() 2280 ctxa, ctxb = map(self.repo.hgchangectx, [reva, revb]) 2281 if ctxa.ancestor(ctxb).rev() == ctxb.rev(): 2282 revs = [reva, revb] 2283 elif ctxa.ancestor(ctxb).rev() == ctxa.rev(): 2284 revs = [revb, reva] 2285 else: 2286 InfoMsgBox(_('Unable to compress history'), 2287 _('Selected changeset pair not related')) 2288 return 2289 dlg = compress.CompressDialog(self._repoagent, revs, self) 2290 dlg.exec_() 2291 2292 def _pickRevision(self): 2293 """Pick selected revision on top of working directory parent""" 2294 opts = {'rev': self.rev} 2295 dlg = pick.PickDialog(self._repoagent, self, **opts) 2296 dlg.exec_() 2297 2298 def rebaseRevision(self): 2299 """Rebase selected revision on top of working directory parent""" 2300 opts = {'source' : self.rev, 'dest': self.repo[b'.'].rev()} 2301 dlg = rebase.RebaseDialog(self._repoagent, self, **opts) 2302 dlg.exec_() 2303 2304 @pyqtSlot() 2305 def rebaseSourceDestRevisionsPair(self): 2306 source, dest = self._selectedIntRevisionsPair() 2307 dlg = rebase.RebaseDialog(self._repoagent, self, 2308 source=source, dest=dest) 2309 dlg.exec_() 2310 2311 def qimportRevision(self): 2312 """QImport revision and all descendents to MQ""" 2313 if b'qparent' in self.repo.tags(): 2314 endrev = b'qparent' 2315 else: 2316 endrev = b'' 2317 2318 # Check whether there are existing patches in the MQ queue whose name 2319 # collides with the revisions that are going to be imported 2320 revList = self.repo.revs(b'%s::%s and not hidden()' % 2321 (hglib.fromunicode(str(self.rev)), endrev)) 2322 2323 if endrev and not revList: 2324 # There is a qparent but the revision list is empty 2325 # This means that the qparent is not a descendant of the 2326 # selected revision 2327 QMessageBox.warning(self, _('Cannot import selected revision'), 2328 _('The selected revision (rev #%d) cannot be imported ' 2329 'because it is not a descendant of ''qparent'' (rev #%d)') \ 2330 % (self.rev, hglib.revsymbol(self.repo, b'qparent').rev())) 2331 return 2332 2333 patchdir = hglib.tounicode(self.repo.vfs.join(b'patches')) 2334 def patchExists(p): 2335 return os.path.exists(os.path.join(patchdir, p)) 2336 2337 # Note that the following two arrays are both ordered by "rev" 2338 defaultPatchNames = ['%d.diff' % rev for rev in revList] 2339 defaultPatchesExist = [patchExists(p) for p in defaultPatchNames] 2340 if any(defaultPatchesExist): 2341 # We will qimport each revision one by one, starting from the newest 2342 # To do so, we will find a valid and unique patch name for each 2343 # revision that we must qimport (i.e. a filename that does not 2344 # already exist) 2345 # and then we will import them one by one starting from the newest 2346 # one, using these unique names 2347 def getUniquePatchName(baseName): 2348 maxRetries = 99 2349 for n in range(1, maxRetries): 2350 patchName = baseName + '_%02d.diff' % n 2351 if not patchExists(patchName): 2352 return patchName 2353 return baseName 2354 2355 patchNames = {} 2356 for n, rev in enumerate(revList): 2357 if defaultPatchesExist[n]: 2358 patchNames[rev] = getUniquePatchName(str(rev)) 2359 else: 2360 # The default name is safe 2361 patchNames[rev] = defaultPatchNames[n] 2362 2363 # qimport each revision individually, starting from the topmost one 2364 revList.reverse() 2365 cmdlines = [] 2366 for rev in revList: 2367 cmdlines.append(['qimport', '--rev', '%s' % rev, 2368 '--name', patchNames[rev]]) 2369 self._runCommandSequence(cmdlines) 2370 else: 2371 # There were no collisions with existing patch names, we can 2372 # simply qimport the whole revision set in a single go 2373 cmdline = ['qimport', '--rev', 2374 '%s::%s' % (self.rev, hglib.tounicode(endrev))] 2375 self._runCommand(cmdline) 2376 2377 def qfinishRevision(self): 2378 """Finish applied patches up to and including selected revision""" 2379 self._mqActions.finishRevision(hglib.tounicode(str(self.rev))) 2380 2381 @pyqtSlot() 2382 def qgotoParentRevision(self): 2383 """Apply an unapplied patch, or qgoto the parent of an applied patch""" 2384 self.qgotoRevision(self.repo[self.rev].p1().rev()) 2385 2386 @pyqtSlot() 2387 def qgotoSelectedRevision(self): 2388 self.qgotoRevision(self.rev) 2389 2390 def qgotoRevision(self, rev): 2391 """Make REV the top applied patch""" 2392 mqw = self._mqActions 2393 ctx = self.repo[rev] 2394 if b'qparent' in ctx.tags(): 2395 mqw.popAllPatches() 2396 else: 2397 mqw.gotoPatch(hglib.tounicode(ctx.thgmqpatchname())) 2398 2399 @pyqtSlot() 2400 def qdeletePatches(self): 2401 """Delete unapplied patch(es)""" 2402 patches = self._selectedUnappliedPatches() 2403 self._mqActions.deletePatches(patches) 2404 2405 @pyqtSlot() 2406 def qfoldPatches(self): 2407 patches = self._selectedUnappliedPatches() 2408 self._mqActions.foldPatches(patches) 2409 2410 def qrename(self): 2411 patches = self._selectedUnappliedPatches() 2412 revs = self._selectedIntRevisions() 2413 if patches: 2414 pname = patches[0] 2415 elif revs: 2416 pname = hglib.tounicode(self.repo[revs[0]].thgmqpatchname()) 2417 else: 2418 return 2419 self._mqActions.renamePatch(pname) 2420 2421 def _qpushRevision(self, move=False, exact=False): 2422 """QPush REV with the selected options""" 2423 ctx = self.repo[self.rev] 2424 patchname = hglib.tounicode(ctx.thgmqpatchname()) 2425 self._mqActions.pushPatch(patchname, move=move, exact=exact) 2426 2427 def qpushRevision(self): 2428 """Call qpush with no options""" 2429 self._qpushRevision(move=False, exact=False) 2430 2431 def qpushExactRevision(self): 2432 """Call qpush using the exact flag""" 2433 self._qpushRevision(exact=True) 2434 2435 def qpushMoveRevision(self): 2436 """Make REV the top applied patch""" 2437 self._qpushRevision(move=True) 2438 2439 def runCustomCommand(self, command, showoutput=False, workingdir='', 2440 files=None): 2441 # type: (Text, bool, Text, Optional[List[Text]]) -> Optional[Union[int, subprocess.Popen]] 2442 """Execute 'custom commands', on the selected repository""" 2443 # Perform variable expansion 2444 # This is done in two steps: 2445 # 1. Expand environment variables 2446 if not pycompat.ispy3: 2447 command = hglib.fromunicode(command) 2448 command = os.path.expandvars(command).strip() 2449 if not command: 2450 InfoMsgBox(_('Invalid command'), 2451 _('The selected command is empty')) 2452 return 2453 if not pycompat.ispy3: 2454 workingdir = hglib.fromunicode(workingdir) 2455 if workingdir: 2456 workingdir = os.path.expandvars(workingdir).strip() 2457 2458 # 2. Expand internal workbench variables 2459 def filelist2str(filelist): 2460 # type: (List[Text]) -> Text 2461 return hglib.tounicode(b' '.join( 2462 procutil.shellquote( 2463 os.path.normpath(self.repo.wjoin(hglib.fromunicode(filename)))) 2464 for filename in filelist 2465 )) 2466 2467 if files is None: 2468 files = [] 2469 2470 selection = self.repoview.selectedRevisions() 2471 2472 def selectionfiles2str(source): 2473 # type: (Text) -> Text 2474 files = set() 2475 for rev in selection: 2476 files.update( 2477 hglib.tounicode(f) 2478 for f in getattr(self.repo[rev], source)() 2479 ) 2480 return filelist2str(sorted(files)) 2481 2482 vars = { 2483 'ROOT': lambda: hglib.tounicode(self.repo.root), 2484 'REVID': lambda: '+'.join(str(self.repo[rev]) for rev in selection), 2485 'REV': lambda: '+'.join(str(rev) for rev in selection), 2486 'FILES': lambda: selectionfiles2str('files'), 2487 'ALLFILES': lambda: selectionfiles2str('manifest'), 2488 'SELECTEDFILES': lambda: filelist2str(files), 2489 } 2490 2491 if len(selection) == 2: 2492 pairvars = { 2493 'REV_A': lambda: selection[0], 2494 'REV_B': lambda: selection[1], 2495 'REVID_A': lambda: str(self.repo[selection[0]]), 2496 'REVID_B': lambda: str(self.repo[selection[1]]), 2497 } 2498 vars.update(pairvars) 2499 2500 for var in vars: 2501 bracedvar = '{%s}' % var 2502 if bracedvar in command: 2503 command = command.replace(bracedvar, str(vars[var]())) 2504 if workingdir and bracedvar in workingdir: 2505 workingdir = workingdir.replace(bracedvar, str(vars[var]())) 2506 if not workingdir: 2507 workingdir = hglib.tounicode(self.repo.root) 2508 2509 # Show the Output Log if configured to do so 2510 if showoutput: 2511 self.makeLogVisible.emit(True) 2512 2513 # If the user wants to run mercurial, 2514 # do so via our usual runCommand method 2515 cmd = shlex.split(command) 2516 cmdtype = cmd[0].lower() 2517 if cmdtype == 'hg': 2518 sess = self._runCommand(pycompat.maplist(hglib.tounicode, cmd[1:])) 2519 sess.commandFinished.connect(self._notifyWorkingDirChanges) 2520 return 2521 elif cmdtype == 'thg': 2522 cmd = cmd[1:] 2523 if '--repository' in cmd: 2524 _ui = hglib.loadui() 2525 else: 2526 cmd += ['--repository', self.repo.root] 2527 _ui = self.repo.ui.copy() 2528 _ui.ferr = pycompat.bytesio() 2529 # avoid circular import of hgqt.run by importing it inplace 2530 from . import run 2531 cmdb = [] 2532 for part in cmd: 2533 if isinstance(part, pycompat.unicode): 2534 cmdb.append(hglib.fromunicode(part)) 2535 else: 2536 cmdb.append(part) 2537 res = run.dispatch(cmdb, u=_ui) 2538 if res: 2539 errormsg = _ui.ferr.getvalue().strip() 2540 if errormsg: 2541 errormsg = \ 2542 _('The following error message was returned:' 2543 '\n\n<b>%s</b>') % hglib.tounicode(errormsg) 2544 errormsg +=\ 2545 _('\n\nPlease check that the "thg" command is valid.') 2546 qtlib.ErrorMsgBox( 2547 _('Failed to execute custom TortoiseHg command'), 2548 _('The command "%s" failed (code %d).') 2549 % (hglib.tounicode(command), res), errormsg) 2550 return res 2551 2552 # Otherwise, run the selected command in the background 2553 try: 2554 res = subprocess.Popen(command, cwd=workingdir, shell=True) 2555 except OSError as ex: 2556 res = 1 2557 qtlib.ErrorMsgBox(_('Failed to execute custom command'), 2558 _('The command "%s" could not be executed.') % hglib.tounicode(command), 2559 _('The following error message was returned:\n\n"%s"\n\n' 2560 'Please check that the command path is valid and ' 2561 'that it is a valid application') % hglib.tounicode(ex.strerror)) 2562 return res 2563 2564 @pyqtSlot(QAction) 2565 def _runCustomCommandByMenu(self, action): 2566 command, showoutput, workingdir = action.data() 2567 self.runCustomCommand(command, showoutput, workingdir) 2568 2569 @pyqtSlot(str, list) 2570 def handleRunCustomCommandRequest(self, toolname, files): 2571 tools, toollist = hglib.tortoisehgtools(self.repo.ui) 2572 if not tools or toolname not in toollist: 2573 return 2574 toolname = str(toolname) 2575 command = tools[toolname].get('command', '') 2576 showoutput = tools[toolname].get('showoutput', False) 2577 workingdir = tools[toolname].get('workingdir', '') 2578 self.runCustomCommand(command, showoutput, workingdir, files) 2579 2580 def _runCommand(self, cmdline): 2581 sess = self._repoagent.runCommand(cmdline, self) 2582 self._handleNewCommand(sess) 2583 return sess 2584 2585 def _runCommandSequence(self, cmdlines): 2586 sess = self._repoagent.runCommandSequence(cmdlines, self) 2587 self._handleNewCommand(sess) 2588 return sess 2589 2590 def _handleNewCommand(self, sess): 2591 self.clearInfoBar() 2592 sess.outputReceived.connect(self._repoviewFrame.showOutput) 2593 2594 @pyqtSlot() 2595 def _notifyWorkingDirChanges(self): 2596 shlib.shell_notify([self.repo.root]) 2597 2598 @pyqtSlot() 2599 def _refreshCommitTabIfNeeded(self): 2600 """Refresh the Commit tab if the user settings require it""" 2601 if self.taskTabsWidget.currentIndex() != self._namedTabs['commit']: 2602 return 2603 2604 refreshwd = self._repoagent.configString( 2605 'tortoisehg', 'refreshwdstatus') 2606 # Valid refreshwd values are 'auto', 'always' and 'alwayslocal' 2607 if refreshwd != 'auto': 2608 if refreshwd == 'always' \ 2609 or paths.is_on_fixed_drive(self.repo.root): 2610 self.commitDemand.forward('refreshWctx') 2611 2612 2613class LightRepoWindow(QMainWindow): 2614 def __init__(self, actionregistry, repoagent): 2615 super(LightRepoWindow, self).__init__() 2616 self._repoagent = repoagent 2617 self.setIconSize(qtlib.smallIconSize()) 2618 2619 repo = repoagent.rawRepo() 2620 val = repo.ui.config(b'tortoisehg', b'tasktabs').lower() 2621 if val not in (b'east', b'west'): 2622 repo.ui.setconfig(b'tortoisehg', b'tasktabs', b'east') 2623 rw = RepoWidget(actionregistry, repoagent, self) 2624 self.setCentralWidget(rw) 2625 2626 self._edittbar = tbar = self.addToolBar(_('&Edit Toolbar')) 2627 tbar.setObjectName('edittbar') 2628 a = tbar.addAction(qtlib.geticon('view-refresh'), _('&Refresh')) 2629 a.setShortcuts(QKeySequence.Refresh) 2630 a.triggered.connect(self.refresh) 2631 2632 tbar = rw.filterBar() 2633 tbar.setObjectName('filterbar') 2634 tbar.setWindowTitle(_('&Filter Toolbar')) 2635 self.addToolBar(tbar) 2636 2637 stbar = cmdui.ThgStatusBar(self) 2638 repoagent.progressReceived.connect(stbar.setProgress) 2639 rw.showMessageSignal.connect(stbar.showMessage) 2640 rw.progress.connect(stbar.progress) 2641 self.setStatusBar(stbar) 2642 2643 s = QSettings() 2644 s.beginGroup('LightRepoWindow') 2645 self.restoreGeometry(qtlib.readByteArray(s, 'geometry')) 2646 self.restoreState(qtlib.readByteArray(s, 'windowState')) 2647 stbar.setVisible(qtlib.readBool(s, 'statusBar', True)) 2648 s.endGroup() 2649 2650 self.setWindowTitle(_('TortoiseHg: %s') % repoagent.displayName()) 2651 2652 def createPopupMenu(self): 2653 menu = super(LightRepoWindow, self).createPopupMenu() 2654 assert menu # should have toolbar 2655 stbar = self.statusBar() 2656 a = menu.addAction(_('S&tatus Bar')) 2657 a.setCheckable(True) 2658 a.setChecked(stbar.isVisibleTo(self)) 2659 a.triggered.connect(stbar.setVisible) 2660 menu.addSeparator() 2661 menu.addAction(_('&Settings'), self._editSettings) 2662 return menu 2663 2664 def closeEvent(self, event): 2665 rw = self.centralWidget() 2666 if not rw.closeRepoWidget(): 2667 event.ignore() 2668 return 2669 s = QSettings() 2670 s.beginGroup('LightRepoWindow') 2671 s.setValue('geometry', self.saveGeometry()) 2672 s.setValue('windowState', self.saveState()) 2673 s.setValue('statusBar', self.statusBar().isVisibleTo(self)) 2674 s.endGroup() 2675 event.accept() 2676 2677 @pyqtSlot() 2678 def refresh(self): 2679 self._repoagent.pollStatus() 2680 rw = self.centralWidget() 2681 rw.reload() 2682 2683 def setSyncUrl(self, url): 2684 rw = self.centralWidget() 2685 rw.setSyncUrl(url) 2686 2687 @pyqtSlot() 2688 def _editSettings(self): 2689 dlg = settings.SettingsDialog(parent=self) 2690 dlg.exec_() 2691