1# mq.py - TortoiseHg MQ widget 2# 3# Copyright 2011 Steve Borho <steve@borho.org> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8from __future__ import absolute_import 9 10import os 11import re 12 13from .qtcore import ( 14 QAbstractListModel, 15 QByteArray, 16 QMimeData, 17 QModelIndex, 18 QObject, 19 QPoint, 20 QTimer, 21 QUrl, 22 Qt, 23 pyqtSignal, 24 pyqtSlot, 25) 26from .qtgui import ( 27 QAbstractItemView, 28 QAction, 29 QCheckBox, 30 QComboBox, 31 QDialog, 32 QDialogButtonBox, 33 QDockWidget, 34 QFont, 35 QFrame, 36 QHBoxLayout, 37 QInputDialog, 38 QListView, 39 QMenu, 40 QMessageBox, 41 QPushButton, 42 QToolBar, 43 QToolButton, 44 QVBoxLayout, 45 QWidget, 46) 47 48from mercurial import ( 49 error, 50 pycompat, 51) 52 53from ..util import hglib 54from ..util.i18n import _ 55from . import ( 56 cmdcore, 57 cmdui, 58 commit, 59 qtlib, 60 qdelete, 61 qfold, 62 rejects, 63) 64 65if hglib.TYPE_CHECKING: 66 from typing import ( 67 Optional, 68 ) 69 70def _checkForRejects(repo, rawoutput, parent=None): 71 """Parse output of qpush/qpop to resolve hunk failure manually""" 72 rejre = re.compile(r'saving rejects to file (.*)\.rej') 73 rejfiles = dict((m.group(1), False) for m in rejre.finditer(rawoutput)) 74 for ufile in sorted(rejfiles): 75 wfile = hglib.fromunicode(ufile) 76 if not os.path.exists(repo.wjoin(wfile)): 77 continue 78 if qtlib.QuestionMsgBox(_('Manually resolve rejected chunks?'), 79 _('%s had rejected chunks, edit patched ' 80 'file together with rejects?') % ufile, 81 parent=parent): 82 dlg = rejects.RejectsDialog(repo.ui, repo.wjoin(wfile), parent) 83 r = dlg.exec_() 84 rejfiles[ufile] = (r == QDialog.Accepted) 85 86 # empty rejfiles means we failed to parse output message 87 return bool(rejfiles) and all(rejfiles.values()) 88 89class QueueManagementActions(QObject): 90 """Container for patch queue management actions""" 91 92 def __init__(self, parent=None): 93 super(QueueManagementActions, self).__init__(parent) 94 assert parent is None or isinstance(parent, QWidget), repr(parent) 95 self._repoagent = None 96 self._cmdsession = cmdcore.nullCmdSession() 97 98 self._actions = { 99 'commitQueue': QAction(_('&Commit to Queue...'), self), 100 'createQueue': QAction(_('Create &New Queue...'), self), 101 'renameQueue': QAction(_('&Rename Active Queue...'), self), 102 'deleteQueue': QAction(_('&Delete Queue...'), self), 103 'purgeQueue': QAction(_('&Purge Queue...'), self), 104 } 105 for name, action in self._actions.items(): 106 action.triggered.connect(getattr(self, '_' + name)) 107 self._updateActions() 108 109 def _parentWidget(self): 110 # type: () -> Optional[QWidget] 111 p = self.parent() 112 assert p is None or isinstance(p, QWidget) 113 return p 114 115 def setRepoAgent(self, repoagent): 116 self._repoagent = repoagent 117 self._updateActions() 118 119 def _updateActions(self): 120 enabled = bool(self._repoagent) and self._cmdsession.isFinished() 121 for action in self._actions.values(): 122 action.setEnabled(enabled) 123 124 def createMenu(self, parent=None): 125 menu = QMenu(parent) 126 menu.addAction(self._actions['commitQueue']) 127 menu.addSeparator() 128 for name in ['createQueue', 'renameQueue', 'deleteQueue', 'purgeQueue']: 129 menu.addAction(self._actions[name]) 130 return menu 131 132 @pyqtSlot() 133 def _commitQueue(self): 134 assert self._repoagent 135 repo = self._repoagent.rawRepo() 136 if os.path.isdir(repo.mq.join(b'.hg')): 137 self._launchCommitDialog() 138 return 139 if not self._cmdsession.isFinished(): 140 return 141 142 cmdline = hglib.buildcmdargs('init', mq=True) 143 self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) 144 sess.commandFinished.connect(self._onQueueRepoInitialized) 145 self._updateActions() 146 147 @pyqtSlot(int) 148 def _onQueueRepoInitialized(self, ret): 149 if ret == 0: 150 self._launchCommitDialog() 151 self._onCommandFinished(ret) 152 153 def _launchCommitDialog(self): 154 if not self._repoagent: 155 return 156 repo = self._repoagent.rawRepo() 157 repoagent = self._repoagent.subRepoAgent(hglib.tounicode(repo.mq.path)) 158 dlg = commit.CommitDialog(repoagent, [], {}, self._parentWidget()) 159 dlg.finished.connect(dlg.deleteLater) 160 dlg.exec_() 161 162 def switchQueue(self, name): 163 return self._runQqueue(None, name) 164 165 @pyqtSlot() 166 def _createQueue(self): 167 name = self._getNewName(_('Create Patch Queue'), 168 _('New patch queue name'), 169 _('Create')) 170 if name: 171 self._runQqueue('create', name) 172 173 @pyqtSlot() 174 def _renameQueue(self): 175 curname = self._activeName() 176 newname = self._getNewName(_('Rename Patch Queue'), 177 _("Rename patch queue '%s' to") % curname, 178 _('Rename')) 179 if newname and curname != newname: 180 self._runQqueue('rename', newname) 181 182 @pyqtSlot() 183 def _deleteQueue(self): 184 name = self._getExistingName(_('Delete Patch Queue'), 185 _('Delete reference to'), 186 _('Delete')) 187 if name: 188 self._runQqueueInactive('delete', name) 189 190 @pyqtSlot() 191 def _purgeQueue(self): 192 name = self._getExistingName(_('Purge Patch Queue'), 193 _('Remove patch directory of'), 194 _('Purge')) 195 if name: 196 self._runQqueueInactive('purge', name) 197 198 def _activeName(self): 199 assert self._repoagent 200 repo = self._repoagent.rawRepo() 201 return hglib.tounicode(repo.thgactivemqname) 202 203 def _existingNames(self): 204 assert self._repoagent 205 return hglib.getqqueues(self._repoagent.rawRepo()) 206 207 def _getNewName(self, title, labeltext, oktext): 208 dlg = QInputDialog(self._parentWidget()) 209 dlg.setWindowTitle(title) 210 dlg.setLabelText(labeltext) 211 dlg.setOkButtonText(oktext) 212 if dlg.exec_(): 213 return dlg.textValue() 214 215 def _getExistingName(self, title, labeltext, oktext): 216 dlg = QInputDialog(self._parentWidget()) 217 dlg.setWindowTitle(title) 218 dlg.setLabelText(labeltext) 219 dlg.setOkButtonText(oktext) 220 dlg.setComboBoxEditable(False) 221 dlg.setComboBoxItems(self._existingNames()) 222 dlg.setTextValue(self._activeName()) 223 if dlg.exec_(): 224 return dlg.textValue() 225 226 def abort(self): 227 self._cmdsession.abort() 228 229 def _runQqueue(self, op, name): 230 """Execute qqueue operation against the specified queue""" 231 assert self._repoagent 232 if not self._cmdsession.isFinished(): 233 return cmdcore.nullCmdSession() 234 235 opts = {} 236 if op: 237 opts[op] = True 238 cmdline = hglib.buildcmdargs('qqueue', name, **opts) 239 self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) 240 sess.commandFinished.connect(self._onCommandFinished) 241 self._updateActions() 242 return sess 243 244 def _runQqueueInactive(self, op, name): 245 """Execute qqueue operation after inactivating the specified queue""" 246 assert self._repoagent 247 if not self._cmdsession.isFinished(): 248 return cmdcore.nullCmdSession() 249 250 if name != self._activeName(): 251 return self._runQqueue(op, name) 252 253 sacrifices = [n for n in self._existingNames() if n != name] 254 if not sacrifices: 255 return self._runQqueue(op, name) # will exit with error 256 257 opts = {} 258 if op: 259 opts[op] = True 260 cmdlines = [hglib.buildcmdargs('qqueue', sacrifices[0]), 261 hglib.buildcmdargs('qqueue', name, **opts)] 262 self._cmdsession = sess = self._repoagent.runCommandSequence(cmdlines, 263 self) 264 sess.commandFinished.connect(self._onCommandFinished) 265 self._updateActions() 266 return sess 267 268 @pyqtSlot(int) 269 def _onCommandFinished(self, ret): 270 if ret != 0: 271 cmdui.errorMessageBox(self._cmdsession, self._parentWidget()) 272 self._updateActions() 273 274 275class PatchQueueActions(QObject): 276 """Container for MQ patch actions except for queue management""" 277 278 def __init__(self, parent=None): 279 super(PatchQueueActions, self).__init__(parent) 280 assert parent is None or isinstance(parent, QWidget), repr(parent) 281 self._repoagent = None 282 self._cmdsession = cmdcore.nullCmdSession() 283 self._opts = {'force': False, 'keep_changes': False} 284 285 def _parentWidget(self): 286 # type: () -> Optional[QWidget] 287 p = self.parent() 288 assert p is None or isinstance(p, QWidget) 289 return p 290 291 def setRepoAgent(self, repoagent): 292 self._repoagent = repoagent 293 294 def gotoPatch(self, patch): 295 opts = {'force': self._opts['force'], 296 'keep_changes': self._opts['keep_changes']} 297 return self._runCommand('qgoto', [patch], opts, self._onPushFinished) 298 299 @pyqtSlot() 300 def pushPatch(self, patch=None, move=False, exact=False): 301 return self._runPush(patch, move=move, exact=exact) 302 303 @pyqtSlot() 304 def pushAllPatches(self): 305 return self._runPush(None, all=True) 306 307 def _runPush(self, patch, **opts): 308 opts['force'] = self._opts['force'] 309 if not opts.get('exact'): 310 # --exact and --keep-changes cannot be used simultaneously 311 # thus we ignore the "default" setting for --keep-changes 312 # when --exact is explicitly set 313 opts['keep_changes'] = self._opts['keep_changes'] 314 return self._runCommand('qpush', [patch], opts, self._onPushFinished) 315 316 @pyqtSlot() 317 def popPatch(self, patch=None): 318 return self._runPop(patch) 319 320 @pyqtSlot() 321 def popAllPatches(self): 322 return self._runPop(None, all=True) 323 324 def _runPop(self, patch, **opts): 325 opts['force'] = self._opts['force'] 326 opts['keep_changes'] = self._opts['keep_changes'] 327 return self._runCommand('qpop', [patch], opts) 328 329 def finishRevision(self, rev): 330 revspec = hglib.formatrevspec('qbase::%s', rev) 331 return self._runCommand('qfinish', [revspec], {}) 332 333 def deletePatches(self, patches): 334 dlg = qdelete.QDeleteDialog(patches, self._parentWidget()) 335 if not dlg.exec_(): 336 return cmdcore.nullCmdSession() 337 return self._runCommand('qdelete', patches, dlg.options()) 338 339 def foldPatches(self, patches): 340 lpatches = pycompat.maplist(hglib.fromunicode, patches) 341 dlg = qfold.QFoldDialog(self._repoagent, lpatches, self._parentWidget()) 342 dlg.finished.connect(dlg.deleteLater) 343 if not dlg.exec_(): 344 return cmdcore.nullCmdSession() 345 return self._runCommand('qfold', dlg.patches(), dlg.options()) 346 347 def renamePatch(self, patch): 348 newname = self._getNewName(_('Rename Patch'), 349 _('Rename patch <b>%s</b> to:') % patch, 350 patch, _('Rename')) 351 if not newname or patch == newname: 352 return cmdcore.nullCmdSession() 353 return self._runCommand('qrename', [patch, newname], {}) 354 355 def guardPatch(self, patch, guards): 356 args = [patch] 357 args.extend(guards) 358 opts = {'none': not guards} 359 return self._runCommand('qguard', args, opts) 360 361 def selectGuards(self, guards): 362 opts = {'none': not guards} 363 return self._runCommand('qselect', guards, opts) 364 365 def _getNewName(self, title, labeltext, curvalue, oktext): 366 dlg = QInputDialog(self._parentWidget()) 367 dlg.setWindowTitle(title) 368 dlg.setLabelText(labeltext) 369 dlg.setTextValue(curvalue) 370 dlg.setOkButtonText(oktext) 371 if dlg.exec_(): 372 return pycompat.unicode(dlg.textValue()) 373 374 def abort(self): 375 self._cmdsession.abort() 376 377 def _runCommand(self, name, args, opts, finishslot=None): 378 assert self._repoagent 379 if not self._cmdsession.isFinished(): 380 return cmdcore.nullCmdSession() 381 cmdline = hglib.buildcmdargs(name, *args, **opts) 382 self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) 383 sess.commandFinished.connect(finishslot or self._onCommandFinished) 384 return sess 385 386 @pyqtSlot(int) 387 def _onPushFinished(self, ret): 388 if ret == 2 and self._repoagent: 389 repo = self._repoagent.rawRepo() 390 output = self._cmdsession.warningString() 391 if _checkForRejects(repo, output, self._parentWidget()): 392 ret = 0 # no further error dialog 393 if ret != 0: 394 cmdui.errorMessageBox(self._cmdsession, self._parentWidget()) 395 396 @pyqtSlot(int) 397 def _onCommandFinished(self, ret): 398 if ret != 0: 399 cmdui.errorMessageBox(self._cmdsession, self._parentWidget()) 400 401 @pyqtSlot() 402 def launchOptionsDialog(self): 403 dlg = OptionsDialog(self._opts, self._parentWidget()) 404 dlg.finished.connect(dlg.deleteLater) 405 dlg.setWindowFlags(Qt.Sheet) 406 dlg.setWindowModality(Qt.WindowModal) 407 if dlg.exec_() == QDialog.Accepted: 408 self._opts.update(dlg.outopts) 409 410 411class PatchQueueModel(QAbstractListModel): 412 """List of all patches in active queue""" 413 414 def __init__(self, repoagent, parent=None): 415 super(PatchQueueModel, self).__init__(parent) 416 self._repoagent = repoagent 417 self._repoagent.repositoryChanged.connect(self._updateCache) 418 self._series = [] 419 self._seriesguards = [] 420 self._statusmap = {} # patch: applied/guarded/unguarded 421 self._buildCache() 422 423 @pyqtSlot() 424 def _updateCache(self): 425 # optimize range of changed signals if necessary 426 repo = self._repoagent.rawRepo() 427 if self._series == repo.mq.series[::-1]: 428 self._buildCache() 429 else: 430 self._updateCacheAndLayout() 431 self.dataChanged.emit(self.index(0), self.index(self.rowCount() - 1)) 432 433 def _updateCacheAndLayout(self): 434 self.layoutAboutToBeChanged.emit() 435 oldcount = len(self._series) 436 oldindexes = [(oi, self._series[oi.row()]) 437 for oi in self.persistentIndexList()] 438 self._buildCache() 439 440 mappedindexes = [] # old -> new 441 missngindexes = [] # old 442 for oi, patch in oldindexes: 443 try: 444 ni = self.index(self._series.index(patch), oi.column()) 445 mappedindexes.append((oi, ni)) 446 except ValueError: 447 missngindexes.append(oi) 448 # if no indexes are moved, assume missing ones were renamed 449 if (missngindexes and oldcount == len(self._series) 450 and all(oi.row() == ni.row() for oi, ni in mappedindexes)): 451 mappedindexes.extend((oi, self.index(oi.row(), oi.column())) 452 for oi in missngindexes) 453 del missngindexes[:] 454 455 for oi, ni in mappedindexes: 456 self.changePersistentIndex(oi, ni) 457 for oi in missngindexes: 458 self.changePersistentIndex(oi, QModelIndex()) 459 self.layoutChanged.emit() 460 461 def _buildCache(self): 462 repo = self._repoagent.rawRepo() 463 self._series = repo.mq.series[::-1] 464 self._seriesguards = [list(xs) for xs in reversed(repo.mq.seriesguards)] 465 466 self._statusmap.clear() 467 self._statusmap.update((p.name, 'applied') for p in repo.mq.applied) 468 for i, patch in enumerate(repo.mq.series): 469 if patch in self._statusmap: 470 continue # applied 471 pushable, why = repo.mq.pushable(i) 472 if not pushable: 473 self._statusmap[patch] = 'guarded' 474 elif why is not None: 475 self._statusmap[patch] = 'unguarded' 476 477 def data(self, index, role=Qt.DisplayRole): 478 if not index.isValid(): 479 return 480 if role in (Qt.DisplayRole, Qt.EditRole): 481 return self.patchName(index) 482 if role == Qt.DecorationRole: 483 return self._statusIcon(index) 484 if role == Qt.FontRole: 485 return self._statusFont(index) 486 if role == Qt.ToolTipRole: 487 return self._toolTip(index) 488 489 def flags(self, index): 490 flags = super(PatchQueueModel, self).flags(index) 491 if not index.isValid(): 492 return flags | Qt.ItemIsDropEnabled # insertion point 493 patch = self._series[index.row()] 494 if self._statusmap.get(patch) != 'applied': 495 flags |= Qt.ItemIsDragEnabled 496 return flags 497 498 def rowCount(self, parent=QModelIndex()): 499 if parent.isValid(): 500 return 0 501 return len(self._series) 502 503 def appliedCount(self): 504 return sum(s == 'applied' for s in self._statusmap.values()) 505 506 def patchName(self, index): 507 if not index.isValid(): 508 return '' 509 return hglib.tounicode(self._series[index.row()]) 510 511 def patchGuards(self, index): 512 if not index.isValid(): 513 return [] 514 return pycompat.maplist(hglib.tounicode, 515 self._seriesguards[index.row()]) 516 517 def isApplied(self, index): 518 if not index.isValid(): 519 return False 520 patch = self._series[index.row()] 521 return self._statusmap.get(patch) == 'applied' 522 523 def _statusIcon(self, index): 524 assert index.isValid() 525 patch = self._series[index.row()] 526 status = self._statusmap.get(patch) 527 if status: 528 return qtlib.geticon('hg-patch-%s' % status) 529 530 def _statusFont(self, index): 531 assert index.isValid() 532 patch = self._series[index.row()] 533 status = self._statusmap.get(patch) 534 if status not in ('applied', 'guarded'): 535 return 536 f = QFont() 537 f.setBold(status == 'applied') 538 f.setItalic(status == 'guarded') 539 return f 540 541 def _toolTip(self, index): 542 assert index.isValid() 543 repo = self._repoagent.rawRepo() 544 patch = self._series[index.row()] 545 try: 546 if patch in repo.thgmqunappliedpatches: 547 ctx = repo[patch] 548 else: 549 ctx = hglib.revsymbol(repo, patch) 550 except error.RepoLookupError: 551 # cache not updated after qdelete or qfinish 552 return 553 guards = self.patchGuards(index) 554 # longsummary() is on the thgrepo wrapped context 555 return '%s: %s\n%s' % (self.patchName(index), 556 guards and ', '.join(guards) or _('no guards'), 557 ctx.longsummary()) # pytype: disable=attribute-error 558 559 def topAppliedIndex(self, column=0): 560 """Index of the last applied, i.e. qtip, patch""" 561 for row, patch in enumerate(self._series): 562 if self._statusmap.get(patch) == 'applied': 563 return self.index(row, column) 564 return QModelIndex() 565 566 def mimeTypes(self): 567 return ['application/vnd.thg.mq.series', 'text/uri-list'] 568 569 def mimeData(self, indexes): 570 repo = self._repoagent.rawRepo() 571 # in the same order as series file 572 patches = [self._series[i.row()] 573 for i in sorted(indexes, reverse=True)] 574 data = QMimeData() 575 data.setData('application/vnd.thg.mq.series', 576 QByteArray(b'\n'.join(patches) + b'\n')) 577 data.setUrls([QUrl.fromLocalFile(hglib.tounicode(repo.mq.join(p))) 578 for p in patches]) 579 return data 580 581 def dropMimeData(self, data, action, row, column, parent): 582 if (action != Qt.MoveAction 583 or not data.hasFormat('application/vnd.thg.mq.series') 584 or row < 0 or parent.isValid()): 585 return False 586 587 repo = self._repoagent.rawRepo() 588 qtiprow = len(self._series) - repo.mq.seriesend(True) 589 if row > qtiprow: 590 return False 591 if row < len(self._series): 592 after = hglib.tounicode(self._series[row]) 593 else: 594 after = None # next to working rev 595 patches = hglib.tounicode( 596 bytes(data.data('application/vnd.thg.mq.series'))).splitlines() 597 cmdline = hglib.buildcmdargs('qreorder', after=after, *patches) 598 self._repoagent.runCommand(cmdline) 599 return True 600 601 def supportedDropActions(self): 602 return Qt.MoveAction 603 604 605class MQPatchesWidget(QDockWidget): 606 patchSelected = pyqtSignal(str) 607 608 def __init__(self, actionregistry, parent): 609 QDockWidget.__init__(self, parent) 610 self._actionregistry = actionregistry 611 self._repoagent = None 612 613 self.setFeatures(QDockWidget.DockWidgetClosable | 614 QDockWidget.DockWidgetMovable | 615 QDockWidget.DockWidgetFloatable) 616 self.setWindowTitle(_('Patch Queue')) 617 618 w = QWidget() 619 mainlayout = QVBoxLayout() 620 mainlayout.setContentsMargins(0, 0, 0, 0) 621 w.setLayout(mainlayout) 622 self.setWidget(w) 623 624 self._patchActions = PatchQueueActions(self) 625 626 # top toolbar 627 w = QWidget() 628 tbarhbox = QHBoxLayout() 629 tbarhbox.setContentsMargins(0, 0, 0, 0) 630 w.setLayout(tbarhbox) 631 mainlayout.addWidget(w) 632 633 # TODO: move QAction instances to PatchQueueActions 634 self._qpushAct = a = QAction(qtlib.geticon('hg-qpush'), '', self) 635 a.setToolTip(_('Apply one patch')) 636 self._qpushAllAct = a = QAction(qtlib.geticon('hg-qpush-all'), '', self) 637 a.setToolTip(_('Apply all patches')) 638 self._qpopAct = a = QAction(qtlib.geticon('hg-qpop'), '', self) 639 a.setToolTip(_('Unapply one patch')) 640 self._qpopAllAct = a = QAction(qtlib.geticon('hg-qpop-all'), '', self) 641 a.setToolTip(_('Unapply all patches')) 642 self._qgotoAct = QAction(qtlib.geticon('hg-qgoto'), '', self) 643 self._qpushMoveAct = a = QAction(qtlib.geticon('hg-qpush-move'), '', 644 self) 645 a.setToolTip(_('Apply only the selected patch')) 646 self._qfinishAct = a = QAction(qtlib.geticon('qfinish'), '', self) 647 a.setToolTip(_('Move applied patches into repository history')) 648 self._qdeleteAct = a = QAction(qtlib.geticon('hg-qdelete'), '', self) 649 a.setToolTip(_('Delete selected patches')) 650 self._qrenameAct = a = QAction(self) 651 self._setGuardsAct = a = QAction(qtlib.geticon('hg-qguard'), '', self) 652 a.setToolTip(_('Configure guards for selected patch')) 653 654 namedActions = { 655 'PatchQueue.pushOnePatch': self._qpushAct, 656 'PatchQueue.pushAllPatches': self._qpushAllAct, 657 'PatchQueue.popOnePatch': self._qpopAct, 658 'PatchQueue.popAllPatches': self._qpopAllAct, 659 'PatchQueue.goToPatch': self._qgotoAct, 660 'PatchQueue.pushMovePatch': self._qpushMoveAct, 661 'PatchQueue.finishRevision': self._qfinishAct, 662 'PatchQueue.deletePatches': self._qdeleteAct, 663 'PatchQueue.renamePatch': self._qrenameAct, 664 'PatchQueue.guardPatch': self._setGuardsAct, 665 } 666 for n, a in namedActions.items(): 667 self.addAction(a) 668 a.setShortcutContext(Qt.WidgetWithChildrenShortcut) 669 self._actionregistry.registerAction(n, a) 670 671 tbar = QToolBar(_('Patch Queue Actions Toolbar'), self) 672 tbar.setIconSize(qtlib.smallIconSize()) 673 tbarhbox.addWidget(tbar) 674 tbar.addAction(self._qpushAct) 675 tbar.addAction(self._qpushAllAct) 676 tbar.addSeparator() 677 tbar.addAction(self._qpopAct) 678 tbar.addAction(self._qpopAllAct) 679 tbar.addSeparator() 680 tbar.addAction(self._qpushMoveAct) 681 tbar.addSeparator() 682 tbar.addAction(self._qfinishAct) 683 tbar.addAction(self._qdeleteAct) 684 tbar.addSeparator() 685 tbar.addAction(self._setGuardsAct) 686 687 self._queueFrame = w = QFrame() 688 mainlayout.addWidget(w) 689 690 # Patch Queue Frame 691 layout = QVBoxLayout() 692 layout.setSpacing(5) 693 layout.setContentsMargins(0, 0, 0, 0) 694 self._queueFrame.setLayout(layout) 695 696 qqueuehbox = QHBoxLayout() 697 qqueuehbox.setSpacing(5) 698 layout.addLayout(qqueuehbox) 699 self._qqueueComboWidget = QComboBox(self) 700 qqueuehbox.addWidget(self._qqueueComboWidget, 1) 701 self._qqueueConfigBtn = QToolButton(self) 702 self._qqueueConfigBtn.setText('...') 703 self._qqueueConfigBtn.setPopupMode(QToolButton.InstantPopup) 704 qqueuehbox.addWidget(self._qqueueConfigBtn) 705 706 self._qqueueActions = QueueManagementActions(self) 707 self._qqueueConfigBtn.setMenu(self._qqueueActions.createMenu(self)) 708 709 self._queueListWidget = QListView(self) 710 self._queueListWidget.setDragDropMode(QAbstractItemView.InternalMove) 711 self._queueListWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) 712 self._queueListWidget.setIconSize(qtlib.smallIconSize() * 0.75) 713 self._queueListWidget.setSelectionMode( 714 QAbstractItemView.ExtendedSelection) 715 self._queueListWidget.setContextMenuPolicy(Qt.CustomContextMenu) 716 self._queueListWidget.customContextMenuRequested.connect( 717 self._onMenuRequested) 718 layout.addWidget(self._queueListWidget, 1) 719 720 bbarhbox = QHBoxLayout() 721 bbarhbox.setSpacing(5) 722 layout.addLayout(bbarhbox) 723 self._guardSelBtn = QPushButton() 724 menu = QMenu(self) 725 menu.triggered.connect(self._onGuardSelectionChange) 726 self._guardSelBtn.setMenu(menu) 727 bbarhbox.addWidget(self._guardSelBtn) 728 729 self._qqueueComboWidget.activated[str].connect(self._onQQueueActivated) 730 731 self._queueListWidget.activated.connect(self._onGotoPatch) 732 733 self._qpushAct.triggered.connect(self._patchActions.pushPatch) 734 self._qpushAllAct.triggered.connect(self._patchActions.pushAllPatches) 735 self._qpopAct.triggered.connect(self._patchActions.popPatch) 736 self._qpopAllAct.triggered.connect(self._patchActions.popAllPatches) 737 self._qgotoAct.triggered.connect(self._onGotoPatch) 738 self._qpushMoveAct.triggered.connect(self._onPushMovePatch) 739 self._qfinishAct.triggered.connect(self._onFinishRevision) 740 self._qdeleteAct.triggered.connect(self._onDelete) 741 self._qrenameAct.triggered.connect(self._onRenamePatch) 742 self._setGuardsAct.triggered.connect(self._onGuardConfigure) 743 744 self.setAcceptDrops(True) 745 746 self.layout().setContentsMargins(2, 2, 2, 2) 747 748 QTimer.singleShot(0, self.reload) 749 750 @property 751 def _repo(self): 752 if self._repoagent: 753 return self._repoagent.rawRepo() 754 755 def setRepoAgent(self, repoagent): 756 if self._repoagent: 757 self._repoagent.repositoryChanged.disconnect(self.reload) 758 self._repoagent = None 759 if repoagent and b'mq' in repoagent.rawRepo().extensions(): 760 self._repoagent = repoagent 761 self._repoagent.repositoryChanged.connect(self.reload) 762 self._changePatchQueueModel() 763 self._patchActions.setRepoAgent(repoagent) 764 self._qqueueActions.setRepoAgent(repoagent) 765 QTimer.singleShot(0, self.reload) 766 767 def _changePatchQueueModel(self): 768 oldmodel = self._queueListWidget.model() 769 if self._repoagent: 770 newmodel = PatchQueueModel(self._repoagent, self) 771 self._queueListWidget.setModel(newmodel) 772 newmodel.dataChanged.connect(self._updatePatchActions) 773 selmodel = self._queueListWidget.selectionModel() 774 selmodel.currentRowChanged.connect(self._onPatchSelected) 775 selmodel.selectionChanged.connect(self._updatePatchActions) 776 self._updatePatchActions() 777 else: 778 self._queueListWidget.setModel(None) 779 if oldmodel: 780 oldmodel.setParent(None) 781 782 @pyqtSlot() 783 def _showActiveQueue(self): 784 combo = self._qqueueComboWidget 785 q = hglib.tounicode(self._repo.thgactivemqname) 786 index = combo.findText(q) 787 combo.setCurrentIndex(index) 788 789 @pyqtSlot(QPoint) 790 def _onMenuRequested(self, pos): 791 menu = QMenu(self) 792 menu.addAction(self._qgotoAct) 793 menu.addAction(self._qpushMoveAct) 794 menu.addAction(self._qfinishAct) 795 menu.addAction(self._qdeleteAct) 796 menu.addAction(self._qrenameAct) 797 menu.addAction(self._setGuardsAct) 798 menu.setAttribute(Qt.WA_DeleteOnClose) 799 menu.popup(self._queueListWidget.viewport().mapToGlobal(pos)) 800 801 def _currentPatchName(self): 802 model = self._queueListWidget.model() 803 assert model is not None 804 index = self._queueListWidget.currentIndex() 805 return model.patchName(index) 806 807 @pyqtSlot() 808 def _onGuardConfigure(self): 809 model = self._queueListWidget.model() 810 assert model is not None 811 index = self._queueListWidget.currentIndex() 812 patch = model.patchName(index) 813 uguards = ' '.join(model.patchGuards(index)) 814 new, ok = qtlib.getTextInput(self, 815 _('Configure guards'), 816 _('Input new guards for %s:') % patch, 817 text=uguards) 818 if not ok or new == uguards: 819 return 820 self._patchActions.guardPatch(patch, pycompat.unicode(new).split()) 821 822 @pyqtSlot() 823 def _onDelete(self): 824 model = self._queueListWidget.model() 825 selmodel = self._queueListWidget.selectionModel() 826 assert model is not None 827 assert selmodel is not None 828 patches = pycompat.maplist(model.patchName, selmodel.selectedRows()) 829 self._patchActions.deletePatches(patches) 830 831 @pyqtSlot() 832 def _onGotoPatch(self): 833 patch = self._currentPatchName() 834 self._patchActions.gotoPatch(patch) 835 836 @pyqtSlot() 837 def _onPushMovePatch(self): 838 patch = self._currentPatchName() 839 self._patchActions.pushPatch(patch, move=True) 840 841 @pyqtSlot() 842 def _onFinishRevision(self): 843 patch = self._currentPatchName() 844 self._patchActions.finishRevision(patch) 845 846 @pyqtSlot() 847 def _onRenamePatch(self): 848 patch = self._currentPatchName() 849 self._patchActions.renamePatch(patch) 850 851 @pyqtSlot() 852 def _onPatchSelected(self): 853 patch = self._currentPatchName() 854 if patch: 855 self.patchSelected.emit(patch) 856 857 @pyqtSlot() 858 def _updatePatchActions(self): 859 model = self._queueListWidget.model() 860 selmodel = self._queueListWidget.selectionModel() 861 assert model is not None 862 assert selmodel is not None 863 864 appliedcnt = model.appliedCount() 865 seriescnt = model.rowCount() 866 self._qpushAllAct.setEnabled(seriescnt > appliedcnt) 867 self._qpushAct.setEnabled(seriescnt > appliedcnt) 868 self._qpopAct.setEnabled(appliedcnt > 0) 869 self._qpopAllAct.setEnabled(appliedcnt > 0) 870 871 indexes = selmodel.selectedRows() 872 anyapplied = any(model.isApplied(i) for i in indexes) 873 self._qgotoAct.setEnabled(len(indexes) == 1 874 and indexes[0] != model.topAppliedIndex()) 875 self._qpushMoveAct.setEnabled(len(indexes) == 1 and not anyapplied) 876 self._qfinishAct.setEnabled(len(indexes) == 1 and anyapplied) 877 self._qdeleteAct.setEnabled(len(indexes) > 0 and not anyapplied) 878 self._setGuardsAct.setEnabled(len(indexes) == 1) 879 self._qrenameAct.setEnabled(len(indexes) == 1) 880 881 @pyqtSlot(str) 882 def _onQQueueActivated(self, text): 883 if text == hglib.tounicode(self._repo.thgactivemqname): 884 return 885 886 if qtlib.QuestionMsgBox(_('Confirm patch queue switch'), 887 _("Do you really want to activate patch queue '%s' ?") % text, 888 parent=self, defaultbutton=QMessageBox.No): 889 sess = self._qqueueActions.switchQueue(text) 890 sess.commandFinished.connect(self._showActiveQueue) 891 else: 892 self._showActiveQueue() 893 894 @pyqtSlot() 895 def reload(self): 896 # Repository mutation should be disabled while incoming bundle is 897 # applied. Some MQ operation could be allowed, but let's simply 898 # disable the widget at all. 899 self.widget().setEnabled(bool(self._repoagent) 900 and not self._repoagent.overlayUrl()) 901 if not self._repoagent: 902 return 903 904 self._loadQQueues() 905 self._showActiveQueue() 906 907 repo = self._repo 908 909 self._allguards = set() 910 for idx, patch in enumerate(repo.mq.series): 911 patchguards = repo.mq.seriesguards[idx] 912 if patchguards: 913 for guard in patchguards: 914 self._allguards.add(guard[1:]) 915 916 for guard in repo.mq.active(): 917 self._allguards.add(guard) 918 self._refreshSelectedGuards() 919 920 self._qqueueComboWidget.setEnabled(self._qqueueComboWidget.count() > 1) 921 922 def _loadQQueues(self): 923 repo = self._repo 924 combo = self._qqueueComboWidget 925 combo.clear() 926 combo.addItems(hglib.getqqueues(repo)) 927 928 def _refreshSelectedGuards(self): 929 total = len(self._allguards) 930 count = len(self._repo.mq.active()) 931 menu = self._guardSelBtn.menu() 932 menu.clear() 933 for guard in self._allguards: 934 a = menu.addAction(hglib.tounicode(guard)) 935 a.setCheckable(True) 936 a.setChecked(guard in self._repo.mq.active()) 937 self._guardSelBtn.setText(_('Guards: %d/%d') % (count, total)) 938 self._guardSelBtn.setEnabled(bool(total)) 939 940 @pyqtSlot(QAction) 941 def _onGuardSelectionChange(self, action): 942 guard = hglib.fromunicode(action.text()) 943 newguards = self._repo.mq.active()[:] 944 if action.isChecked(): 945 newguards.append(guard) 946 elif guard in newguards: 947 newguards.remove(guard) 948 self._patchActions.selectGuards(pycompat.maplist(hglib.tounicode, 949 newguards)) 950 951 def keyPressEvent(self, event): 952 if event.key() == Qt.Key_Escape: 953 self._patchActions.abort() 954 self._qqueueActions.abort() 955 else: 956 return super(MQPatchesWidget, self).keyPressEvent(event) 957 958 959class OptionsDialog(QDialog): 960 'Utility dialog for configuring uncommon options' 961 def __init__(self, opts, parent=None): 962 QDialog.__init__(self, parent) 963 self.setWindowTitle(_('MQ options')) 964 965 layout = QVBoxLayout() 966 self.setLayout(layout) 967 968 self.forcecb = QCheckBox( 969 _('Force push or pop (--force)')) 970 layout.addWidget(self.forcecb) 971 972 self.keepcb = QCheckBox( 973 _('Tolerate non-conflicting local changes (--keep-changes)')) 974 layout.addWidget(self.keepcb) 975 976 self.forcecb.setChecked(opts.get('force', False)) 977 self.keepcb.setChecked(opts.get('keep_changes', False)) 978 979 for cb in [self.forcecb, self.keepcb]: 980 cb.clicked.connect(self._resolveopts) 981 982 BB = QDialogButtonBox 983 bb = QDialogButtonBox(BB.Ok|BB.Cancel) 984 bb.accepted.connect(self.accept) 985 bb.rejected.connect(self.reject) 986 self.bb = bb 987 layout.addWidget(bb) 988 989 @pyqtSlot() 990 def _resolveopts(self): 991 # cannot use both --force and --keep-changes 992 exclmap = {self.forcecb: [self.keepcb], 993 self.keepcb: [self.forcecb], 994 } 995 sendercb = self.sender() 996 if sendercb.isChecked(): 997 for cb in exclmap[sendercb]: 998 cb.setChecked(False) 999 1000 def accept(self): 1001 outopts = {'force': self.forcecb.isChecked(), 1002 'keep_changes': self.keepcb.isChecked()} 1003 self.outopts = outopts 1004 QDialog.accept(self) 1005