/tortoisehg/hgqt/postreview.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 400 lines · 289 code · 77 blank · 34 comment · 67 complexity · e88d6b37d2d9442ea40c02ea7b70b327 MD5 · raw file

  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, cmdutil
  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. self.dialog._error_message = msg
  51. def loadCombos(self):
  52. #Get the index of a users previously selected repo id
  53. index = 0
  54. count = 0
  55. self.dialog.qui.progress_label.setText("Loading repositories...")
  56. for r in self.reviewboard.repositories():
  57. if r.id == self.dialog.repo_id:
  58. index = count
  59. self.dialog.qui.repo_id_combo.addItem(str(r.id) + ": " + r.name)
  60. count += 1
  61. if self.dialog.qui.repo_id_combo.count():
  62. self.dialog.qui.repo_id_combo.setCurrentIndex(index)
  63. self.dialog.qui.progress_label.setText("Loading existing reviews...")
  64. for r in self.reviewboard.pending_user_requests():
  65. summary = str(r.id) + ": " + r.summary[0:100]
  66. self.dialog.qui.review_id_combo.addItem(summary)
  67. if self.dialog.qui.review_id_combo.count():
  68. self.dialog.qui.review_id_combo.setCurrentIndex(0)
  69. class PostReviewDialog(QDialog):
  70. """Dialog for sending patches to reviewboard"""
  71. def __init__(self, ui, repo, revs, parent=None):
  72. super(PostReviewDialog, self).__init__(parent)
  73. self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
  74. self.ui = ui
  75. self.repo = repo
  76. self.error_message = None
  77. self.cmd = None
  78. self.qui = Ui_PostReviewDialog()
  79. self.qui.setupUi(self)
  80. self.initChangesets(revs)
  81. self.readSettings()
  82. self.review_thread = LoadReviewDataThread(self)
  83. self.review_thread.finished.connect(self.errorPrompt)
  84. self.review_thread.start()
  85. def keyPressEvent(self, event):
  86. # don't post review by just hitting enter
  87. if event.key() in (Qt.Key_Return, Qt.Key_Enter):
  88. if event.modifiers() == Qt.ControlModifier and self.isValid():
  89. self.accept() # Ctrl+Enter
  90. return
  91. super(PostReviewDialog, self).keyPressEvent(event)
  92. @pyqtSlot()
  93. def passwordPrompt(self):
  94. pwd, ok = QInputDialog.getText(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 cmdutil.revrange(self.repo,
  169. iter(str(e) for e in revs))
  170. if selected_revs:
  171. selectedrevs = purerevs(selected_revs)
  172. else:
  173. selectedrevs = purerevs(revs)
  174. self._changesets = _ChangesetsModel(self.repo,
  175. # TODO: [':'] is inefficient
  176. revs=purerevs(revs or [':']),
  177. selectedrevs=selectedrevs,
  178. parent=self)
  179. self.qui.changesets_view.setModel(self._changesets)
  180. @property
  181. def selectedRevs(self):
  182. """Returns list of revisions to be sent"""
  183. return self._changesets.selectedrevs
  184. @property
  185. def allRevs(self):
  186. """Returns list of revisions to be sent"""
  187. return self._changesets.revs
  188. def getRepoId(self):
  189. comboText = self.qui.repo_id_combo.currentText().split(":")
  190. return str(comboText[0])
  191. def getReviewId(self):
  192. comboText = self.qui.review_id_combo.currentText().split(":")
  193. return str(comboText[0])
  194. def getSummary(self):
  195. comboText = self.qui.review_id_combo.currentText().split(":")
  196. return str(comboText[1])
  197. def postReviewOpts(self, **opts):
  198. """Generate opts for reviewboard by form values"""
  199. opts['outgoingchanges'] = self.qui.outgoing_changes_check.isChecked()
  200. opts['branch'] = self.qui.branch_check.isChecked()
  201. opts['publish'] = self.qui.publish_immediately_check.isChecked()
  202. if self.qui.tab_widget.currentIndex() == 1:
  203. opts["existing"] = self.getReviewId()
  204. opts['update'] = self.qui.update_fields.isChecked()
  205. opts['summary'] = self.getSummary()
  206. else:
  207. opts['repoid'] = self.getRepoId()
  208. opts['summary'] = hglib.fromunicode(self.qui.summary_edit.currentText())
  209. if (len(self.selectedRevs) > 1):
  210. #Set the parent to the revision below the last one on the list
  211. #so all checked revisions are included in the request
  212. opts['parent'] = str(self.selectedRevs[0] - 1)
  213. # Always use the upstream repo to determine the parent diff base
  214. # without the diff uploaded to review board dies
  215. opts['outgoing'] = True
  216. #Set the password just in case the user has opted to not save it
  217. opts['password'] = str(self.password)
  218. #Finally we want to pass the repo path to the hg extension
  219. opts['repository'] = self.repo.root
  220. return opts
  221. def isValid(self):
  222. """Filled all required values?"""
  223. if not self.qui.repo_id_combo.currentText():
  224. return False
  225. if self.qui.tab_widget.currentIndex() == 1:
  226. if not self.qui.review_id_combo.currentText():
  227. return False
  228. if not self.allRevs:
  229. return False
  230. return True
  231. @pyqtSlot()
  232. def tabChanged(self):
  233. self.qui.post_review_button.setEnabled(self.isValid())
  234. @pyqtSlot()
  235. def branchCheckToggle(self):
  236. if self.qui.branch_check.isChecked():
  237. self.qui.outgoing_changes_check.setChecked(False)
  238. self.toggleOutgoingChangesets()
  239. @pyqtSlot()
  240. def outgoingChangesCheckToggle(self):
  241. if self.qui.outgoing_changes_check.isChecked():
  242. self.qui.branch_check.setChecked(False)
  243. self.toggleOutgoingChangesets()
  244. def toggleOutgoingChangesets(self):
  245. branch = self.qui.branch_check.isChecked()
  246. outgoing = self.qui.outgoing_changes_check.isChecked()
  247. if branch or outgoing:
  248. self.initChangesets(self.allRevs, [self.selectedRevs.pop()])
  249. self.qui.changesets_view.setEnabled(False)
  250. else:
  251. self.initChangesets(self.allRevs, self.allRevs)
  252. self.qui.changesets_view.setEnabled(True)
  253. def close(self):
  254. super(PostReviewDialog, self).close()
  255. def accept(self):
  256. if not self.password and not self.passwordPrompt():
  257. return
  258. self.qui.progress_bar.show()
  259. self.qui.progress_label.setText("Posting Review...")
  260. self.qui.progress_label.show()
  261. def cmdargs(opts):
  262. args = []
  263. for k, v in opts.iteritems():
  264. if isinstance(v, bool):
  265. if v:
  266. args.append('--%s' % k.replace('_', '-'))
  267. else:
  268. for e in isinstance(v, basestring) and [v] or v:
  269. args += ['--%s' % k.replace('_', '-'), e]
  270. return args
  271. hglib.loadextension(self.ui, 'reviewboard')
  272. opts = self.postReviewOpts()
  273. revstr = str(self.selectedRevs.pop())
  274. self.qui.post_review_button.setEnabled(False)
  275. self.qui.close_button.setEnabled(False)
  276. self.cmd = cmdui.Runner(_('Review Board'), False, self)
  277. self.cmd.commandFinished.connect(self.onCompletion)
  278. self.cmd.run(['postreview'] + cmdargs(opts) + [revstr])
  279. @pyqtSlot()
  280. def onCompletion(self):
  281. self.qui.progress_bar.hide()
  282. self.qui.progress_label.hide()
  283. output = self.cmd.core.rawoutput()
  284. saved = 'saved:' in output
  285. published = 'published:' in output
  286. if (saved or published):
  287. if saved:
  288. url = output.split('saved: ').pop().strip()
  289. msg = _('Review draft posted to %s\n' % url)
  290. else:
  291. url = output.split('published: ').pop().strip()
  292. msg = _('Review published to %s\n' % url)
  293. QDesktopServices.openUrl(QUrl(url))
  294. qtlib.InfoMsgBox(_('Review Board'), _('Success'),
  295. msg, parent=self)
  296. else:
  297. error = output.split('abort: ').pop().strip()
  298. if error[:29] == "HTTP Error: basic auth failed":
  299. if self.passwordPrompt():
  300. self.accept()
  301. else:
  302. self.qui.post_review_button.setEnabled(True)
  303. self.qui.close_button.setEnabled(True)
  304. return
  305. else:
  306. qtlib.ErrorMsgBox(_('Review Board'),
  307. _('Error'), error)
  308. self.writeSettings()
  309. super(PostReviewDialog, self).accept()
  310. @pyqtSlot()
  311. def onSettingsButtonClicked(self):
  312. from tortoisehg.hgqt import settings
  313. settings.SettingsDialog(parent=self, focus='reviewboard.server').exec_()
  314. def run(ui, *pats, **opts):
  315. revs = opts.get('rev') or None
  316. if not revs and len(pats):
  317. revs = pats[0]
  318. repo = opts.get('repo') or thgrepo.repository(ui, path=paths.find_root())
  319. try:
  320. return PostReviewDialog(repo.ui, repo, revs)
  321. except error.RepoLookupError, e:
  322. qtlib.ErrorMsgBox(_('Failed to open Review Board dialog'),
  323. hglib.tounicode(e.message))