
Add a schema change to store whether the user can submit a revision. Offer the submit button on that revision, as well as on the review dialog if applicable. Allow the submit action to be bound to reviewkeys. Also bind it to a key command. Change-Id: I341663ac84d2ac09f9b1ef8c9e0dff45d2658e1d
1005 lines
43 KiB
Python
1005 lines
43 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 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
|
|
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 = {}
|
|
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)
|
|
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 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())
|
|
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] = int(button.get_label())
|
|
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
|
|
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',
|
|
message.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)
|
|
|
|
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 mont 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.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.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 = 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 = 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 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 = ''
|
|
self.title = 'Change %s%s%s' % (change.number, reviewed, hidden)
|
|
self.app.status.update(title=self.title)
|
|
self.project_key = change.project.key
|
|
self.change_rest_id = change.id
|
|
|
|
self.change_id_label.set_text(('change-data', change.change_id))
|
|
self.owner_label.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(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 = []
|
|
max_values = {}
|
|
min_values = {}
|
|
approval_headers = [urwid.Text(('table-header', 'Name'))]
|
|
for label in change.labels:
|
|
if label.value > max_values.get(label.category, 0):
|
|
max_values[label.category] = label.value
|
|
if label.value < min_values.get(label.category, 0):
|
|
min_values[label.category] = label.value
|
|
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':
|
|
if approval.value > 0:
|
|
val = '+%i' % approval.value
|
|
if approval.value == max_values.get(approval.category):
|
|
val = ('max-label', val)
|
|
else:
|
|
val = ('positive-label', val)
|
|
else:
|
|
val = '%i' % approval.value
|
|
if approval.value == min_values.get(approval.category):
|
|
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]:
|
|
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:
|
|
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 = {}
|
|
for revision in change.revisions:
|
|
children.update((r.change.key, r.change.subject)
|
|
for r in session.getRevisionsByParent(revision.commit)
|
|
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 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.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 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()
|