|
# postreview.py - post review dialog for TortoiseHg
#
# Copyright 2011 Michael De Wildt <michael.dewildt@gmail.com>
#
# A dialog to allow users to post a review to reviewboard
# http:///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
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from mercurial import error, extensions, cmdutil
from tortoisehg.util import hglib, paths
from tortoisehg.hgqt.i18n import _
from tortoisehg.hgqt import cmdui, qtlib, thgrepo
from tortoisehg.hgqt.postreview_ui import Ui_PostReviewDialog
from tortoisehg.hgqt.hgemail import _ChangesetsModel
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'http://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, repo, revs, parent=None):
super(PostReviewDialog, self).__init__(parent)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.ui = ui
self.repo = repo
self.error_message = None
self.cmd = 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)
@pyqtSlot()
def passwordPrompt(self):
pwd, ok = QInputDialog.getText(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 self.cmd and self.cmd.core.running():
self.cmd.commandFinished.disconnect(self.onCompletion)
self.cmd.cancel()
# 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(s.value('reviewboard/geom').toByteArray())
self.qui.publish_immediately_check.setChecked(
s.value('reviewboard/publish_immediately_check').toBool())
self.qui.outgoing_changes_check.setChecked(
s.value('reviewboard/outgoing_changes_check').toBool())
self.qui.branch_check.setChecked(
s.value('reviewboard/branch_check').toBool())
self.qui.update_fields.setChecked(
s.value('reviewboard/update_fields').toBool())
self.qui.summary_edit.addItems(
s.value('reviewboard/summary_edit_history').toStringList())
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 = s.value('reviewboard/repo_id').toInt()[0]
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 cmdutil.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 str(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
opts['parent'] = str(self.selectedRevs[0] - 1)
# 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)
#Finally we want to pass the repo path to the hg extension
opts['repository'] = self.repo.root
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
hglib.loadextension(self.ui, 'reviewboard')
opts = self.postReviewOpts()
revstr = str(self.selectedRevs.pop())
self.qui.post_review_button.setEnabled(False)
self.qui.close_button.setEnabled(False)
self.cmd = cmdui.Runner(False, self)
self.cmd.setTitle(_('Review Board'))
self.cmd.commandFinished.connect(self.onCompletion)
self.cmd.run(['postreview'] + cmdargs(opts) + [revstr])
@pyqtSlot()
def onCompletion(self):
self.qui.progress_bar.hide()
self.qui.progress_label.hide()
output = self.cmd.core.rawoutput()
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()
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()
def run(ui, *pats, **opts):
revs = opts.get('rev') or None
if not revs and len(pats):
revs = pats[0]
repo = opts.get('repo') or thgrepo.repository(ui, path=paths.find_root())
try:
return PostReviewDialog(repo.ui, repo, revs)
except error.RepoLookupError, e:
qtlib.ErrorMsgBox(_('Failed to open Review Board dialog'),
hglib.tounicode(e.message))
|
Loading...