Changeset 4d597971d9d0…
Parent b425ef45faca…
by
Changes to 7 files · Browse files at 4d597971d9d0 Showing diff from parent b425ef45faca Diff from another changeset...
|
|
|
@@ -0,0 +1,297 @@ + # postreview.py - post review dialog for TortoiseHg
+#
+# Copyright 2010 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 the reviewboard mercurial plugin which can be
+# downloaded from:
+#
+# http://bitbucket.org/michaeldewildt/mercurial-reviewboard
+#
+# It is a fork of mdelagra's plugin with some small changes to make it
+# play nicer with the thg ui. Mdelagra's reviewboard extension is a fork
+# of the original extension found on the Mercurial website.
+#
+# Original: http://mercurial.selenic.com/wiki/ReviewboardExtension
+# Mdelagra's Fork: http://bitbucket.org/mdelagra/mercurial-reviewboard/overview/
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2, incorporated herein by reference.
+import time, datetime
+
+from PyQt4.QtCore import *
+from PyQt4.QtGui import *
+from mercurial import error, util, 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
+from hgext import reviewboard
+
+class LoadReviewDataThread(QThread):
+ def __init__ (self, dialog):
+ super(LoadReviewDataThread, self).__init__(dialog)
+ self.dialog = dialog
+
+ def run(self):
+ msg = None
+ try:
+ self._reviewboard = reviewboard.ReviewBoard(self.dialog.server,
+ None, False)
+ self._reviewboard.login(self.dialog.user, self.dialog.password)
+ self.load_combos()
+
+ except reviewboard.ReviewBoardError, e:
+ if self.dialog.server:
+ msg = e.message
+ else:
+ msg = _("The review board server is not setup in settings")
+
+ self.dialog._error_message = msg
+
+ def load_combos(self):
+ for r in self._reviewboard.repositories():
+ self.dialog._qui.repo_id_combo.addItem(str(r['id']) + ": " + r['name'])
+
+ self.dialog._qui.repo_id_combo.setCurrentIndex(0)
+ for r in self._reviewboard.requests():
+ if self.is_valid_request(r):
+ summary = str(r['id']) + ": " + str(r['summary'])[0:100]
+ self.dialog._qui.review_id_combo.addItem(summary)
+
+ self.dialog._qui.review_id_combo.setCurrentIndex(0)
+
+ def is_valid_request(self, request):
+ #We only want to include pending requests
+ if request['status'] != 'pending':
+ return False
+ #And requests for the current user
+ if request['submitter']['username'] != self.dialog.user:
+ return False
+
+ #And only requests within the last week
+ delta = datetime.timedelta(days=7)
+ today = datetime.datetime.today()
+ sevenDaysAgo = today - delta
+ dateToCompare = datetime.datetime.strptime(request["last_updated"],
+ "%Y-%m-%d %H:%M:%S")
+ if (dateToCompare < sevenDaysAgo):
+ return False
+
+ return True
+
+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._qui = Ui_PostReviewDialog()
+ self._qui.setupUi(self)
+
+ self._initchangesets(revs)
+ self._readsettings()
+
+ self._review_thread = LoadReviewDataThread(self)
+ self._review_thread.start()
+ self._review_thread.finished.connect(self.error_prompt)
+
+ def keyPressEvent(self, event):
+ # don't post review by just hitting enter
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ if event.modifiers() == Qt.ControlModifier and self._isvalid():
+ self.accept() # Ctrl+Enter
+
+ return
+
+ super(PostReviewDialog, self).keyPressEvent(event)
+
+ @pyqtSlot()
+ def error_prompt(self):
+ if self._error_message:
+ qtlib.ErrorMsgBox(_('Error'), self._error_message)
+
+ # Dispose of the reviewdatathread
+ self._review_thread.terminate()
+ self._review_thread.wait()
+
+ self.close()
+ elif self._isvalid():
+ self._qui.post_review_button.setEnabled(True)
+
+ def closeEvent(self, event):
+ 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.update_fields.setChecked(
+ s.value('reviewboard/update_fields').toBool())
+ self._qui.summary_edit.addItems(
+ s.value('reviewboard/summary_edit_history').toStringList())
+
+ 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/outgoing_changes_check',
+ self._qui.outgoing_changes_check.isChecked())
+ s.setValue('reviewboard/update_fields',
+ self._qui.update_fields.isChecked())
+
+ 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['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'] = str(self._qui.summary_edit.currentText())
+
+ if (len(self._selectedrevs) > 1):
+ opts['parent'] = str(self._selectedrevs[0])
+
+ # Always use the upstream repo to determine the parent diff base
+ # without the diff uploaded to reviewboard dies
+ # TODO: Fix this is a bug in the postreview extension
+ opts['outgoing'] = True
+
+ return opts
+
+ def _isvalid(self):
+ """Filled all required values?"""
+ if not self._qui.repo_id_combo.currentText():
+ return False
+
+ if self._qui.update_review_tab.isActiveWindow():
+ if not self._qui.review_id_combo.currentText():
+ return False
+
+ if not self._allrevs:
+ return False
+
+ return True
+
+ @pyqtSlot()
+ def toggle_outgoing_changesets(self):
+ if self._qui.changesets_view.isEnabled():
+ 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 accept(self):
+ 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())
+ cmd = cmdui.Dialog(['postreview'] + cmdargs(opts) + [revstr], self)
+ cmd.setWindowTitle(_('Posting Review'))
+ cmd.show_output(False)
+ if cmd.exec_():
+ self._writesettings()
+ super(PostReviewDialog, self).accept()
+
+ @pyqtSlot()
+ def on_settings_button_clicked(self):
+ from tortoisehg.hgqt import settings
+
+ settings.SettingsDialog(parent=self, focus='reviewboard.server').exec_()
+
+def run(ui, *pats, **opts):
+ revs = opts.get('rev') or None
+ if not revs and len(pats):
+ revs = pats[0]
+ repo = 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))
|
|
|
@@ -0,0 +1,243 @@ + <?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PostReviewDialog</class>
+ <widget class="QDialog" name="PostReviewDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>660</width>
+ <height>459</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Review Board</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTabWidget" name="tab_widget">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>110</height>
+ </size>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="post_review_tab">
+ <attribute name="title">
+ <string>Post Review</string>
+ </attribute>
+ <layout class="QFormLayout" name="formLayout_2">
+ <item row="0" column="0">
+ <widget class="QLabel" name="repo_id_label">
+ <property name="text">
+ <string>Repository ID:</string>
+ </property>
+ <property name="buddy">
+ <cstring>repo_id_combo</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="repo_id_combo">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="editable">
+ <bool>false</bool>
+ </property>
+ <property name="insertPolicy">
+ <enum>QComboBox::InsertAtTop</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="summary_label">
+ <property name="text">
+ <string>Summary:</string>
+ </property>
+ <property name="buddy">
+ <cstring>summary_edit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="summary_edit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ <property name="insertPolicy">
+ <enum>QComboBox::InsertAtTop</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="update_review_tab">
+ <attribute name="title">
+ <string>Update Review</string>
+ </attribute>
+ <layout class="QFormLayout" name="formLayout_3">
+ <item row="0" column="0">
+ <widget class="QLabel" name="review_id_label">
+ <property name="text">
+ <string>Review ID:</string>
+ </property>
+ <property name="buddy">
+ <cstring>review_id_combo</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="review_id_combo">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="editable">
+ <bool>false</bool>
+ </property>
+ <property name="insertPolicy">
+ <enum>QComboBox::InsertAtTop</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QCheckBox" name="update_fields">
+ <property name="text">
+ <string>Update the fields of this existing request</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="options_group">
+ <property name="title">
+ <string>Options</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QCheckBox" name="outgoing_changes_check">
+ <property name="text">
+ <string>Create diff with all outgoing changesets on this branch</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QCheckBox" name="publish_immediately_check">
+ <property name="text">
+ <string>Publish request immediately</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="changesets_box">
+ <property name="title">
+ <string>Changesets</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QTreeView" name="changesets_view">
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="dialogbuttons_layout">
+ <item>
+ <widget class="QPushButton" name="settings_button">
+ <property name="toolTip">
+ <string extracomment="Configure email settings"/>
+ </property>
+ <property name="text">
+ <string>Settings</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>25</width>
+ <height>19</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="post_review_button">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Post Review</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>changesets_view</tabstop>
+ <tabstop>post_review_button</tabstop>
+ <tabstop>settings_button</tabstop>
+ </tabstops>
+ <resources/>
+<connections>
+ <connection>
+ <sender>post_review_button</sender>
+ <signal>clicked()</signal>
+ <receiver>PostReviewDialog</receiver>
+ <slot>accept()</slot>
+ </connection>
+ <connection>
+ <sender>outgoing_changes_check</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>PostReviewDialog</receiver>
+ <slot>toggle_outgoing_changesets()</slot>
+ </connection>
+</connections>
+</ui>
|
@@ -291,7 +291,8 @@ ('qdelete', _('Delete patch'), None, None, None, self.qdeleteRevision),
('strip', _('Strip...'), None, None, None, self.stripRevision),
('qpop-all', _('Pop all patches'), None, None, None, self.qpopAllRevision),
- ('qgoto', _('Goto patch'), None, None, None, self.qgotoRevision)
+ ('qgoto', _('Goto patch'), None, None, None, self.qgotoRevision),
+ ('postreview', _('Review Board...'), 'reviewboard', None, None, self.sendToReviewBoard)
]
self._actions = {}
@@ -625,10 +626,11 @@ # for unapplied patches.
menu = QMenu(self)
- allactions = [['all', ['update', 'manifest', 'merge', 'tag',
+ allactions = [['all', ['update', 'manifest', 'merge', 'tag',
'backout', 'email', 'archive', 'copyhash']],
['rebase', ['rebase']],
- ['mq', ['qgoto', 'qpop-all', 'qimport', 'qfinish', 'qdelete', 'strip']]]
+ ['mq', ['qgoto', 'qpop-all', 'qimport', 'qfinish', 'qdelete', 'strip']],
+ ['reviewboard', ['postreview']]]
exs = self.repo.extensions()
for ext, actions in allactions:
@@ -661,7 +663,9 @@ 'qimport': normalrev,
'qfinish': appliedpatch,
'qdelete': unappliedpatch,
- 'strip': normalrev}
+ 'strip': normalrev,
+ 'postreview': normalrev}
+
for action, enabled in enabled.iteritems():
self._actions[action].setEnabled(enabled)
@@ -697,6 +701,10 @@ dlg = thgstrip.StripDialog(self.repo, rev=str(self.rev), parent=self)
dlg.exec_()
+ def sendToReviewBoard(self):
+ run.postreview(self.repo.ui, rev=self.repoview.selectedRevisions(),
+ repo=self.repo)
+
def emailRevision(self):
run.email(self.repo.ui, rev=self.repoview.selectedRevisions(),
repo=self.repo)
|
@@ -511,6 +511,11 @@ from tortoisehg.hgqt.resolve import run
qtrun(run, ui, *pats, **opts)
+def postreview(ui, *pats, **opts):
+ """post changesets to reviewboard"""
+ from tortoisehg.hgqt.postreview import run
+ qtrun(run, ui, *pats, **opts)
+
def merge(ui, *pats, **opts):
"""merge wizard"""
from tortoisehg.hgqt.merge import run
|
@@ -487,6 +487,23 @@ (_('Log Font'), 'tortoisehg.fontlog', genFontEdit,
_('Font used to display changelog data. Default: monospace 10')),
)),
+
+({'name': 'reviewboard', 'label': _('Review Board'), 'icon': 'reviewboard'}, (
+ (_('Server'), 'reviewboard.server', genEditCombo,
+ _('Path to review board'
+ ' example "http://demo.reviewboard.org"')),
+ (_('User'), 'reviewboard.user', genEditCombo,
+ _('User name to authenticate with review board')),
+ (_('Password'), 'reviewboard.password', genPasswordEntry,
+ _('Password to authenticate with review board')),
+ (_('Target Groups'), 'reviewboard.target_groups', genEditCombo,
+ _('A comma seperated list of target groups')),
+ (_('Target People'), 'reviewboard.target_people', genEditCombo,
+ _('A comma seperated list of target people')),
+ (_('Browser'), 'reviewboard.browser', genEditCombo,
+ _('The browser to launch new review requests in')),
+ )),
+
)
CONF_GLOBAL = 0
|
@@ -49,5 +49,6 @@ <file>icons/push.svg</file>
<file>icons/qpush.svg</file>
<file>icons/qpop.svg</file>
+ <file>icons/reviewboard.png</file>
</qresource>
</RCC>
|
Loading...