# postreview.py - post review dialog for TortoiseHg # # Copyright 2011 Michael De Wildt # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. """A dialog to allow users to post a review to reviewboard https://www.reviewboard.org This dialog requires a fork of the review board mercurial plugin, maintained by mdelagra, that can be downloaded from: https://bitbucket.org/mdelagra/mercurial-reviewboard/ More information can be found at http://www.mikeyd.com.au/tortoisehg-reviewboard """ from __future__ import absolute_import from .qtcore import ( QSettings, QThread, QUrl, Qt, pyqtSlot, ) from .qtgui import ( QDesktopServices, QDialog, QKeySequence, QLineEdit, QShortcut, ) from mercurial import ( extensions, scmutil, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, qtlib, ) from .hgemail import _ChangesetsModel from .postreview_ui import Ui_PostReviewDialog class LoadReviewDataThread(QThread): def __init__ (self, dialog): super(LoadReviewDataThread, self).__init__(dialog) self.dialog = dialog def run(self): msg = None if not self.dialog.server: msg = _("Invalid Settings - The ReviewBoard server is not setup") elif not self.dialog.user: msg = _("Invalid Settings - Please provide your ReviewBoard username") else: rb = extensions.find("reviewboard") try: pwd = self.dialog.password #if we don't have a password send something here to skip #the cli getpass in the extension. We will set the password #later if not pwd: pwd = "None" self.reviewboard = rb.make_rbclient(self.dialog.server, self.dialog.user, pwd) self.loadCombos() except rb.ReviewBoardError, e: msg = e.msg except TypeError: msg = _("Invalid reviewboard plugin. Please download the " "Mercurial reviewboard plugin version 3.5 or higher " "from the website below.\n\n %s") % \ u'https://bitbucket.org/mdelagra/mercurial-reviewboard/' self.dialog.error_message = msg def loadCombos(self): #Get the index of a users previously selected repo id index = 0 count = 0 self.dialog.qui.progress_label.setText("Loading repositories...") for r in self.reviewboard.repositories(): if r.id == self.dialog.repo_id: index = count self.dialog.qui.repo_id_combo.addItem(str(r.id) + ": " + r.name) count += 1 if self.dialog.qui.repo_id_combo.count(): self.dialog.qui.repo_id_combo.setCurrentIndex(index) self.dialog.qui.progress_label.setText("Loading existing reviews...") for r in self.reviewboard.pending_user_requests(): summary = str(r.id) + ": " + r.summary[0:100] self.dialog.qui.review_id_combo.addItem(summary) if self.dialog.qui.review_id_combo.count(): self.dialog.qui.review_id_combo.setCurrentIndex(0) class PostReviewDialog(QDialog): """Dialog for sending patches to reviewboard""" def __init__(self, ui, repoagent, revs, parent=None): super(PostReviewDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.ui = ui self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._cmdoutputs = [] self.error_message = None self.qui = Ui_PostReviewDialog() self.qui.setupUi(self) self.initChangesets(revs) self.readSettings() self.review_thread = LoadReviewDataThread(self) self.review_thread.finished.connect(self.errorPrompt) self.review_thread.start() QShortcut(QKeySequence('Ctrl+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept) @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot() def passwordPrompt(self): pwd, ok = qtlib.getTextInput(self, _('Review Board'), _('Password:'), mode=QLineEdit.Password) if ok and pwd: self.password = pwd return True else: self.password = None return False @pyqtSlot() def errorPrompt(self): self.qui.progress_bar.hide() self.qui.progress_label.hide() if self.error_message: qtlib.ErrorMsgBox(_('Review Board'), _('Error'), self.error_message) self.close() elif self.isValid(): self.qui.post_review_button.setEnabled(True) def closeEvent(self, event): if not self._cmdsession.isFinished(): self._cmdsession.abort() event.ignore() return # Dispose of the review data thread self.review_thread.terminate() self.review_thread.wait() self.writeSettings() super(PostReviewDialog, self).closeEvent(event) def readSettings(self): s = QSettings() self.restoreGeometry(qtlib.readByteArray(s, 'reviewboard/geom')) self.qui.publish_immediately_check.setChecked( qtlib.readBool(s, 'reviewboard/publish_immediately_check')) self.qui.outgoing_changes_check.setChecked( qtlib.readBool(s, 'reviewboard/outgoing_changes_check')) self.qui.branch_check.setChecked( qtlib.readBool(s, 'reviewboard/branch_check')) self.qui.update_fields.setChecked( qtlib.readBool(s, 'reviewboard/update_fields')) self.qui.summary_edit.addItems( qtlib.readStringList(s, 'reviewboard/summary_edit_history')) try: self.repo_id = int(self.repo.ui.config('reviewboard', 'repoid')) except Exception: self.repo_id = None if not self.repo_id: self.repo_id = qtlib.readInt(s, 'reviewboard/repo_id') self.server = self.repo.ui.config('reviewboard', 'server') self.user = self.repo.ui.config('reviewboard', 'user') self.password = self.repo.ui.config('reviewboard', 'password') self.browser = self.repo.ui.config('reviewboard', 'browser') def writeSettings(self): s = QSettings() s.setValue('reviewboard/geom', self.saveGeometry()) s.setValue('reviewboard/publish_immediately_check', self.qui.publish_immediately_check.isChecked()) s.setValue('reviewboard/branch_check', self.qui.branch_check.isChecked()) s.setValue('reviewboard/outgoing_changes_check', self.qui.outgoing_changes_check.isChecked()) s.setValue('reviewboard/update_fields', self.qui.update_fields.isChecked()) s.setValue('reviewboard/repo_id', self.getRepoId()) def itercombo(w): if w.currentText(): yield w.currentText() for i in xrange(w.count()): if w.itemText(i) != w.currentText(): yield w.itemText(i) s.setValue('reviewboard/summary_edit_history', list(itercombo(self.qui.summary_edit))[:10]) def initChangesets(self, revs, selected_revs=None): def purerevs(revs): return scmutil.revrange(self.repo, iter(str(e) for e in revs)) if selected_revs: selectedrevs = purerevs(selected_revs) else: selectedrevs = purerevs(revs) self._changesets = _ChangesetsModel(self.repo, # TODO: [':'] is inefficient revs=purerevs(revs or [':']), selectedrevs=selectedrevs, parent=self) self.qui.changesets_view.setModel(self._changesets) @property def selectedRevs(self): """Returns list of revisions to be sent""" return self._changesets.selectedrevs @property def allRevs(self): """Returns list of revisions to be sent""" return self._changesets.revs def getRepoId(self): comboText = self.qui.repo_id_combo.currentText().split(":") return str(comboText[0]) def getReviewId(self): comboText = self.qui.review_id_combo.currentText().split(":") return str(comboText[0]) def getSummary(self): comboText = self.qui.review_id_combo.currentText().split(":") return hglib.fromunicode(comboText[1]) def postReviewOpts(self, **opts): """Generate opts for reviewboard by form values""" opts['outgoingchanges'] = self.qui.outgoing_changes_check.isChecked() opts['branch'] = self.qui.branch_check.isChecked() opts['publish'] = self.qui.publish_immediately_check.isChecked() if self.qui.tab_widget.currentIndex() == 1: opts["existing"] = self.getReviewId() opts['update'] = self.qui.update_fields.isChecked() opts['summary'] = self.getSummary() else: opts['repoid'] = self.getRepoId() opts['summary'] = hglib.fromunicode(self.qui.summary_edit.currentText()) if (len(self.selectedRevs) > 1): #Set the parent to the revision below the last one on the list #so all checked revisions are included in the request ctx = self.repo[self.selectedRevs[0]] opts['parent'] = str(ctx.p1().rev()) # Always use the upstream repo to determine the parent diff base # without the diff uploaded to review board dies opts['outgoing'] = True #Set the password just in case the user has opted to not save it opts['password'] = str(self.password) return opts def isValid(self): """Filled all required values?""" if not self.qui.repo_id_combo.currentText(): return False if self.qui.tab_widget.currentIndex() == 1: if not self.qui.review_id_combo.currentText(): return False if not self.allRevs: return False return True @pyqtSlot() def tabChanged(self): self.qui.post_review_button.setEnabled(self.isValid()) @pyqtSlot() def branchCheckToggle(self): if self.qui.branch_check.isChecked(): self.qui.outgoing_changes_check.setChecked(False) self.toggleOutgoingChangesets() @pyqtSlot() def outgoingChangesCheckToggle(self): if self.qui.outgoing_changes_check.isChecked(): self.qui.branch_check.setChecked(False) self.toggleOutgoingChangesets() def toggleOutgoingChangesets(self): branch = self.qui.branch_check.isChecked() outgoing = self.qui.outgoing_changes_check.isChecked() if branch or outgoing: self.initChangesets(self.allRevs, [self.selectedRevs.pop()]) self.qui.changesets_view.setEnabled(False) else: self.initChangesets(self.allRevs, self.allRevs) self.qui.changesets_view.setEnabled(True) def close(self): super(PostReviewDialog, self).close() def accept(self): if not self.isValid(): return if not self.password and not self.passwordPrompt(): return self.qui.progress_bar.show() self.qui.progress_label.setText("Posting Review...") self.qui.progress_label.show() def cmdargs(opts): args = [] for k, v in opts.iteritems(): if isinstance(v, bool): if v: args.append('--%s' % k.replace('_', '-')) else: for e in isinstance(v, basestring) and [v] or v: args += ['--%s' % k.replace('_', '-'), e] return args opts = self.postReviewOpts() revstr = str(self.selectedRevs.pop()) self.qui.post_review_button.setEnabled(False) self.qui.close_button.setEnabled(False) cmdline = map(hglib.tounicode, ['postreview'] + cmdargs(opts) + [revstr]) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) del self._cmdoutputs[:] sess.commandFinished.connect(self.onCompletion) sess.outputReceived.connect(self._captureOutput) @pyqtSlot() def onCompletion(self): self.qui.progress_bar.hide() self.qui.progress_label.hide() output = hglib.fromunicode(''.join(self._cmdoutputs), 'replace') saved = 'saved:' in output published = 'published:' in output if (saved or published): if saved: url = output.split('saved: ').pop().strip() msg = _('Review draft posted to %s\n') % url else: url = output.split('published: ').pop().strip() msg = _('Review published to %s\n') % url QDesktopServices.openUrl(QUrl(url)) qtlib.InfoMsgBox(_('Review Board'), _('Success'), msg, parent=self) else: error = output.split('abort: ').pop().strip() if error[:29] == "HTTP Error: basic auth failed": if self.passwordPrompt(): self.accept() else: self.qui.post_review_button.setEnabled(True) self.qui.close_button.setEnabled(True) return else: qtlib.ErrorMsgBox(_('Review Board'), _('Error'), error) self.writeSettings() super(PostReviewDialog, self).accept() @pyqtSlot(str, str) def _captureOutput(self, msg, label): if label != 'control': self._cmdoutputs.append(unicode(msg)) @pyqtSlot() def onSettingsButtonClicked(self): from tortoisehg.hgqt import settings if settings.SettingsDialog(parent=self, focus='reviewboard.server').exec_(): # not use repo.configChanged because it can clobber user input # accidentally. self.repo.invalidateui() # force reloading config immediately self.readSettings()