PageRenderTime 56ms CodeModel.GetById 19ms app.highlight 33ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/postreview.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 400 lines | 289 code | 77 blank | 34 comment | 68 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.
 17from PyQt4.QtCore import *
 18from PyQt4.QtGui import *
 19from mercurial import error, extensions, cmdutil
 20from tortoisehg.util import hglib, paths
 21from tortoisehg.hgqt.i18n import _
 22from tortoisehg.hgqt import cmdui, qtlib, thgrepo
 23from tortoisehg.hgqt.postreview_ui import Ui_PostReviewDialog
 24from tortoisehg.hgqt.hgemail import _ChangesetsModel
 25
 26class LoadReviewDataThread(QThread):
 27    def __init__ (self, dialog):
 28        super(LoadReviewDataThread, self).__init__(dialog)
 29        self.dialog = dialog
 30
 31    def run(self):
 32        msg = None
 33        if not self.dialog.server:
 34            msg = _("Invalid Settings - The ReviewBoard server is not setup")
 35        elif not self.dialog.user:
 36            msg = _("Invalid Settings - Please provide your ReviewBoard username")
 37        else:
 38            rb = extensions.find("reviewboard")
 39            try:
 40                pwd = self.dialog.password
 41                #if we don't have a password send something here to skip
 42                #the cli getpass in the extension. We will set the password
 43                #later
 44                if not pwd:
 45                    pwd = "None"
 46                    
 47                self.reviewboard = rb.make_rbclient(self.dialog.server,
 48                                                    self.dialog.user,
 49                                                    pwd)
 50                self.loadCombos()
 51
 52            except rb.ReviewBoardError, e:
 53                msg = e.msg
 54
 55        self.dialog._error_message = msg
 56
 57    def loadCombos(self):
 58        #Get the index of a users previously selected repo id
 59        index = 0
 60        count = 0
 61
 62        self.dialog.qui.progress_label.setText("Loading repositories...")
 63        for r in self.reviewboard.repositories():
 64            if r.id == self.dialog.repo_id:
 65                index = count
 66            self.dialog.qui.repo_id_combo.addItem(str(r.id) + ": " + r.name)
 67            count += 1
 68
 69        if self.dialog.qui.repo_id_combo.count():
 70            self.dialog.qui.repo_id_combo.setCurrentIndex(index)
 71
 72        self.dialog.qui.progress_label.setText("Loading existing reviews...")
 73        for r in self.reviewboard.pending_user_requests():
 74            summary = str(r.id) + ": " + r.summary[0:100]
 75            self.dialog.qui.review_id_combo.addItem(summary)
 76
 77        if self.dialog.qui.review_id_combo.count():
 78            self.dialog.qui.review_id_combo.setCurrentIndex(0)
 79
 80class PostReviewDialog(QDialog):
 81    """Dialog for sending patches to reviewboard"""
 82    def __init__(self, ui, repo, revs, parent=None):
 83        super(PostReviewDialog, self).__init__(parent)
 84        self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
 85        self.ui = ui
 86        self.repo = repo
 87        self.error_message = None
 88        self.cmd = None
 89
 90        self.qui = Ui_PostReviewDialog()
 91        self.qui.setupUi(self)
 92
 93        self.initChangesets(revs)
 94        self.readSettings()
 95
 96        self.review_thread = LoadReviewDataThread(self)
 97        self.review_thread.finished.connect(self.errorPrompt)              
 98        self.review_thread.start()
 99
