diff --git a/gertty/alembic/versions/02ca927a2b55_fix_server_table.py b/gertty/alembic/versions/02ca927a2b55_fix_server_table.py new file mode 100644 index 0000000..c81b5e6 --- /dev/null +++ b/gertty/alembic/versions/02ca927a2b55_fix_server_table.py @@ -0,0 +1,31 @@ +"""fix_server_table + +Revision ID: 02ca927a2b55 +Revises: 45d33eccc7a7 +Create Date: 2020-12-18 10:41:24.274607 + +""" + +# revision identifiers, used by Alembic. +revision = '02ca927a2b55' +down_revision = '45d33eccc7a7' + +from alembic import op +import sqlalchemy as sa + + +# The original had a reference to own_account.key which is invalid and +# caused later upgrades to fail. Drop and recreate the table to +# correct; Gertty will fill in the data again. + +def upgrade(): + op.drop_table('server') + op.create_table('server', + sa.Column('key', sa.Integer(), nullable=False), + sa.Column('own_account_key', sa.Integer(), sa.ForeignKey('account.key'), index=True), + sa.PrimaryKeyConstraint('key') + ) + + +def downgrade(): + pass diff --git a/gertty/alembic/versions/ad440301e47f_wip.py b/gertty/alembic/versions/ad440301e47f_wip.py new file mode 100644 index 0000000..1840138 --- /dev/null +++ b/gertty/alembic/versions/ad440301e47f_wip.py @@ -0,0 +1,42 @@ +"""wip + +Revision ID: ad440301e47f +Revises: 02ca927a2b55 +Create Date: 2020-12-18 10:42:07.689266 + +""" + +# revision identifiers, used by Alembic. +revision = 'ad440301e47f' +down_revision = '02ca927a2b55' + +from alembic import op +import sqlalchemy as sa + +import warnings + +from gertty.dbsupport import sqlite_alter_columns + + +def upgrade(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + op.add_column('change', sa.Column('wip', sa.Boolean())) + op.add_column('change', sa.Column('pending_wip', sa.Boolean())) + op.add_column('change', sa.Column('pending_wip_message', sa.Text())) + + connection = op.get_bind() + change = sa.sql.table('change', + sa.sql.column('wip', sa.Boolean()), + sa.sql.column('pending_wip', sa.Boolean())) + connection.execute(change.update().values({'wip':False, + 'pending_wip':False})) + + sqlite_alter_columns('change', [ + sa.Column('wip', sa.Boolean(), index=True, nullable=False), + sa.Column('pending_wip', sa.Boolean(), index=True, nullable=False), + ]) + + +def downgrade(): + pass diff --git a/gertty/db.py b/gertty/db.py index 7c47b45..ee509e6 100644 --- a/gertty/db.py +++ b/gertty/db.py @@ -84,6 +84,9 @@ change_table = Table( Column('pending_status_message', Text), Column('last_seen', DateTime, index=True), Column('outdated', Boolean, index=True, nullable=False), + Column('wip', Boolean, index=True, nullable=False), + Column('pending_wip', Boolean, index=True, nullable=False), + Column('pending_wip_message', Text), ) change_conflict_table = Table( 'change_conflict', metadata, @@ -316,11 +319,12 @@ class Topic(object): class Change(object): def __init__(self, project, id, owner, number, branch, change_id, subject, created, updated, status, topic=None, - hidden=False, reviewed=False, starred=False, held=False, - pending_rebase=False, pending_topic=False, - pending_starred=False, pending_status=False, - pending_status_message=None, pending_hashtags=False, - outdated=False): + hidden=False, reviewed=False, starred=False, + wip=False, held=False, pending_rebase=False, + pending_topic=False, pending_starred=False, + pending_status=False, pending_status_message=None, + pending_hashtags=False, pending_wip=False, + pending_wip_message=None, outdated=False): self.project_key = project.key self.account_key = owner.key self.id = id @@ -334,6 +338,7 @@ class Change(object): self.status = status self.hidden = hidden self.reviewed = reviewed + self.wip = wip self.starred = starred self.held = held self.pending_rebase = pending_rebase @@ -342,6 +347,8 @@ class Change(object): self.pending_starred = pending_starred self.pending_status = pending_status self.pending_status_message = pending_status_message + self.pending_wip = pending_wip + self.pending_wip_message = pending_wip_message self.outdated = outdated def getCategories(self): @@ -1098,6 +1105,9 @@ class DatabaseSession(object): def getPendingStarred(self): return self.session().query(Change).filter_by(pending_starred=True).all() + def getPendingWIP(self): + return self.session().query(Change).filter_by(pending_wip=True).all() + def getPendingStatusChanges(self): return self.session().query(Change).filter_by(pending_status=True).all() diff --git a/gertty/keymap.py b/gertty/keymap.py index 04a1976..fe830eb 100644 --- a/gertty/keymap.py +++ b/gertty/keymap.py @@ -57,6 +57,8 @@ PREV_CHANGE = 'previous change' TOGGLE_HIDDEN_COMMENTS = 'toggle hidden comments' ABANDON_CHANGE = 'abandon change' RESTORE_CHANGE = 'restore change' +WIP_CHANGE = 'mark change work in progress' +READY_CHANGE = 'mark change ready for review' REBASE_CHANGE = 'rebase change' CHERRY_PICK_CHANGE = 'cherry pick change' REFRESH = 'refresh' @@ -126,6 +128,8 @@ DEFAULT_KEYMAP = { TOGGLE_HIDDEN_COMMENTS: 't', ABANDON_CHANGE: 'ctrl a', RESTORE_CHANGE: 'ctrl e', + WIP_CHANGE: 'ctrl w', + READY_CHANGE: 'ctrl y', REBASE_CHANGE: 'ctrl b', CHERRY_PICK_CHANGE: 'ctrl x', REFRESH: 'ctrl r', diff --git a/gertty/sync.py b/gertty/sync.py index 0811024..f4d0f1c 100644 --- a/gertty/sync.py +++ b/gertty/sync.py @@ -688,6 +688,10 @@ class SyncChangeTask(Task): change.starred = True else: change.starred = False + if remote_change.get('work_in_progress'): + change.wip = True + else: + change.wip = False change.subject = remote_change['subject'] change.updated = dateutil.parser.parse(remote_change['updated']) change.topic = remote_change.get('topic') @@ -1079,6 +1083,8 @@ class UploadReviewsTask(Task): sync.submitTask(ChangeStatusTask(c.key, self.priority)) for c in session.getPendingStarred(): sync.submitTask(ChangeStarredTask(c.key, self.priority)) + for c in session.getPendingWIP(): + sync.submitTask(ChangeWIPTask(c.key, self.priority)) for c in session.getPendingCherryPicks(): sync.submitTask(SendCherryPickTask(c.key, self.priority)) for r in session.getPendingCommitMessages(): @@ -1228,15 +1234,44 @@ class ChangeStatusTask(Task): change.pending_status_message = None # Inside db session for rollback if change.status == 'ABANDONED': - sync.post('changes/%s/abandon' % (change.id,), - data) + sync.post('changes/%s/abandon' % (change.id,), data) elif change.status == 'NEW': - sync.post('changes/%s/restore' % (change.id,), - data) + 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 ChangeWIPTask(Task): + def __init__(self, change_key, priority=NORMAL_PRIORITY): + super(ChangeWIPTask, self).__init__(priority) + self.change_key = change_key + + def __repr__(self): + return '' % (self.change_key,) + + def __eq__(self, other): + if (other.__class__ == self.__class__ and + other.change_key == self.change_key): + return True + return False + + def run(self, sync): + app = sync.app + with app.db.getSession() as session: + change = session.getChange(self.change_key) + if change.pending_wip_message: + data = dict(message=change.pending_wip_message) + else: + data = {} + change.pending_wip = False + change.pending_wip_message = None + # Inside db session for rollback + if change.wip: + sync.post('changes/%s/wip' % (change.id,), data) + else: + sync.post('changes/%s/ready' % (change.id,), data) + sync.submitTask(SyncChangeTask(change.id, priority=self.priority)) + class SendCherryPickTask(Task): def __init__(self, cp_key, priority=NORMAL_PRIORITY): super(SendCherryPickTask, self).__init__(priority) diff --git a/gertty/view/change.py b/gertty/view/change.py index e8b5c6e..8f38c07 100644 --- a/gertty/view/change.py +++ b/gertty/view/change.py @@ -555,6 +555,8 @@ class ChangeView(urwid.WidgetWrap): "Rebase this change (remotely)"), (keymap.RESTORE_CHANGE, "Restore this change"), + (keymap.READY_CHANGE, + "Mark this change ready for review"), (keymap.REFRESH, "Refresh this change"), (keymap.EDIT_TOPIC, @@ -565,6 +567,8 @@ class ChangeView(urwid.WidgetWrap): "Submit this change"), (keymap.CHERRY_PICK_CHANGE, "Propose this change to another branch"), + (keymap.WIP_CHANGE, + "Mark this change work in progress"), ] def help(self): @@ -734,7 +738,8 @@ class ChangeView(urwid.WidgetWrap): self.hashtags_label.set_text(('change-data', ' '.join([x.name for x in change.hashtags]))) self.created_label.set_text(('change-data', str(self.app.time(change.created)))) self.updated_label.set_text(('change-data', str(self.app.time(change.updated)))) - self.status_label.set_text(('change-data', change.status)) + stat = change.wip and 'WIP' or change.status + self.status_label.set_text(('change-data', stat)) self.permalink_url = urlparse.urljoin(self.app.config.url, str(change.number)) self.permalink_label.text.set_text(('change-data', self.permalink_url)) self.commit_message.set_text(change.revisions[-1].message) @@ -1043,6 +1048,12 @@ class ChangeView(urwid.WidgetWrap): if keymap.ABANDON_CHANGE in commands: self.abandonChange() return None + if keymap.WIP_CHANGE in commands: + self.wipChange() + return None + if keymap.READY_CHANGE in commands: + self.readyChange() + return None if keymap.EDIT_COMMIT_MESSAGE in commands: self.editCommitMessage() return None @@ -1112,6 +1123,39 @@ class ChangeView(urwid.WidgetWrap): self.app.backScreen() self.refresh() + def wipChange(self): + dialog = mywid.TextEditDialog(u'Mark Change Work In Progress', + u'WIP message:', + u'WIP Change', + self.pending_status_message) + urwid.connect_signal(dialog, 'cancel', self.app.backScreen) + urwid.connect_signal(dialog, 'save', lambda button: + self.doWip(dialog, True)) + self.app.popup(dialog) + + def readyChange(self): + dialog = mywid.TextEditDialog(u'Mark Change Ready For Review', + u'Ready message:', + u'Ready Change', + self.pending_status_message) + urwid.connect_signal(dialog, 'cancel', self.app.backScreen) + urwid.connect_signal(dialog, 'save', lambda button: + self.doWip(dialog, False)) + self.app.popup(dialog) + + def doWip(self, dialog, state): + change_key = None + with self.app.db.getSession() as session: + change = session.getChange(self.change_key) + change.wip = state + change.pending_wip = True + change.pending_wip_message = dialog.entry.edit_text + change_key = change.key + self.app.sync.submitTask( + sync.ChangeWIPTask(change_key, sync.HIGH_PRIORITY)) + self.app.backScreen() + self.refresh() + def editCommitMessage(self): with self.app.db.getSession() as session: change = session.getChange(self.change_key)