
Add a process mark to the change list so that multiple changes may be selected at once for further operations. Allow multiple changes to be reviwed at once by selecting them with the process mark and then pressing the usual review command key. The categories are simply taken from the first change in the list for simplicity. This should be fine most of the time, but if the changes have different categories available to them, or the user has different access levels, this may not behave as intended. Change-Id: I04a790d91b27b270cf1269c7bcb39c12d857ab32
1048 lines
46 KiB
Python
1048 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 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, app, revision_key):
|
|
self.revision_key = revision_key
|
|
self.app = 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'))
|
|
|
|
rows = []
|
|
categories = []
|
|
values = {}
|
|
descriptions = {}
|
|
self.button_groups = {}
|
|
message = ''
|
|
with self.app.db.getSession() as session:
|
|
revision = session.getRevision(self.revision_key)
|
|
change = revision.change
|
|
buttons = [('pack', save_button)]
|
|
if revision.can_submit:
|
|
buttons.append(('pack', submit_button))
|
|
buttons.append(('pack', cancel_button))
|
|
buttons = urwid.Columns(buttons, dividechars=2)
|
|
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 getValues(self):
|
|
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()
|
|
return (approvals, message)
|
|
|
|
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.change_view.app,
|
|
self.revision_row.revision_key)
|
|
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):
|
|
approvals, message = self.dialog.getValues()
|
|
self.change_view.saveReview(self.revision_row.revision_key, approvals,
|
|
message, 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)
|
|
table = mywid.Table(columns=3)
|
|
total_added = 0
|
|
total_removed = 0
|
|
for rfile in revision.files:
|
|
if rfile.status is None:
|
|
continue
|
|
added = rfile.inserted or 0
|
|
removed = rfile.deleted or 0
|
|
total_added += added
|
|
total_removed += removed
|
|
table.addRow([urwid.Text(('filename', rfile.display_path), 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 = sum([len(f.draft_comments) for f in revision.files])
|
|
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 = sum([len(f.comments) for f in revision.files]) - 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_keys = self.app.saveReviews([revision_key], approvals,
|
|
message, upload, submit)
|
|
if upload:
|
|
for message_key in message_keys:
|
|
self.app.sync.submitTask(
|
|
sync.UploadReviewTask(message_key, sync.HIGH_PRIORITY))
|
|
self.refresh()
|