PageRenderTime 26ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/tortoisehg-2.4.2/tortoisehg/hgqt/postreview.py

#
Python | 402 lines | 294 code | 73 blank | 35 comment | 67 complexity | 36ae584eb30286bbfe3b7b8d6799fd82 MD5 | raw file
Possible License(s): GPL-2.0
  1. # postreview.py - post review dialog for TortoiseHg
  2. #
  3. # Copyright 2011 Michael De Wildt <michael.dewildt@gmail.com>
  4. #
  5. # A dialog to allow users to post a review to reviewboard
  6. # http:///www.reviewboard.org
  7. #
  8. # This dialog requires a fork of the review board mercurial plugin, maintained
  9. # by mdelagra, that can be downloaded from:
  10. #
  11. # https://bitbucket.org/mdelagra/mercurial-reviewboard/
  12. #
  13. # More information can be found at http://www.mikeyd.com.au/tortoisehg-reviewboard
  14. #
  15. # This software may be used and distributed according to the terms of the
  16. # GNU General Public License version 2, incorporated herein by reference.
  17. from PyQt4.QtCore import *
  18. from PyQt4.QtGui import *
  19. from mercurial import error, extensions, scmutil
  20. from tortoisehg.util import hglib, paths
  21. from tortoisehg.hgqt.i18n import _
  22. from tortoisehg.hgqt import cmdui, qtlib, thgrepo
  23. from tortoisehg.hgqt.postreview_ui import Ui_PostReviewDialog
  24. from tortoisehg.hgqt.hgemail import _ChangesetsModel
  25. class LoadReviewDataThread(QThread):
  26. def __init__ (self, dialog):
  27. super(LoadReviewDataThread, self).__init__(dialog)
  28. self.dialog = dialog
  29. def run(self):
  30. msg = None
  31. if not self.dialog.server:
  32. msg = _("Invalid Settings - The ReviewBoard server is not setup")
  33. elif not self.dialog.user:
  34. msg = _("Invalid Settings - Please provide your ReviewBoard username")
  35. else:
  36. rb = extensions.find("reviewboard")
  37. try:
  38. pwd = self.dialog.password
  39. #if we don't have a password send something here to skip
  40. #the cli getpass in the extension. We will set the password
  41. #later
  42. if not pwd:
  43. pwd = "None"
  44. self.reviewboard = rb.make_rbclient(self.dialog.server,
  45. self.dialog.user,
  46. pwd)
  47. self.loadCombos()
  48. except rb.ReviewBoardError, e:
  49. msg = e.msg
  50. except TypeError:
  51. msg = _("Invalid reviewboard plugin. Please download the "
  52. "mercurial reviewboard plugin version 3.5 or higher "
  53. "from the website below.\n\n %s") % \
  54. u'http://bitbucket.org/mdelagra/mercurial-reviewboard/'
  55. self.dialog.error_message = msg
  56. def loadCombos(self):
  57. #Get the index of a users previously selected repo id
  58. index = 0
  59. count = 0
  60. self.dialog.qui.progress_label.setText("Loading repositories...")
  61. for r in self.reviewboard.repositories():
  62. if r.id == self.dialog.repo_id:
  63. index = count
  64. self.dialog.qui.repo_id_combo.addItem(str(r.id) + ": " + r.name)
  65. count += 1
  66. if self.dialog.qui.repo_id_combo.count():
  67. self.dialog.qui.repo_id_combo.setCurrentIndex(index)
  68. self.dialog.qui.progress_label.setText("Loading existing reviews...")
  69. for r in self.reviewboard.pending_user_requests():
  70. summary = str(r.id) + ": " + r.summary[0:100]
  71. self.dialog.qui.review_id_combo.addItem(summary)
  72. if self.dialog.qui.review_id_combo.count():
  73. self.dialog.qui.review_id_combo.setCurrentIndex(0)
  74. class PostReviewDialog(QDialog):
  75. """Dialog for sending patches to reviewboard"""
  76. def __init__(self, ui, repo, revs, parent=None):
  77. super(PostReviewDialog, self).__init__(parent)
  78. self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
  79. self.ui = ui
  80. self.repo = repo
  81. self.error_message = None
  82. self.cmd = None
  83. self.qui = Ui_PostReviewDialog()
  84. self.qui.setupUi(self)
  85. self.initChangesets(revs)
  86. self.readSettings()
  87. self.review_thread = LoadReviewDataThread(self)
  88. self.review_thread.finished.connect(self.errorPrompt)
  89. self.review_thread.start()
  90. QShortcut(QKeySequence('Ctrl+Return'), self, self.accept)
  91. QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept)
  92. @pyqtSlot()
  93. def passwordPrompt(self):
  94. pwd, ok = qtlib.getTextInput(self,
  95. _('Review Board'),
  96. _('Password:'),
  97. mode=QLineEdit.Password)
  98. if ok and pwd:
  99. self.password = pwd
  100. return True
  101. else:
  102. self.password = None
  103. return False
  104. @pyqtSlot()
  105. def errorPrompt(self):
  106. self.qui.progress_bar.hide()
  107. self.qui.progress_label.hide()
  108. if self.error_message:
  109. qtlib.ErrorMsgBox(_('Review Board'),
  110. _('Error'), self.error_message)
  111. self.close()
  112. elif self.isValid():
  113. self.qui.post_review_button.setEnabled(True)
  114. def closeEvent(self, event):
  115. if self.cmd and self.cmd.core.running():
  116. self.cmd.commandFinished.disconnect(self.onCompletion)
  117. self.cmd.cancel()
  118. # Dispose of the review data thread
  119. self.review_thread.terminate()
  120. self.review_thread.wait()
  121. self.writeSettings()
  122. super(PostReviewDialog, self).closeEvent(event)
  123. def readSettings(self):
  124. s = QSettings()
  125. self.restoreGeometry(s.value('reviewboard/geom').toByteArray())
  126. self.qui.publish_immediately_check.setChecked(
  127. s.value('reviewboard/publish_immediately_check').toBool())
  128. self.qui.outgoing_changes_check.setChecked(
  129. s.value('reviewboard/outgoing_changes_check').toBool())
  130. self.qui.branch_check.setChecked(
  131. s.value('reviewboard/branch_check').toBool())
  132. self.qui.update_fields.setChecked(
  133. s.value('reviewboard/update_fields').toBool())
  134. self.qui.summary_edit.addItems(
  135. s.value('reviewboard/summary_edit_history').toStringList())
  136. try:
  137. self.repo_id = int(self.repo.ui.config('reviewboard', 'repoid'))
  138. except Exception:
  139. self.repo_id = None
  140. if not self.repo_id:
  141. self.repo_id = s.value('reviewboard/repo_id').toInt()[0]
  142. self.server = self.repo.ui.config('reviewboard', 'server')
  143. self.user = self.repo.ui.config('reviewboard', 'user')
  144. self.password = self.repo.ui.config('reviewboard', 'password')
  145. self.browser = self.repo.ui.config('reviewboard', 'browser')
  146. def writeSettings(self):
  147. s = QSettings()
  148. s.setValue('reviewboard/geom', self.saveGeometry())
  149. s.setValue('reviewboard/publish_immediately_check',
  150. self.qui.publish_immediately_check.isChecked())
  151. s.setValue('reviewboard/branch_check',
  152. self.qui.branch_check.isChecked())
  153. s.setValue('reviewboard/outgoing_changes_check',
  154. self.qui.outgoing_changes_check.isChecked())
  155. s.setValue('reviewboard/update_fields',
  156. self.qui.update_fields.isChecked())
  157. s.setValue('reviewboard/repo_id', self.getRepoId())
  158. def itercombo(w):
  159. if w.currentText():
  160. yield w.currentText()
  161. for i in xrange(w.count()):
  162. if w.itemText(i) != w.currentText():
  163. yield w.itemText(i)
  164. s.setValue('reviewboard/summary_edit_history',
  165. list(itercombo(self.qui.summary_edit))[:10])
  166. def initChangesets(self, revs, selected_revs=None):
  167. def purerevs(revs):
  168. return scmutil.revrange(self.repo, iter(str(e) for e in revs))
  169. if selected_revs:
  170. selectedrevs = purerevs(selected_revs)
  171. else:
  172. selectedrevs = purerevs(revs)
  173. self._changesets = _ChangesetsModel(self.repo,
  174. # TODO: [':'] is inefficient
  175. revs=purerevs(revs or [':']),
  176. selectedrevs=selectedrevs,
  177. parent=self)
  178. self.qui.changesets_view.setModel(self._changesets)
  179. @property
  180. def selectedRevs(self):
  181. """Returns list of revisions to be sent"""
  182. return self._changesets.selectedrevs
  183. @property
  184. def allRevs(self):
  185. """Returns list of revisions to be sent"""
  186. return self._changesets.revs
  187. def getRepoId(self):
  188. comboText = self.qui.repo_id_combo.currentText().split(":")
  189. return str(comboText[0])
  190. def getReviewId(self):
  191. comboText = self.qui.review_id_combo.currentText().split(":")
  192. return str(comboText[0])
  193. def getSummary(self):
  194. comboText = self.qui.review_id_combo.currentText().split(":")
  195. return str(comboText[1])
  196. def postReviewOpts(self, **opts):
  197. """Generate opts for reviewboard by form values"""
  198. opts['outgoingchanges'] = self.qui.outgoing_changes_check.isChecked()
  199. opts['branch'] = self.qui.branch_check.isChecked()
  200. opts['publish'] = self.qui.publish_immediately_check.isChecked()
  201. if self.qui.tab_widget.currentIndex() == 1:
  202. opts["existing"] = self.getReviewId()
  203. opts['update'] = self.qui.update_fields.isChecked()
  204. opts['summary'] = self.getSummary()
  205. else:
  206. opts['repoid'] = self.getRepoId()
  207. opts['summary'] = hglib.fromunicode(self.qui.summary_edit.currentText())
  208. if (len(self.selectedRevs) > 1):
  209. #Set the parent to the revision below the last one on the list
  210. #so all checked revisions are included in the request
  211. opts['parent'] = str(self.selectedRevs[0] - 1)
  212. # Always use the upstream repo to determine the parent diff base
  213. # without the diff uploaded to review board dies
  214. opts['outgoing'] = True
  215. #Set the password just in case the user has opted to not save it
  216. opts['password'] = str(self.password)
  217. #Finally we want to pass the repo path to the hg extension
  218. opts['repository'] = self.repo.root
  219. return opts
  220. def isValid(self):
  221. """Filled all required values?"""
  222. if not self.qui.repo_id_combo.currentText():
  223. return False
  224. if self.qui.tab_widget.currentIndex() == 1:
  225. if not self.qui.review_id_combo.currentText():
  226. return False
  227. if not self.allRevs:
  228. return False
  229. return True
  230. @pyqtSlot()
  231. def tabChanged(self):
  232. self.qui.post_review_button.setEnabled(self.isValid())
  233. @pyqtSlot()
  234. def branchCheckToggle(self):
  235. if self.qui.branch_check.isChecked():
  236. self.qui.outgoing_changes_check.setChecked(False)
  237. self.toggleOutgoingChangesets()
  238. @pyqtSlot()
  239. def outgoingChangesCheckToggle(self):
  240. if self.qui.outgoing_changes_check.isChecked():
  241. self.qui.branch_check.setChecked(False)
  242. self.toggleOutgoingChangesets()
  243. def toggleOutgoingChangesets(self):
  244. branch = self.qui.branch_check.isChecked()
  245. outgoing = self.qui.outgoing_changes_check.isChecked()
  246. if branch or outgoing:
  247. self.initChangesets(self.allRevs, [self.selectedRevs.pop()])
  248. self.qui.changesets_view.setEnabled(False)
  249. else:
  250. self.initChangesets(self.allRevs, self.allRevs)
  251. self.qui.changesets_view.setEnabled(True)
  252. def close(self):
  253. super(PostReviewDialog, self).close()
  254. def accept(self):
  255. if not self.isValid():
  256. return
  257. if not self.password and not self.passwordPrompt():
  258. return
  259. self.qui.progress_bar.show()
  260. self.qui.progress_label.setText("Posting Review...")
  261. self.qui.progress_label.show()
  262. def cmdargs(opts):
  263. args = []
  264. for k, v in opts.iteritems():
  265. if isinstance(v, bool):
  266. if v:
  267. args.append('--%s' % k.replace('_', '-'))
  268. else:
  269. for e in isinstance(v, basestring) and [v] or v:
  270. args += ['--%s' % k.replace('_', '-'), e]
  271. return args
  272. hglib.loadextension(self.ui, 'reviewboard')
  273. opts = self.postReviewOpts()
  274. revstr = str(self.selectedRevs.pop())
  275. self.qui.post_review_button.setEnabled(False)
  276. self.qui.close_button.setEnabled(False)
  277. self.cmd = cmdui.Runner(False, self)
  278. self.cmd.setTitle(_('Review Board'))
  279. self.cmd.commandFinished.connect(self.onCompletion)
  280. self.cmd.run(['postreview'] + cmdargs(opts) + [revstr])
  281. @pyqtSlot()
  282. def onCompletion(self):
  283. self.qui.progress_bar.hide()
  284. self.qui.progress_label.hide()
  285. output = self.cmd.core.rawoutput()
  286. saved = 'saved:' in output
  287. published = 'published:' in output
  288. if (saved or published):
  289. if saved:
  290. url = output.split('saved: ').pop().strip()
  291. msg = _('Review draft posted to %s\n') % url
  292. else:
  293. url = output.split('published: ').pop().strip()
  294. msg = _('Review published to %s\n') % url
  295. QDesktopServices.openUrl(QUrl(url))
  296. qtlib.InfoMsgBox(_('Review Board'), _('Success'),
  297. msg, parent=self)
  298. else:
  299. error = output.split('abort: ').pop().strip()
  300. if error[:29] == "HTTP Error: basic auth failed":
  301. if self.passwordPrompt():
  302. self.accept()
  303. else:
  304. self.qui.post_review_button.setEnabled(True)
  305. self.qui.close_button.setEnabled(True)
  306. return
  307. else:
  308. qtlib.ErrorMsgBox(_('Review Board'),
  309. _('Error'), error)
  310. self.writeSettings()
  311. super(PostReviewDialog, self).accept()
  312. @pyqtSlot()
  313. def onSettingsButtonClicked(self):
  314. from tortoisehg.hgqt import settings
  315. if settings.SettingsDialog(parent=self, focus='reviewboard.server').exec_():
  316. # not use repo.configChanged because it can clobber user input
  317. # accidentally.
  318. self.repo.invalidateui() # force reloading config immediately
  319. self.readSettings()
  320. def run(ui, *pats, **opts):
  321. revs = opts.get('rev') or None
  322. if not revs and len(pats):
  323. revs = pats[0]
  324. repo = opts.get('repo') or thgrepo.repository(ui, path=paths.find_root())
  325. try:
  326. return PostReviewDialog(repo.ui, repo, revs)
  327. except error.RepoLookupError, e:
  328. qtlib.ErrorMsgBox(_('Failed to open Review Board dialog'),
  329. hglib.tounicode(e.message))