diff --git a/examples/gerrit-gertty.yaml b/examples/gerrit-gertty.yaml index 3227586..05260c4 100644 --- a/examples/gerrit-gertty.yaml +++ b/examples/gerrit-gertty.yaml @@ -47,8 +47,10 @@ dashboards: # change screen. Any pending comments or review messages will be # attached to the review; otherwise an empty review will be left. The # approvals list is exhaustive, so if you specify an empty list, -# Gertty will submit a review that clears any previous approvals. -# They will appear in the help text for the change screen. +# Gertty will submit a review that clears any previous approvals. To +# submit the change with the review, include 'submit: True' with the +# reviewkey. Reviewkeys appear in the help text for the change +# screen. reviewkeys: - key: 'meta 0' approvals: [] @@ -60,3 +62,8 @@ reviewkeys: approvals: - category: 'Code-Review' value: 2 + - key: 'meta 3' + approvals: + - category: 'Code-Review' + value: 2 + submit: True diff --git a/examples/reference-gertty.yaml b/examples/reference-gertty.yaml index e8536e7..e049793 100644 --- a/examples/reference-gertty.yaml +++ b/examples/reference-gertty.yaml @@ -157,8 +157,10 @@ dashboards: # change screen. Any pending comments or review messages will be # attached to the review; otherwise an empty review will be left. The # approvals list is exhaustive, so if you specify an empty list, -# Gertty will submit a review that clears any previous approvals. -# They will appear in the help text for the change screen. +# Gertty will submit a review that clears any previous approvals. To +# submit the change with the review, include 'submit: True' with the +# reviewkey. Reviewkeys appear in the help text for the change +# screen. reviewkeys: - key: 'meta 0' approvals: [] @@ -170,3 +172,8 @@ reviewkeys: approvals: - category: 'Code-Review' value: 2 + - key: 'meta 3' + approvals: + - category: 'Code-Review' + value: 2 + submit: True \ No newline at end of file diff --git a/gertty/alembic/versions/312cd5a9f878_add_can_submit_column.py b/gertty/alembic/versions/312cd5a9f878_add_can_submit_column.py new file mode 100644 index 0000000..8b4b054 --- /dev/null +++ b/gertty/alembic/versions/312cd5a9f878_add_can_submit_column.py @@ -0,0 +1,36 @@ +"""add can_submit column + +Revision ID: 312cd5a9f878 +Revises: 46b175bfa277 +Create Date: 2014-09-18 16:37:13.149729 + +""" + +# revision identifiers, used by Alembic. +revision = '312cd5a9f878' +down_revision = '46b175bfa277' + +import warnings + +from alembic import op +import sqlalchemy as sa + +from gertty.dbsupport import sqlite_alter_columns + + +def upgrade(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + op.add_column('revision', sa.Column('can_submit', sa.Boolean())) + + conn = op.get_bind() + q = sa.text('update revision set can_submit=:submit') + res = conn.execute(q, submit=False) + + sqlite_alter_columns('revision', [ + sa.Column('can_submit', sa.Boolean(), nullable=False), + ]) + + +def downgrade(): + pass diff --git a/gertty/config.py b/gertty/config.py index db0b9e3..e6a3338 100644 --- a/gertty/config.py +++ b/gertty/config.py @@ -83,6 +83,7 @@ class ConfigSchema(object): v.Required('value'): int} reviewkey = {v.Required('approvals'): [reviewkey_approval], + 'submit': bool, v.Required('key'): str} reviewkeys = [reviewkey] diff --git a/gertty/db.py b/gertty/db.py index e463624..591a625 100644 --- a/gertty/db.py +++ b/gertty/db.py @@ -74,6 +74,7 @@ revision_table = Table( Column('fetch_auth', Boolean, nullable=False), Column('fetch_ref', String(255), nullable=False), Column('pending_message', Boolean, index=True, nullable=False), + Column('can_submit', Boolean, nullable=False), ) message_table = Table( 'message', metadata, @@ -268,7 +269,8 @@ class Change(object): class Revision(object): def __init__(self, change, number, message, commit, parent, - fetch_auth, fetch_ref, pending_message=False): + fetch_auth, fetch_ref, pending_message=False, + can_submit=False): self.change_key = change.key self.number = number self.message = message @@ -277,6 +279,7 @@ class Revision(object): self.fetch_auth = fetch_auth self.fetch_ref = fetch_ref self.pending_message = pending_message + self.can_submit = can_submit def createMessage(self, *args, **kw): session = Session.object_session(self) diff --git a/gertty/keymap.py b/gertty/keymap.py index 75e9ce4..afb5189 100644 --- a/gertty/keymap.py +++ b/gertty/keymap.py @@ -52,6 +52,7 @@ CHERRY_PICK_CHANGE = 'cherry pick change' REFRESH = 'refresh' EDIT_TOPIC = 'edit topic' EDIT_COMMIT_MESSAGE = 'edit commit message' +SUBMIT_CHANGE = 'submit change' # Project list screen: TOGGLE_LIST_REVIEWED = 'toggle list reviewed' TOGGLE_LIST_SUBSCRIBED = 'toggle list subscribed' @@ -95,6 +96,7 @@ DEFAULT_KEYMAP = { REFRESH: 'ctrl r', EDIT_TOPIC: 'ctrl t', EDIT_COMMIT_MESSAGE: 'ctrl d', + SUBMIT_CHANGE: 'ctrl u', TOGGLE_LIST_REVIEWED: 'l', TOGGLE_LIST_SUBSCRIBED: 'L', diff --git a/gertty/sync.py b/gertty/sync.py index 95e6a29..e758a7a 100644 --- a/gertty/sync.py +++ b/gertty/sync.py @@ -306,7 +306,7 @@ class SyncChangeTask(Task): def run(self, sync): app = sync.app - remote_change = sync.get('changes/%s?o=DETAILED_LABELS&o=ALL_REVISIONS&o=ALL_COMMITS&o=MESSAGES&o=DETAILED_ACCOUNTS' % self.change_id) + remote_change = sync.get('changes/%s?o=DETAILED_LABELS&o=ALL_REVISIONS&o=ALL_COMMITS&o=MESSAGES&o=DETAILED_ACCOUNTS&o=CURRENT_ACTIONS' % self.change_id) # Perform subqueries this task will need outside of the db session for remote_commit, remote_revision in remote_change.get('revisions', {}).items(): remote_comments_data = sync.get('changes/%s/revisions/%s/comments' % (self.change_id, remote_commit)) @@ -360,6 +360,8 @@ class SyncChangeTask(Task): revision.message = remote_revision['commit']['message'] # TODO: handle multiple parents parent_revision = session.getRevisionByCommit(revision.parent) + actions = remote_revision.get('actions', {}) + revision.can_submit = 'submit' in actions # TODO: use a singleton list of closed states if not parent_revision and change.status not in ['MERGED', 'ABANDONED']: sync.submitTask(SyncChangeByCommitTask(revision.parent, self.priority)) @@ -667,6 +669,8 @@ class ChangeStatusTask(Task): elif change.status == 'NEW': sync.post('changes/%s/restore' % (change.id,), data) + elif change.status == 'SUBMITTED': + sync.post('changes/%s/submit' % (change.id,), {}) sync.submitTask(SyncChangeTask(change.id, priority=self.priority)) class SendCherryPickTask(Task): @@ -722,11 +726,16 @@ class UploadReviewTask(Task): def run(self, sync): app = sync.app + submit = False + change_id = None with app.db.getSession() as session: message = session.getMessage(self.message_key) revision = message.revision change = message.revision.change + change_id = change.id current_revision = change.revisions[-1] + if change.pending_status and change.status == 'SUBMITTED': + submit = True data = dict(message=message.message, strict_labels=False) if revision == current_revision: @@ -753,7 +762,15 @@ class UploadReviewTask(Task): # Inside db session for rollback sync.post('changes/%s/revisions/%s/review' % (change.id, revision.commit), data) - sync.submitTask(SyncChangeTask(change.id, priority=self.priority)) + if submit: + # In another db session in case submit fails after posting + # the message succeeds + with app.db.getSession() as session: + change = session.getChangeByID(change_id) + change.pending_status = False + change.pending_status_message = None + sync.post('changes/%s/submit' % (change_id,), {}) + sync.submitTask(SyncChangeTask(change_id, priority=self.priority)) class Sync(object): def __init__(self, app): diff --git a/gertty/view/change.py b/gertty/view/change.py index a530c6f..2056743 100644 --- a/gertty/view/change.py +++ b/gertty/view/change.py @@ -81,19 +81,26 @@ class CherryPickDialog(urwid.WidgetWrap): 'Propose Change to Branch')) class ReviewDialog(urwid.WidgetWrap): - signals = ['save', 'cancel'] + signals = ['submit', 'save', 'cancel'] def __init__(self, revision_row): self.revision_row = revision_row self.change_view = revision_row.change_view self.app = self.change_view.app save_button = mywid.FixedButton(u'Save') + submit_button = mywid.FixedButton(u'Save and Submit') cancel_button = mywid.FixedButton(u'Cancel') urwid.connect_signal(save_button, 'click', lambda button:self._emit('save')) + urwid.connect_signal(submit_button, 'click', + lambda button:self._emit('submit')) urwid.connect_signal(cancel_button, 'click', lambda button:self._emit('cancel')) - buttons = urwid.Columns([('pack', save_button), ('pack', cancel_button)], - dividechars=2) + + buttons = [('pack', save_button)] + if revision_row.can_submit: + buttons.append(('pack', submit_button)) + buttons.append(('pack', cancel_button)) + buttons = urwid.Columns(buttons, dividechars=2) rows = [] categories = [] values = {} @@ -143,7 +150,7 @@ class ReviewDialog(urwid.WidgetWrap): fill = urwid.Filler(pile, valign='top') super(ReviewDialog, self).__init__(urwid.LineBox(fill, 'Review')) - def save(self, upload=False): + def save(self, upload=False, submit=False): approvals = {} for category, group in self.button_groups.items(): for button in group: @@ -151,7 +158,7 @@ class ReviewDialog(urwid.WidgetWrap): approvals[category] = int(button.get_label()) message = self.message.edit_text.strip() self.change_view.saveReview(self.revision_row.revision_key, approvals, - message, upload) + message, upload, submit) def keypress(self, size, key): r = super(ReviewDialog, self).keypress(size, key) @@ -172,15 +179,17 @@ class ReviewButton(mywid.FixedButton): def openReview(self): self.dialog = ReviewDialog(self.revision_row) urwid.connect_signal(self.dialog, 'save', - lambda button: self.closeReview(True)) + lambda button: self.closeReview(True, False)) + urwid.connect_signal(self.dialog, 'submit', + lambda button: self.closeReview(True, True)) urwid.connect_signal(self.dialog, 'cancel', - lambda button: self.closeReview(False)) + lambda button: self.closeReview(False, False)) self.change_view.app.popup(self.dialog, relative_width=50, relative_height=75, min_width=60, min_height=20) - def closeReview(self, upload): - self.dialog.save(upload) + def closeReview(self, upload, submit): + self.dialog.save(upload, submit) self.change_view.app.backScreen() class RevisionRow(urwid.WidgetWrap): @@ -198,6 +207,7 @@ class RevisionRow(urwid.WidgetWrap): self.revision_key = revision.key self.project_name = revision.change.project.name self.commit_sha = revision.commit + self.can_submit = revision.can_submit self.title = mywid.TextButton(u'', on_press = self.expandContract) stats = repo.diffstat(revision.parent, revision.commit) table = mywid.Table(columns=3) @@ -233,6 +243,10 @@ class RevisionRow(urwid.WidgetWrap): on_press=self.checkout), mywid.FixedButton(('revision-button', "Local Cherry-Pick"), on_press=self.cherryPick)] + if self.can_submit: + buttons.append(mywid.FixedButton(('revision-button', "Submit"), + on_press=lambda x: self.change_view.doSubmitChange())) + buttons = [('pack', urwid.AttrMap(b, None, focus_map=focus_map)) for b in buttons] buttons = urwid.Columns(buttons + [urwid.Text('')], dividechars=2) buttons = urwid.AttrMap(buttons, 'revision-button') @@ -387,6 +401,8 @@ class ChangeView(urwid.WidgetWrap): "Refresh this change"), (key(keymap.EDIT_TOPIC), "Edit the topic of this change"), + (key(keymap.SUBMIT_CHANGE), + "Submit this change"), (key(keymap.CHERRY_PICK_CHANGE), "Propose this change to another branch"), ] @@ -774,6 +790,9 @@ class ChangeView(urwid.WidgetWrap): sync.SyncChangeTask(self.change_rest_id, priority=sync.HIGH_PRIORITY)) self.app.status.update() return None + if keymap.SUBMIT_CHANGE in commands: + self.doSubmitChange() + return None if keymap.EDIT_TOPIC in commands: self.editTopic() return None @@ -896,6 +915,18 @@ class ChangeView(urwid.WidgetWrap): self.app.backScreen() self.refresh() + def doSubmitChange(self): + change_key = None + with self.app.db.getSession() as session: + change = session.getChange(self.change_key) + change.status = 'SUBMITTED' + change.pending_status = True + change.pending_status_message = None + change_key = change.key + self.app.sync.submitTask( + sync.ChangeStatusTask(change_key, sync.HIGH_PRIORITY)) + self.refresh() + def editTopic(self): dialog = EditTopicDialog(self.app, self.topic) urwid.connect_signal(dialog, 'save', @@ -924,9 +955,10 @@ class ChangeView(urwid.WidgetWrap): self.app.log.debug("Reviewkey %s with approvals %s" % (reviewkey['key'], approvals)) row = self.revision_rows[self.last_revision_key] - self.saveReview(row.revision_key, approvals, '', True) + submit = reviewkey.get('submit', False) + self.saveReview(row.revision_key, approvals, '', True, submit) - def saveReview(self, revision_key, approvals, message, upload): + def saveReview(self, revision_key, approvals, message, upload, submit): message_key = None with self.app.db.getSession() as session: account = session.getAccountByUsername(self.app.config.username) @@ -961,6 +993,10 @@ class ChangeView(urwid.WidgetWrap): message_key = draft_message.key if upload: change.reviewed = True + if submit: + change.status = 'SUBMITTED' + change.pending_status = True + change.pending_status_message = None # Outside of db session if upload: self.app.sync.submitTask(