# Copyright 2014 OpenStack Foundation # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import logging import urwid from gertty import gitrepo from gertty import keymap from gertty import mywid from gertty import sync from gertty.view import side_diff as view_side_diff from gertty.view import unified_diff as view_unified_diff from gertty.view import mouse_scroll_decorator import gertty.view class EditTopicDialog(mywid.ButtonDialog): signals = ['save', 'cancel'] def __init__(self, app, topic): self.app = app save_button = mywid.FixedButton('Save') cancel_button = mywid.FixedButton('Cancel') urwid.connect_signal(save_button, 'click', lambda button:self._emit('save')) urwid.connect_signal(cancel_button, 'click', lambda button:self._emit('cancel')) super(EditTopicDialog, self).__init__("Edit Topic", "Edit the change topic.", entry_prompt="Topic: ", entry_text=topic, buttons=[save_button, cancel_button]) def keypress(self, size, key): r = super(EditTopicDialog, self).keypress(size, key) commands = self.app.config.keymap.getCommands(r) if keymap.ACTIVATE in commands: self._emit('save') return None return r class CherryPickDialog(urwid.WidgetWrap): signals = ['save', 'cancel'] def __init__(self, change): save_button = mywid.FixedButton('Propose Change') cancel_button = mywid.FixedButton('Cancel') urwid.connect_signal(save_button, 'click', lambda button:self._emit('save')) urwid.connect_signal(cancel_button, 'click', lambda button:self._emit('cancel')) button_widgets = [('pack', save_button), ('pack', cancel_button)] button_columns = urwid.Columns(button_widgets, dividechars=2) rows = [] self.entry = urwid.Edit(edit_text=change.revisions[-1].message, multiline=True) self.branch_buttons = [] rows.append(urwid.Text(u"Branch:")) for branch in change.project.branches: b = mywid.FixedRadioButton(self.branch_buttons, branch.name, state=(branch.name == change.branch)) rows.append(b) rows.append(urwid.Divider()) rows.append(urwid.Text(u"Commit message:")) rows.append(self.entry) rows.append(urwid.Divider()) rows.append(button_columns) pile = urwid.Pile(rows) fill = urwid.Filler(pile, valign='top') super(CherryPickDialog, self).__init__(urwid.LineBox(fill, 'Propose Change to Branch')) class ReviewDialog(urwid.WidgetWrap): 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 = [('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 = {} descriptions = {} self.button_groups = {} message = '' with self.app.db.getSession() as session: revision = session.getRevision(self.revision_row.revision_key) change = revision.change if revision == change.revisions[-1]: for label in change.labels: d = descriptions.setdefault(label.category, {}) d[label.value] = label.description vmin = d.setdefault('min', label.value) if label.value < vmin: d['min'] = label.value vmax = d.setdefault('max', label.value) if label.value > vmax: d['max'] = label.value for label in change.permitted_labels: if label.category not in categories: categories.append(label.category) values[label.category] = [] values[label.category].append(label.value) draft_approvals = {} for approval in change.draft_approvals: draft_approvals[approval.category] = approval for category in categories: rows.append(urwid.Text(category)) group = [] self.button_groups[category] = group current = draft_approvals.get(category) if current is None: current = 0 else: current = current.value for value in sorted(values[category], reverse=True): if value > 0: strvalue = '+%s' % value elif value == 0: strvalue = ' 0' else: strvalue = str(value) strvalue += ' ' + descriptions[category][value] b = urwid.RadioButton(group, strvalue, state=(value == current)) b._value = value if value > 0: if value == descriptions[category]['max']: b = urwid.AttrMap(b, 'max-label') else: b = urwid.AttrMap(b, 'positive-label') elif value < 0: if value == descriptions[category]['min']: b = urwid.AttrMap(b, 'min-label') else: b = urwid.AttrMap(b, 'negative-label') rows.append(b) rows.append(urwid.Divider()) m = revision.getPendingMessage() if not m: m = revision.getDraftMessage() if m: message = m.message self.message = urwid.Edit("Message: \n", edit_text=message, multiline=True) rows.append(self.message) rows.append(urwid.Divider()) rows.append(buttons) pile = urwid.Pile(rows) fill = urwid.Filler(pile, valign='top') super(ReviewDialog, self).__init__(urwid.LineBox(fill, 'Review')) def save(self, upload=False, submit=False): approvals = {} for category, group in self.button_groups.items(): for button in group: if button.state: approvals[category] = button._value message = self.message.edit_text.strip() self.change_view.saveReview(self.revision_row.revision_key, approvals, message, upload, submit) def keypress(self, size, key): r = super(ReviewDialog, self).keypress(size, key) commands = self.app.config.keymap.getCommands(r) if keymap.PREV_SCREEN in commands: self._emit('cancel') return None return r class ReviewButton(mywid.FixedButton): def __init__(self, revision_row): super(ReviewButton, self).__init__(('revision-button', u'Review')) self.revision_row = revision_row self.change_view = revision_row.change_view urwid.connect_signal(self, 'click', lambda button: self.openReview()) def openReview(self): self.dialog = ReviewDialog(self.revision_row) urwid.connect_signal(self.dialog, 'save', 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, False)) self.change_view.app.popup(self.dialog, relative_width=50, relative_height=75, min_width=60, min_height=20) def closeReview(self, upload, submit): self.dialog.save(upload, submit) self.change_view.app.backScreen() class RevisionRow(urwid.WidgetWrap): revision_focus_map = { 'revision-name': 'focused-revision-name', 'revision-commit': 'focused-revision-commit', 'revision-comments': 'focused-revision-comments', 'revision-drafts': 'focused-revision-drafts', } def __init__(self, app, change_view, repo, revision, expanded=False): super(RevisionRow, self).__init__(urwid.Pile([])) self.app = app self.change_view = change_view 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) total_added = 0 total_removed = 0 for added, removed, filename in stats: try: added = int(added) except ValueError: added = 0 try: removed = int(removed) except ValueError: removed = 0 total_added += added total_removed += removed table.addRow([urwid.Text(('filename', filename), wrap='clip'), urwid.Text([('lines-added', '+%i' % (added,)), ', '], align=urwid.RIGHT), urwid.Text(('lines-removed', '-%i' % (removed,)))]) table.addRow([urwid.Text(''), urwid.Text([('lines-added', '+%i' % (total_added,)), ', '], align=urwid.RIGHT), urwid.Text(('lines-removed', '-%i' % (total_removed,)))]) table = urwid.Padding(table, width='pack') focus_map={'revision-button': 'focused-revision-button'} self.review_button = ReviewButton(self) buttons = [self.review_button, mywid.FixedButton(('revision-button', "Diff"), on_press=self.diff), mywid.FixedButton(('revision-button', "Local Checkout"), 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') self.more = urwid.Pile([table, buttons]) padded_title = urwid.Padding(self.title, width='pack') self.pile = urwid.Pile([padded_title]) self._w = urwid.AttrMap(self.pile, None, focus_map=self.revision_focus_map) self.expanded = False self.update(revision) if expanded: self.expandContract(None) def update(self, revision): line = [('revision-name', 'Patch Set %s ' % revision.number), ('revision-commit', revision.commit)] num_drafts = len(revision.draft_comments) if num_drafts: pending_message = revision.getPendingMessage() if not pending_message: line.append(('revision-drafts', ' (%s draft%s)' % ( num_drafts, num_drafts>1 and 's' or ''))) num_comments = len(revision.comments) - num_drafts if num_comments: line.append(('revision-comments', ' (%s inline comment%s)' % ( num_comments, num_comments>1 and 's' or ''))) self.title.text.set_text(line) def expandContract(self, button): if self.expanded: self.pile.contents.pop() self.expanded = False else: self.pile.contents.append((self.more, ('pack', None))) self.expanded = True def diff(self, button): self.change_view.diff(self.revision_key) def checkout(self, button): repo = self.app.getRepo(self.project_name) try: repo.checkout(self.commit_sha) dialog = mywid.MessageDialog('Checkout', 'Change checked out in %s' % repo.path) min_height=8 except gitrepo.GitCheckoutError as e: dialog = mywid.MessageDialog('Error', e.msg) min_height=12 urwid.connect_signal(dialog, 'close', lambda button: self.app.backScreen()) self.app.popup(dialog, min_height=min_height) def cherryPick(self, button): repo = self.app.getRepo(self.project_name) try: repo.cherryPick(self.commit_sha) dialog = mywid.MessageDialog('Cherry-Pick', 'Change cherry-picked in %s' % repo.path) min_height=8 except gitrepo.GitCheckoutError as e: dialog = mywid.MessageDialog('Error', e.msg) min_height=12 urwid.connect_signal(dialog, 'close', lambda button: self.app.backScreen()) self.app.popup(dialog, min_height=min_height) class ChangeButton(mywid.FixedButton): button_left = urwid.Text(u' ') button_right = urwid.Text(u' ') def __init__(self, change_view, change_key, text): super(ChangeButton, self).__init__('') self.set_label(text) self.change_view = change_view self.change_key = change_key urwid.connect_signal(self, 'click', lambda button: self.openChange()) def set_label(self, text): super(ChangeButton, self).set_label(text) def openChange(self): self.change_view.app.changeScreen(ChangeView(self.change_view.app, self.change_key)) class ChangeMessageBox(mywid.HyperText): def __init__(self, app, message): super(ChangeMessageBox, self).__init__(u'') self.app = app self.refresh(message) def refresh(self, message): self.message_created = message.created created = self.app.time(message.created) lines = message.message.split('\n') if message.draft: lines.insert(0, '') lines.insert(0, 'Patch Set %s:' % (message.revision.number,)) text = [('change-message-name', message.author_name), ('change-message-header', ': '+lines.pop(0)), ('change-message-header', created.strftime(' (%Y-%m-%d %H:%M:%S%z)'))] if message.draft and not message.pending: text.append(('change-message-draft', ' (draft)')) if lines and lines[-1]: lines.append('') comment_text = ['\n'.join(lines)] for commentlink in self.app.config.commentlinks: comment_text = commentlink.run(self.app, comment_text) self.set_text(text+comment_text) class CommitMessageBox(mywid.HyperText): def __init__(self, app, message): self.app = app super(CommitMessageBox, self).__init__(message) def set_text(self, text): text = [text] for commentlink in self.app.config.commentlinks: text = commentlink.run(self.app, text) super(CommitMessageBox, self).set_text(text) @mouse_scroll_decorator.ScrollByWheel class ChangeView(urwid.WidgetWrap): def help(self): key = self.app.config.keymap.formatKeys ret = [ (key(keymap.LOCAL_CHECKOUT), "Checkout the most recent revision into the local repo"), (key(keymap.DIFF), "Show the diff of the most recent revision"), (key(keymap.TOGGLE_HIDDEN), "Toggle the hidden flag for the current change"), (key(keymap.NEXT_CHANGE), "Go to the next change in the list"), (key(keymap.PREV_CHANGE), "Go to the previous change in the list"), (key(keymap.REVIEW), "Leave a review for the most recent revision"), (key(keymap.TOGGLE_HELD), "Toggle the held flag for the current change"), (key(keymap.TOGGLE_HIDDEN_COMMENTS), "Toggle display of hidden comments"), (key(keymap.SEARCH_RESULTS), "Back to the list of changes"), (key(keymap.TOGGLE_REVIEWED), "Toggle the reviewed flag for the current change"), (key(keymap.TOGGLE_STARRED), "Toggle the starred flag for the current change"), (key(keymap.LOCAL_CHERRY_PICK), "Cherry-pick the most recent revision onto the local repo"), (key(keymap.ABANDON_CHANGE), "Abandon this change"), (key(keymap.EDIT_COMMIT_MESSAGE), "Edit the commit message of this change"), (key(keymap.REBASE_CHANGE), "Rebase this change (remotely)"), (key(keymap.RESTORE_CHANGE), "Restore this change"), (key(keymap.REFRESH), "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"), ] for k in self.app.config.reviewkeys.values(): action = ', '.join(['{category}:{value}'.format(**a) for a in k['approvals']]) ret.append((keymap.formatKey(k['key']), action)) return ret def __init__(self, app, change_key): super(ChangeView, self).__init__(urwid.Pile([])) self.log = logging.getLogger('gertty.view.change') self.app = app self.change_key = change_key self.revision_rows = {} self.message_rows = {} self.last_revision_key = None self.hide_comments = True self.change_id_label = urwid.Text(u'', wrap='clip') self.owner_label = mywid.TextButton(u'', on_press=self.searchOwner) self.project_label = urwid.Text(u'', wrap='clip') self.branch_label = urwid.Text(u'', wrap='clip') self.topic_label = urwid.Text(u'', wrap='clip') self.created_label = urwid.Text(u'', wrap='clip') self.updated_label = urwid.Text(u'', wrap='clip') self.status_label = urwid.Text(u'', wrap='clip') change_info = [] change_info_map={'change-data': 'focused-change-data'} for l, v in [("Change-Id", self.change_id_label), ("Owner", urwid.Padding(urwid.AttrMap(self.owner_label, None, focus_map=change_info_map), width='pack')), ("Project", self.project_label), ("Branch", self.branch_label), ("Topic", self.topic_label), ("Created", self.created_label), ("Updated", self.updated_label), ("Status", self.status_label), ]: row = urwid.Columns([(12, urwid.Text(('change-header', l), wrap='clip')), v]) change_info.append(row) change_info = urwid.Pile(change_info) self.commit_message = CommitMessageBox(app, u'') votes = mywid.Table([]) self.depends_on = urwid.Pile([]) self.depends_on_rows = {} self.needed_by = urwid.Pile([]) self.needed_by_rows = {} self.related_changes = urwid.Pile([self.depends_on, self.needed_by]) self.results = mywid.HyperText(u'') # because it scrolls better than a table self.grid = mywid.MyGridFlow([change_info, self.commit_message, votes, self.results], cell_width=80, h_sep=2, v_sep=1, align='left') self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([])) self._w.contents.append((self.app.header, ('pack', 1))) self._w.contents.append((urwid.Divider(), ('pack', 1))) self._w.contents.append((self.listbox, ('weight', 1))) self._w.set_focus(2) self.listbox.body.append(self.grid) self.listbox.body.append(urwid.Divider()) self.listbox.body.append(self.related_changes) self.listbox.body.append(urwid.Divider()) self.listbox_patchset_start = len(self.listbox.body) self.checkGitRepo() self.refresh() self.listbox.set_focus(3) self.grid.set_focus(1) def checkGitRepo(self): missing_revisions = set() change_number = None change_id = None with self.app.db.getSession() as session: change = session.getChange(self.change_key) change_number = change.number change_id = change.id repo = self.app.getRepo(change.project.name) for revision in change.revisions: if not repo.hasCommit(revision.parent): missing_revisions.add(revision.parent) if not repo.hasCommit(revision.commit): missing_revisions.add(revision.commit) if missing_revisions: break if missing_revisions: if self.app.sync.offline: raise gertty.view.DisplayError("Git commits not present in local repository") self.app.log.warning("Missing some commits for change %s %s", change_number, missing_revisions) task = sync.SyncChangeTask(change_id, force_fetch=True, priority=sync.HIGH_PRIORITY) self.app.sync.submitTask(task) succeeded = task.wait(300) if not succeeded: raise gertty.view.DisplayError("Git commits not present in local repository") def interested(self, event): if not ((isinstance(event, sync.ChangeAddedEvent) and self.change_key in event.related_change_keys) or (isinstance(event, sync.ChangeUpdatedEvent) and self.change_key in event.related_change_keys)): self.log.debug("Ignoring refresh change due to event %s" % (event,)) return False self.log.debug("Refreshing change due to event %s" % (event,)) return True def refresh(self): change_info = [] with self.app.db.getSession() as session: change = session.getChange(self.change_key) self.topic = change.topic or '' self.pending_status_message = change.pending_status_message or '' reviewed = hidden = starred = held = '' if change.reviewed: reviewed = ' (reviewed)' if change.hidden: hidden = ' (hidden)' if change.starred: starred = '* ' if change.held: held = ' (held)' self.title = '%sChange %s%s%s%s' % (starred, change.number, reviewed, hidden, held) self.app.status.update(title=self.title) self.project_key = change.project.key self.change_rest_id = change.id if change.owner: self.owner_email = change.owner.email else: self.owner_email = None self.change_id_label.set_text(('change-data', change.change_id)) self.owner_label.text.set_text(('change-data', change.owner_name)) self.project_label.set_text(('change-data', change.project.name)) self.branch_label.set_text(('change-data', change.branch)) self.topic_label.set_text(('change-data', self.topic)) 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)) self.commit_message.set_text(change.revisions[-1].message) categories = [] approval_headers = [urwid.Text(('table-header', 'Name'))] for label in change.labels: if label.category in categories: continue approval_headers.append(urwid.Text(('table-header', label.category))) categories.append(label.category) votes = mywid.Table(approval_headers) approvals_for_name = {} pending_message = change.revisions[-1].getPendingMessage() for approval in change.approvals: # Don't display draft approvals unless they are pending-upload if approval.draft and not pending_message: continue approvals = approvals_for_name.get(approval.reviewer.name) if not approvals: approvals = {} row = [] row.append(urwid.Text(('reviewer-name', approval.reviewer.name))) for i, category in enumerate(categories): w = urwid.Text(u'', align=urwid.CENTER) approvals[category] = w row.append(w) approvals_for_name[approval.reviewer.name] = approvals votes.addRow(row) if str(approval.value) != '0': cat_min, cat_max = change.getMinMaxPermittedForCategory(approval.category) if approval.value > 0: val = '+%i' % approval.value if approval.value == cat_max: val = ('max-label', val) else: val = ('positive-label', val) else: val = '%i' % approval.value if approval.value == cat_min: val = ('min-label', val) else: val = ('negative-label', val) approvals[approval.category].set_text(val) votes = urwid.Padding(votes, width='pack') # TODO: update the existing table rather than replacing it # wholesale. It will become more important if the table # gets selectable items (like clickable names). self.grid.contents[2] = (votes, ('given', 80)) self.refreshDependencies(session, change) repo = self.app.getRepo(change.project.name) # The listbox has both revisions and messages in it (and # may later contain the vote table and change header), so # keep track of the index separate from the loop. listbox_index = self.listbox_patchset_start for revno, revision in enumerate(change.revisions): self.last_revision_key = revision.key row = self.revision_rows.get(revision.key) if not row: row = RevisionRow(self.app, self, repo, revision, expanded=(revno==len(change.revisions)-1)) self.listbox.body.insert(listbox_index, row) self.revision_rows[revision.key] = row row.update(revision) # Revisions are extremely unlikely to be deleted, skip # that case. listbox_index += 1 if len(self.listbox.body) == listbox_index: self.listbox.body.insert(listbox_index, urwid.Divider()) listbox_index += 1 # Get the set of messages that should be displayed display_messages = [] result_systems = {} for message in change.messages: if (message.revision == change.revisions[-1] and message.author and message.author.name): for commentlink in self.app.config.commentlinks: results = commentlink.getTestResults(self.app, message.message) if results: result_system = result_systems.get(message.author.name, {}) result_systems[message.author.name] = result_system result_system.update(results) skip = False if self.hide_comments and message.author and message.author.name: for regex in self.app.config.hide_comments: if regex.match(message.author.name): skip = True break if not skip: display_messages.append(message) # The set of message keys currently displayed unseen_keys = set(self.message_rows.keys()) # Make sure all of the messages that should be displayed are for message in display_messages: row = self.message_rows.get(message.key) if not row: box = ChangeMessageBox(self.app, message) row = urwid.Padding(box, width=80) self.listbox.body.insert(listbox_index, row) self.message_rows[message.key] = row else: unseen_keys.remove(message.key) if message.created != row.original_widget.message_created: row.original_widget.refresh(message) listbox_index += 1 # Remove any messages that should not be displayed for key in unseen_keys: row = self.message_rows.get(key) self.listbox.body.remove(row) del self.message_rows[key] listbox_index -= 1 self._updateTestResults(result_systems) def _updateTestResults(self, result_systems): text = [] for system, results in result_systems.items(): for job, result in results.items(): text.append(result) if text: self.results.set_text(text) else: self.results.set_text('') def _updateDependenciesWidget(self, changes, widget, widget_rows, header): if not changes: if len(widget.contents) > 0: widget.contents[:] = [] return if len(widget.contents) == 0: widget.contents.append((urwid.Text(('table-header', header)), widget.options())) unseen_keys = set(widget_rows.keys()) i = 1 for key, subject in changes.items(): row = widget_rows.get(key) if not row: row = urwid.AttrMap(urwid.Padding(ChangeButton(self, key, subject), width='pack'), 'link', focus_map={None: 'focused-link'}) row = (row, widget.options('pack')) widget.contents.insert(i, row) if not widget.selectable(): widget.set_focus(i) if not self.related_changes.selectable(): self.related_changes.set_focus(widget) widget_rows[key] = row else: row[0].original_widget.original_widget.set_label(subject) unseen_keys.remove(key) i += 1 for key in unseen_keys: row = widget_rows[key] widget.contents.remove(row) del widget_rows[key] def refreshDependencies(self, session, change): revision = change.revisions[-1] # Handle depends-on parents = {} parent = session.getRevisionByCommit(revision.parent) if parent and parent.change.status != 'MERGED': subject = parent.change.subject if parent != parent.change.revisions[-1]: subject += ' [OUTDATED]' parents[parent.change.key] = subject self._updateDependenciesWidget(parents, self.depends_on, self.depends_on_rows, header='Depends on:') # Handle needed-by children = {} children.update((r.change.key, r.change.subject) for r in session.getRevisionsByParent([revision.commit for revision in change.revisions]) if (r.change.status != 'MERGED' and r == r.change.revisions[-1])) self._updateDependenciesWidget(children, self.needed_by, self.needed_by_rows, header='Needed by:') def toggleReviewed(self): with self.app.db.getSession() as session: change = session.getChange(self.change_key) change.reviewed = not change.reviewed def toggleHidden(self): with self.app.db.getSession() as session: change = session.getChange(self.change_key) change.hidden = not change.hidden def toggleStarred(self): with self.app.db.getSession() as session: change = session.getChange(self.change_key) change.starred = not change.starred change.pending_starred = True self.app.sync.submitTask( sync.ChangeStarredTask(self.change_key, sync.HIGH_PRIORITY)) def toggleHeld(self): return self.app.toggleHeldChange(self.change_key) def keypress(self, size, key): r = super(ChangeView, self).keypress(size, key) commands = self.app.config.keymap.getCommands(r) if keymap.TOGGLE_REVIEWED in commands: self.toggleReviewed() self.refresh() return None if keymap.TOGGLE_HIDDEN in commands: self.toggleHidden() self.refresh() return None if keymap.TOGGLE_STARRED in commands: self.toggleStarred() self.refresh() return None if keymap.TOGGLE_HELD in commands: self.toggleHeld() self.refresh() return None if keymap.REVIEW in commands: row = self.revision_rows[self.last_revision_key] row.review_button.openReview() return None if keymap.DIFF in commands: row = self.revision_rows[self.last_revision_key] row.diff(None) return None if keymap.LOCAL_CHECKOUT in commands: row = self.revision_rows[self.last_revision_key] row.checkout(None) return None if keymap.LOCAL_CHERRY_PICK in commands: row = self.revision_rows[self.last_revision_key] row.cherryPick(None) return None if keymap.SEARCH_RESULTS in commands: widget = self.app.findChangeList() if widget: self.app.backScreen(widget) return None if ((keymap.NEXT_CHANGE in commands) or (keymap.PREV_CHANGE in commands)): widget = self.app.findChangeList() if widget: if keymap.NEXT_CHANGE in commands: new_change_key = widget.getNextChangeKey(self.change_key) else: new_change_key = widget.getPrevChangeKey(self.change_key) if new_change_key: try: view = ChangeView(self.app, new_change_key) self.app.changeScreen(view, push=False) except gertty.view.DisplayError as e: self.app.error(e.message) return None if keymap.TOGGLE_HIDDEN_COMMENTS in commands: self.hide_comments = not self.hide_comments self.refresh() return None if keymap.ABANDON_CHANGE in commands: self.abandonChange() return None if keymap.EDIT_COMMIT_MESSAGE in commands: self.editCommitMessage() return None if keymap.REBASE_CHANGE in commands: self.rebaseChange() return None if keymap.RESTORE_CHANGE in commands: self.restoreChange() return None if keymap.REFRESH in commands: self.app.sync.submitTask( 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 if keymap.CHERRY_PICK_CHANGE in commands: self.cherryPickChange() return None if r in self.app.config.reviewkeys: self.reviewKey(self.app.config.reviewkeys[r]) return None return r def diff(self, revision_key): if self.app.config.diff_view == 'unified': screen = view_unified_diff.UnifiedDiffView(self.app, revision_key) else: screen = view_side_diff.SideDiffView(self.app, revision_key) self.app.changeScreen(screen) def abandonChange(self): dialog = mywid.TextEditDialog(u'Abandon Change', u'Abandon message:', u'Abandon Change', self.pending_status_message) urwid.connect_signal(dialog, 'cancel', self.app.backScreen) urwid.connect_signal(dialog, 'save', lambda button: self.doAbandonRestoreChange(dialog, 'ABANDONED')) self.app.popup(dialog) def restoreChange(self): dialog = mywid.TextEditDialog(u'Restore Change', u'Restore message:', u'Restore Change', self.pending_status_message) urwid.connect_signal(dialog, 'cancel', self.app.backScreen) urwid.connect_signal(dialog, 'save', lambda button: self.doAbandonRestoreChange(dialog, 'NEW')) self.app.popup(dialog) def doAbandonRestoreChange(self, dialog, state): change_key = None with self.app.db.getSession() as session: change = session.getChange(self.change_key) change.status = state change.pending_status = True change.pending_status_message = dialog.entry.edit_text change_key = change.key self.app.sync.submitTask( sync.ChangeStatusTask(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) dialog = mywid.TextEditDialog(u'Edit Commit Message', u'Commit message:', u'Save', change.revisions[-1].message) urwid.connect_signal(dialog, 'cancel', self.app.backScreen) urwid.connect_signal(dialog, 'save', lambda button: self.doEditCommitMessage(dialog)) self.app.popup(dialog, relative_width=50, relative_height=75, min_width=60, min_height=20) def doEditCommitMessage(self, dialog): revision_key = None with self.app.db.getSession() as session: change = session.getChange(self.change_key) revision = change.revisions[-1] revision.message = dialog.entry.edit_text revision.pending_message = True revision_key = revision.key self.app.sync.submitTask( sync.ChangeCommitMessageTask(revision_key, sync.HIGH_PRIORITY)) self.app.backScreen() self.refresh() def rebaseChange(self): dialog = mywid.YesNoDialog(u'Rebase Change', u'Perform a remote rebase of this change?') urwid.connect_signal(dialog, 'no', self.app.backScreen) urwid.connect_signal(dialog, 'yes', self.doRebaseChange) self.app.popup(dialog) def doRebaseChange(self, button=None): change_key = None with self.app.db.getSession() as session: change = session.getChange(self.change_key) change.pending_rebase = True change_key = change.key self.app.sync.submitTask( sync.RebaseChangeTask(change_key, sync.HIGH_PRIORITY)) self.app.backScreen() self.refresh() def cherryPickChange(self): with self.app.db.getSession() as session: change = session.getChange(self.change_key) dialog = CherryPickDialog(change) urwid.connect_signal(dialog, 'cancel', self.app.backScreen) urwid.connect_signal(dialog, 'save', lambda button: self.doCherryPickChange(dialog)) self.app.popup(dialog, relative_width=50, relative_height=75, min_width=60, min_height=20) def doCherryPickChange(self, dialog): cp_key = None with self.app.db.getSession() as session: change = session.getChange(self.change_key) branch = None for button in dialog.branch_buttons: if button.state: branch = button.get_label() message = dialog.entry.edit_text self.app.log.debug("Creating pending cherry-pick of %s to %s" % (change.revisions[-1].commit, branch)) cp = change.revisions[-1].createPendingCherryPick(branch, message) cp_key = cp.key self.app.sync.submitTask( sync.SendCherryPickTask(cp_key, sync.HIGH_PRIORITY)) 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', lambda button: self.closeEditTopic(dialog, True)) urwid.connect_signal(dialog, 'cancel', lambda button: self.closeEditTopic(dialog, False)) self.app.popup(dialog) def closeEditTopic(self, dialog, save): if save: change_key = None with self.app.db.getSession() as session: change = session.getChange(self.change_key) change.topic = dialog.entry.edit_text change.pending_topic = True change_key = change.key self.app.sync.submitTask( sync.SetTopicTask(change_key, sync.HIGH_PRIORITY)) self.app.backScreen() self.refresh() def searchOwner(self, widget): if self.owner_email: self.app.doSearch("status:open owner:%s" % (self.owner_email,)) def reviewKey(self, reviewkey): approvals = {} for a in reviewkey['approvals']: approvals[a['category']] = a['value'] self.app.log.debug("Reviewkey %s with approvals %s" % (reviewkey['key'], approvals)) row = self.revision_rows[self.last_revision_key] submit = reviewkey.get('submit', False) self.saveReview(row.revision_key, approvals, '', True, submit) 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) revision = session.getRevision(revision_key) change = revision.change draft_approvals = {} for approval in change.draft_approvals: draft_approvals[approval.category] = approval categories = set() for label in change.permitted_labels: categories.add(label.category) for category in categories: value = approvals.get(category, 0) approval = draft_approvals.get(category) if not approval: approval = change.createApproval(account, category, 0, draft=True) draft_approvals[category] = approval approval.value = value draft_message = revision.getPendingMessage() if not draft_message: draft_message = revision.getDraftMessage() if not draft_message: if message or upload: draft_message = revision.createMessage(None, account, datetime.datetime.utcnow(), '', draft=True) if draft_message: draft_message.created = datetime.datetime.utcnow() draft_message.message = message draft_message.pending = upload 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( sync.UploadReviewTask(message_key, sync.HIGH_PRIORITY)) self.refresh()