gertty/gertty/view/change.py

1088 lines
48 KiB
Python

# 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 urlparse
import urwid
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):
self.app.localCheckoutCommit(self.project_name, self.commit_sha)
def cherryPick(self, button):
self.app.localCherryPickCommit(self.project_name, self.commit_sha)
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):
try:
self.change_view.app.changeScreen(ChangeView(self.change_view.app, self.change_key))
except gertty.view.DisplayError as e:
self.change_view.app.error(e.message)
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,))
if message.author.username == self.app.config.username:
name_style = 'change-message-own-name'
header_style = 'change-message-own-header'
else:
name_style = 'change-message-name'
header_style = 'change-message-header'
text = [(name_style, message.author_name),
(header_style, ': '+lines.pop(0)),
(header_style,
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 = mywid.TextButton(u'', on_press=self.searchChangeId)
self.owner_label = mywid.TextButton(u'', on_press=self.searchOwner)
self.project_label = mywid.TextButton(u'', on_press=self.searchProject)
self.branch_label = urwid.Text(u'', wrap='clip')
self.topic_label = mywid.TextButton(u'', on_press=self.searchTopic)
self.created_label = urwid.Text(u'', wrap='clip')
self.updated_label = urwid.Text(u'', wrap='clip')
self.status_label = urwid.Text(u'', wrap='clip')
self.permalink_label = urwid.Text(u'', wrap='clip')
change_info = []
change_info_map={'change-data': 'focused-change-data'}
for l, v in [("Change-Id", urwid.Padding(urwid.AttrMap(self.change_id_label, None,
focus_map=change_info_map),
width='pack')),
("Owner", urwid.Padding(urwid.AttrMap(self.owner_label, None,
focus_map=change_info_map),
width='pack')),
("Project", urwid.Padding(urwid.AttrMap(self.project_label, None,
focus_map=change_info_map),
width='pack')),
("Branch", self.branch_label),
("Topic", urwid.Padding(urwid.AttrMap(self.topic_label, None,
focus_map=change_info_map),
width='pack')),
("Created", self.created_label),
("Updated", self.updated_label),
("Status", self.status_label),
("Permalink", self.permalink_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):
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.project_name = change.project.name
self.change_rest_id = change.id
self.change_id = change.change_id
if change.owner:
self.owner_email = change.owner.email
else:
self.owner_email = None
self.change_id_label.text.set_text(('change-data', change.change_id))
self.owner_label.text.set_text(('change-data', change.owner_name))
self.project_label.text.set_text(('change-data', change.project.name))
self.branch_label.set_text(('change-data', change.branch))
self.topic_label.text.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))
url = urlparse.urljoin(self.app.config.url, str(change.number))
self.permalink_label.set_text(('change-data', url))
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 = []
if approval.reviewer.username == self.app.config.username:
style = 'reviewer-own-name'
else:
style = 'reviewer-name'
row.append(urwid.Text((style, 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 searchChangeId(self, widget):
self.app.doSearch("status:open change:%s" % (self.change_id,))
def searchOwner(self, widget):
if self.owner_email:
self.app.doSearch("status:open owner:%s" % (self.owner_email,))
def searchProject(self, widget):
self.app.doSearch("status:open project:%s" % (self.project_name,))
def searchTopic(self, widget):
if self.topic:
self.app.doSearch("status:open topic:%s" % (self.topic,))
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()