boartty/gertty/view/change.py

376 lines
16 KiB
Python

# Copyright 2014 OpenStack Foundation
#
# 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 urwid
import gitrepo
import mywid
import sync
import view.diff
class ReviewDialog(urwid.WidgetWrap):
signals = ['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')
cancel_button = mywid.FixedButton(u'Cancel')
urwid.connect_signal(save_button, 'click',
lambda button:self._emit('save'))
urwid.connect_signal(cancel_button, 'click',
lambda button:self._emit('cancel'))
buttons = urwid.Columns([('pack', save_button), ('pack', cancel_button)],
dividechars=2)
rows = []
categories = []
values = {}
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.permitted_labels:
if label.category not in categories:
categories.append(label.category)
values[label.category] = []
values[label.category].append(label.value)
pending_approvals = {}
for approval in change.pending_approvals:
pending_approvals[approval.category] = approval
for category in categories:
rows.append(urwid.Text(category))
group = []
self.button_groups[category] = group
current = pending_approvals.get(category)
if current is None:
current = 0
else:
current = current.value
for value in values[category]:
if value > 0:
strvalue = '+%s' % value
elif value == 0:
strvalue = ' 0'
else:
strvalue = str(value)
b = urwid.RadioButton(group, strvalue, state=(value == current))
rows.append(b)
rows.append(urwid.Divider())
for m in revision.messages:
if m.pending:
message = m.message
break
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):
message_key = None
with self.app.db.getSession() as session:
revision = session.getRevision(self.revision_row.revision_key)
change = revision.change
pending_approvals = {}
for approval in change.pending_approvals:
pending_approvals[approval.category] = approval
for category, group in self.button_groups.items():
approval = pending_approvals.get(category)
if not approval:
approval = change.createApproval(u'(draft)', category, 0, pending=True)
pending_approvals[category] = approval
for button in group:
if button.state:
approval.value = int(button.get_label())
message = None
for m in revision.messages:
if m.pending:
message = m
break
if not message:
message = revision.createMessage(None,
datetime.datetime.utcnow(),
u'(draft)', '', pending=True)
message.message = self.message.edit_text.strip()
message_key = message.key
change.reviewed = True
return message_key
def keypress(self, size, key):
r = super(ReviewDialog, self).keypress(size, key)
if r=='esc':
self._emit('cancel')
return None
return r
class ReviewButton(mywid.FixedButton):
def __init__(self, revision_row):
super(ReviewButton, self).__init__(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))
urwid.connect_signal(self.dialog, 'cancel',
lambda button: self.closeReview(False))
self.change_view.app.popup(self.dialog,
relative_width=50, relative_height=75,
min_width=60, min_height=20)
def closeReview(self, save):
if save:
message_key = self.dialog.save()
self.change_view.app.sync.submitTask(
sync.UploadReviewTask(message_key, sync.HIGH_PRIORITY))
self.change_view.refresh()
self.change_view.app.backScreen()
class RevisionRow(urwid.WidgetWrap):
revision_focus_map = {
'revision-name': 'reversed-revision-name',
'revision-commit': 'reversed-revision-commit',
'revision-comments': 'reversed-revision-comments',
'revision-drafts': 'reversed-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
line = [('revision-name', 'Patch Set %s ' % revision.number),
('revision-commit', revision.commit)]
if len(revision.pending_comments):
line.append(('revision-drafts', ' (%s drafts)' % len(revision.pending_comments)))
if len(revision.comments):
line.append(('revision-comments', ' (%s inline comments)' % len(revision.comments)))
self.title = mywid.TextButton(line, on_press = self.expandContract)
stats = repo.diffstat(revision.parent, revision.commit)
rows = []
total_added = 0
total_removed = 0
for added, removed, filename in stats:
total_added += int(added)
total_removed += int(removed)
rows.append(urwid.Columns([urwid.Text(filename),
(10, urwid.Text('+%s, -%s' % (added, removed))),
]))
rows.append(urwid.Columns([urwid.Text(''),
(10, urwid.Text('+%s, -%s' % (total_added, total_removed))),
]))
table = urwid.Pile(rows)
buttons = urwid.Columns([('pack', ReviewButton(self)),
('pack', mywid.FixedButton("Diff", on_press=self.diff)),
('pack', mywid.FixedButton("Checkout", on_press=self.checkout)),
urwid.Text(''),
], dividechars=2)
self.more = urwid.Pile([table, buttons])
self.pile = urwid.Pile([self.title])
self._w = urwid.AttrMap(self.pile, None, focus_map=self.revision_focus_map)
self.expanded = False
if expanded:
self.expandContract(None)
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)
class ChangeMessageBox(urwid.Text):
def __init__(self, message):
super(ChangeMessageBox, self).__init__(u'')
lines = message.message.split('\n')
text = [('change-message-name', message.name),
('change-message-header', ': '+lines.pop(0))]
if lines and lines[-1]:
lines.append('')
text += '\n'.join(lines)
self.set_text(text)
class ChangeView(urwid.WidgetWrap):
help = """
<r> Toggle the reviewed flag for the current change.
<ESC> Go back to the previous screen.
"""
def __init__(self, app, change_key):
super(ChangeView, self).__init__(urwid.Pile([]))
self.app = app
self.change_key = change_key
self.revision_rows = {}
self.message_rows = {}
self.change_id_label = urwid.Text(u'', wrap='clip')
self.owner_label = urwid.Text(u'', wrap='clip')
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 = []
for l, v in [("Change-Id", self.change_id_label),
("Owner", self.owner_label),
("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 = urwid.Text(u'')
top = urwid.Columns([change_info, ('weight', 1, self.commit_message)])
votes = mywid.Table([])
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((top, ('pack', None)))
self._w.contents.append((urwid.Divider(), ('pack', 1)))
self._w.contents.append((votes, ('pack', None)))
self._w.contents.append((urwid.Divider(), ('pack', 1)))
self._w.contents.append((self.listbox, ('weight', 1)))
self._w.set_focus(6)
self.refresh()
def refresh(self):
change_info = []
with self.app.db.getSession() as session:
change = session.getChange(self.change_key)
if change.reviewed:
reviewed = ' (reviewed)'
else:
reviewed = ''
self.title = 'Change %s%s' % (change.number, reviewed)
self.app.status.update(title=self.title)
self.project_key = change.project.key
self.change_id_label.set_text(('change-data', change.change_id))
self.owner_label.set_text(('change-data', change.owner))
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', change.topic or ''))
self.created_label.set_text(('change-data', str(change.created)))
self.updated_label.set_text(('change-data', str(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 = {}
for approval in change.approvals:
approvals = approvals_for_name.get(approval.name)
if not approvals:
approvals = {}
row = []
row.append(urwid.Text(approval.name))
for i, category in enumerate(categories):
w = urwid.Text(u'')
approvals[category] = w
row.append(w)
approvals_for_name[approval.name] = approvals
votes.addRow(row)
if str(approval.value) != '0':
approvals[approval.category].set_text(str(approval.value))
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._w.contents[4] = (votes, ('pack', None))
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 = 0
for revno, revision in enumerate(change.revisions):
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
# 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
for message in change.messages:
row = self.message_rows.get(message.key)
if not row:
row = ChangeMessageBox(message)
self.listbox.body.insert(listbox_index, row)
self.message_rows[message.key] = row
# Messages are extremely unlikely to be deleted, skip
# that case.
listbox_index += 1
def toggleReviewed(self):
with self.app.db.getSession() as session:
change = session.getChange(self.change_key)
change.reviewed = not change.reviewed
def keypress(self, size, key):
r = super(ChangeView, self).keypress(size, key)
if r=='r':
self.toggleReviewed()
self.refresh()
return None
return r
def diff(self, revision_key):
self.app.changeScreen(view.diff.DiffView(self.app, revision_key))