/tortoisehg/hgqt/repowidget.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 1130 lines · 1080 code · 40 blank · 10 comment · 12 complexity · 790e5ff1b2091dbdd18ffc212005002e MD5 · raw file

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