100    def keyPressEvent(self, event):
101        # don't post review by just hitting enter
102        if event.key() in (Qt.Key_Return, Qt.Key_Enter):
103            if event.modifiers() == Qt.ControlModifier and self.isValid():
104                self.accept()  # Ctrl+Enter
105
106            return
107
108        super(PostReviewDialog, self).keyPressEvent(event)
109
110    @pyqtSlot()
111    def passwordPrompt(self):
112        pwd, ok = QInputDialog.getText(self,
113                                       _('Review Board'),
114                                       _('Password:'),
115                                       mode=QLineEdit.Password)
116        if ok and pwd:
117            self.password = pwd
118            return True
119        else:
120            self.password = None
121            return False
122
123    @pyqtSlot()
124    def errorPrompt(self):
125        self.qui.progress_bar.hide()
126        self.qui.progress_label.hide()
127
128        if self.error_message:
129            qtlib.ErrorMsgBox(_('Review Board'),
130                              _('Error'), self.error_message)
131            self.close()
132        elif self.isValid():
133            self.qui.post_review_button.setEnabled(True)
134
135    def closeEvent(self, event):
136        if self.cmd and self.cmd.core.running():
137            self.cmd.commandFinished.disconnect(self.onCompletion)
138            self.cmd.cancel()
139            
140        # Dispose of the review data thread
141        self.review_thread.terminate()
142        self.review_thread.wait()
143
144        self.writeSettings()
145        super(PostReviewDialog, self).closeEvent(event)
146
147    def readSettings(self):
148        s = QSettings()
149
150        self.restoreGeometry(s.value('reviewboard/geom').toByteArray())
151
152        self.qui.publish_immediately_check.setChecked(
153                s.value('reviewboard/publish_immediately_check').toBool())
154        self.qui.outgoing_changes_check.setChecked(
155                s.value('reviewboard/outgoing_changes_check').toBool())
156        self.qui.branch_check.setChecked(
157                s.value('reviewboard/branch_check').toBool())
158        self.qui.update_fields.setChecked(
159                s.value('reviewboard/update_fields').toBool())
160        self.qui.summary_edit.addItems(
161                s.value('reviewboard/summary_edit_history').toStringList())
162
163        try:
164            self.repo_id = int(self.repo.ui.config('reviewboard', 'repoid'))
165        except Exception:
166            self.repo_id = None
167            
168        if not self.repo_id: 
169            self.repo_id = s.value('reviewboard/repo_id').toInt()[0]
170
171        self.server = self.repo.ui.config('reviewboard', 'server')
172        self.user = self.repo.ui.config('reviewboard', 'user')
173        self.password = self.repo.ui.config('reviewboard', 'password')
174        self.browser = self.repo.ui.config('reviewboard', 'browser')        
175
176    def writeSettings(self):
177        s = QSettings()
178        s.setValue('reviewboard/geom', self.saveGeometry())
179        s.setValue('reviewboard/publish_immediately_check',
180                   self.qui.publish_immediately_check.isChecked())
181        s.setValue('reviewboard/branch_check',
182                   self.qui.branch_check.isChecked())
183        s.setValue('reviewboard/outgoing_changes_check',
184                   self.qui.outgoing_changes_check.isChecked())
185        s.setValue('reviewboard/update_fields',
186                   self.qui.update_fields.isChecked())
187        s.setValue('reviewboard/repo_id', self.getRepoId())
188
189        def itercombo(w):
190            if w.currentText():
191                yield w.currentText()
192            for i in xrange(w.count()):
193                if w.itemText(i) != w.currentText():
194                    yield w.itemText(i)
195
196        s.setValue('reviewboard/summary_edit_history',
197                   list(itercombo(self.qui.summary_edit))[:10])
198
199    def initChangesets(self, revs, selected_revs=None):
200        def purerevs(revs):
201            return cmdutil.revrange(self.repo,
202                                    iter(str(e) for e in revs))
203        if selected_revs:
204             selectedrevs = purerevs(selected_revs)
205        else:
206             selectedrevs = purerevs(revs)
207
208        self._changesets = _ChangesetsModel(self.repo,
209                                            # TODO: [':'] is inefficient
210                                            revs=purerevs(revs or [':']),
211                                            selectedrevs=selectedrevs,
212                                            parent=self)
213
214        self.qui.changesets_view.setModel(self._changesets)
215
216    @property
217    def selectedRevs(self):
218        """Returns list of revisions to be sent"""
219        return self._changesets.selectedrevs
220
221    @property
222    def allRevs(self):
223        """Returns list of revisions to be sent"""
224        return self._changesets.revs
225
226    def getRepoId(self):
227        comboText = self.qui.repo_id_combo.currentText().split(":")
228        return str(comboText[0])
229
230    def getReviewId(self):
231        comboText = self.qui.review_id_combo.currentText().split(":")
232        return str(comboText[0])
233
234    def getSummary(self):
235        comboText = self.qui.review_id_combo.currentText().split(":")
236        return str(comboText[1])
237
238    def postReviewOpts(self, **opts):
239        """Generate opts for reviewboard by form values"""        
240        opts['outgoingchanges'] = self.qui.outgoing_changes_check.isChecked()
241        opts['branch'] = self.qui.branch_check.isChecked()
242        opts['publish'] = self.qui.publish_immediately_check.isChecked()
243
244        if self.qui.tab_widget.currentIndex() == 1:
245            opts["existing"] = self.getReviewId()
246            opts['update'] = self.qui.update_fields.isChecked()
247            opts['summary'] = self.getSummary()
248        else:
249            opts['repoid'] = self.getRepoId()
250            opts['summary'] = hglib.fromunicode(self.qui.summary_edit.currentText())
251
252        if (len(self.selectedRevs) > 1):
253            #Set the parent to the revision below the last one on the list
254            #so all checked revisions are included in the request
255            opts['parent'] = str(self.selectedRevs[0] - 1)
256
257        # Always use the upstream repo to determine the parent diff base
258        # without the diff uploaded to review board dies
259        opts['outgoing'] = True
260
261        #Set the password just in  case the user has opted to not save it
262        opts['password'] = str(self.password)
263
264        #Finally we want to pass the repo path to the hg extension
265        opts['repository'] = self.repo.root
266
267        return opts
268
269    def isValid(self):
270        """Filled all required values?"""
271        if not self.qui.repo_id_combo.currentText():
272            return False
273
274        if self.qui.tab_widget.currentIndex() == 1:
275            if not self.qui.review_id_combo.currentText():
276                return False
277
278        if not self.allRevs:
279            return False
280
281        return True
282
283    @pyqtSlot()
284    def tabChanged(self):
285        self.qui.post_review_button.setEnabled(self.isValid())
286
287    @pyqtSlot()
288    def branchCheckToggle(self):
289        if self.qui.branch_check.isChecked():
290            self.qui.outgoing_changes_check.setChecked(False)
291
292        self.toggleOutgoingChangesets()
293
294    @pyqtSlot()
295    def outgoingChangesCheckToggle(self):
296        if self.qui.outgoing_changes_check.isChecked():
297            self.qui.branch_check.setChecked(False)
298
299        self.toggleOutgoingChangesets()
300
301    def toggleOutgoingChangesets(self):
302        branch = self.qui.branch_check.isChecked()
303        outgoing = self.qui.outgoing_changes_check.isChecked()
304        if branch or outgoing:
305            self.initChangesets(self.allRevs, [self.selectedRevs.pop()])
306            self.qui.changesets_view.setEnabled(False)
307        else:
308            self.initChangesets(self.allRevs, self.allRevs)
309            self.qui.changesets_view.setEnabled(True)
310
311    def close(self):        
312        super(PostReviewDialog, self).close()
313
314    def accept(self):
315        if not self.password and not self.passwordPrompt():
316            return
317
318        self.qui.progress_bar.show()
319        self.qui.progress_label.setText("Posting Review...")
320        self.qui.progress_label.show()
321
322        def cmdargs(opts):
323            args = []
324            for k, v in opts.iteritems():
325                if isinstance(v, bool):
326                    if v:
327                        args.append('--%s' % k.replace('_', '-'))
328                else:
329                    for e in isinstance(v, basestring) and [v] or v:
330                        args += ['--%s' % k.replace('_', '-'), e]
331
332            return args
333
334        hglib.loadextension(self.ui, 'reviewboard')
335
336        opts = self.postReviewOpts()
337
338        revstr = str(self.selectedRevs.pop())
339
340        self.qui.post_review_button.setEnabled(False)
341        self.qui.close_button.setEnabled(False)
342
343        self.cmd = cmdui.Runner(_('Review Board'), False, self)
344        self.cmd.commandFinished.connect(self.onCompletion)
345        self.cmd.run(['postreview'] + cmdargs(opts) + [revstr])
346
347    @pyqtSlot()
348    def onCompletion(self):
349        self.qui.progress_bar.hide()
350        self.qui.progress_label.hide()
351
352        output = self.cmd.core.rawoutput()
353
354        saved = 'saved:' in output
355        published = 'published:' in output
356        if (saved or published):
357            if saved:
358                url = output.split('saved: ').pop().strip()
359                msg = _('Review draft posted to %s\n' % url)
360            else:
361                url = output.split('published: ').pop().strip()
362                msg = _('Review published to %s\n' % url)
363
364            QDesktopServices.openUrl(QUrl(url))
365
366            qtlib.InfoMsgBox(_('Review Board'), _('Success'),
367                               msg, parent=self)
368        else:
369            error = output.split('abort: ').pop().strip()
370            if error[:29] == "HTTP Error: basic auth failed":
371                if self.passwordPrompt():
372                    self.accept()
373                else:
374                    self.qui.post_review_button.setEnabled(True)
375                    self.qui.close_button.setEnabled(True)
376                    return
377            else:
378                qtlib.ErrorMsgBox(_('Review Board'),
379                                  _('Error'), error)
380
381        self.writeSettings()
382        super(PostReviewDialog, self).accept()
383
384    @pyqtSlot()
385    def onSettingsButtonClicked(self):
386        from tortoisehg.hgqt import settings
387
388        settings.SettingsDialog(parent=self, focus='reviewboard.server').exec_()
389
390def run(ui, *pats, **opts):
391    revs = opts.get('rev') or None
392    if not revs and len(pats):
393        revs = pats[0]
394    repo = opts.get('repo') or thgrepo.repository(ui, path=paths.find_root())
395
396    try:
397        return PostReviewDialog(repo.ui, repo, revs)
398    except error.RepoLookupError, e:
399        qtlib.ErrorMsgBox(_('Failed to open Review Board dialog'),
400                          hglib.tounicode(e.message))