
This is implemented by a callout to the widget's existing handler for key_up and key_down events, and letting the key_* event propagate into an appropriate place where it's handled as the user's input. Reusing that handler makes it possible to work in a generic manner, so the code has no builtin knowledge of the type or layout of the class it's operating on. The same code is used for project listing, change listing, single change history view and a diff view as well. Change-Id: I14600f9dbdf1c066cf5f497f9afcf4e213f03257
1068 lines
46 KiB
Python
1068 lines
46 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 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_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 ''
|
|
if change.reviewed:
|
|
reviewed = ' (reviewed)'
|
|
else:
|
|
reviewed = ''
|
|
if change.hidden:
|
|
hidden = ' (hidden)'
|
|
else:
|
|
hidden = ''
|
|
if change.starred:
|
|
starred = '* '
|
|
else:
|
|
starred = ''
|
|
self.title = '%sChange %s%s%s' % (starred, change.number,
|
|
reviewed, hidden)
|
|
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 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.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()
|