From 324144c90ace30735fd78ed4aca675a93110dd3a Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Thu, 6 Oct 2022 09:04:38 -0700 Subject: [PATCH] WIP: support inline comment threads Change-Id: I570c4a85953511c1f5d74653df9d2c4aae45a474 --- .../fc132b7742df_unresolved_comment.py | 37 +++ gertty/db.py | 95 +++++- gertty/palette.py | 17 + gertty/sync.py | 12 +- gertty/view/change.py | 297 +++++++++++++++++- gertty/view/diff.py | 50 ++- gertty/view/side_diff.py | 99 +++++- gertty/view/unified_diff.py | 48 +-- 8 files changed, 592 insertions(+), 63 deletions(-) create mode 100644 gertty/alembic/versions/fc132b7742df_unresolved_comment.py diff --git a/gertty/alembic/versions/fc132b7742df_unresolved_comment.py b/gertty/alembic/versions/fc132b7742df_unresolved_comment.py new file mode 100644 index 0000000..b2cc392 --- /dev/null +++ b/gertty/alembic/versions/fc132b7742df_unresolved_comment.py @@ -0,0 +1,37 @@ +"""unresolved-comment + +Revision ID: fc132b7742df +Revises: ad440301e47f +Create Date: 2022-07-14 14:58:18.264675 + +""" + +# revision identifiers, used by Alembic. +revision = 'fc132b7742df' +down_revision = 'ad440301e47f' + +import warnings + +from alembic import op +import sqlalchemy as sa + +from gertty.dbsupport import sqlite_alter_columns + + +def upgrade(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + op.add_column('comment', sa.Column('unresolved', sa.Boolean())) + + connection = op.get_bind() + comment = sa.sql.table('comment', + sa.sql.column('unresolved', sa.Boolean())) + connection.execute(comment.update().values({'unresolved':False})) + + sqlite_alter_columns('comment', [ + sa.Column('unresolved', sa.Boolean(), index=True, nullable=False), + ]) + + +def downgrade(): + pass diff --git a/gertty/db.py b/gertty/db.py index 92cc278..a3762a1 100644 --- a/gertty/db.py +++ b/gertty/db.py @@ -138,6 +138,7 @@ comment_table = Table( Column('line', Integer), Column('message', Text, nullable=False), Column('draft', Boolean, index=True, nullable=False), + Column('unresolved', Boolean, index=True, nullable=False), Column('robot_id', String(255)), Column('robot_run_id', String(255)), Column('url', Text()), @@ -317,6 +318,32 @@ class Topic(object): self.projects.remove(project) session.flush() +class Thread: + def __init__(self): + self.origin_id = None + self.ids = set() + self.key = None + self.comments = [] + self.unresolved = False + self.patchset = None + self.path = None + self.line = None + self.has_draft = False + + def addComment(self, path, ps, message, comment): + self.unresolved = comment.unresolved + if comment.id: + self.ids.add(comment.id) + self.comments.append(comment) + if self.key is None: + self.origin_id = comment.id + self.key = (path, ps, comment.line or 0, comment.message) + self.line = comment.line or '' + self.patchset = ps + self.path = path + if comment.draft: + self.has_draft = True + class Change(object): def __init__(self, project, id, owner, number, branch, change_id, subject, created, updated, status, topic=None, @@ -361,6 +388,71 @@ class Change(object): self._updateApprovalCache() return self._approval_cache.get(category, 0) + def createComment(self, timestamp, revision, fileobj, + account, in_reply_to, parent, line, text): + draft_message = revision.getPendingMessage() + if not draft_message: + draft_message = revision.getDraftMessage() + if not draft_message: + draft_message = revision.createMessage(None, account, + timestamp, + '', draft=True) + for other_comment in self.getCommentsForMessage(draft_message): + if other_comment.draft: + other_comment.created = timestamp + draft_message.created = timestamp + comment = fileobj.createComment(None, account, in_reply_to, + timestamp, + parent, + line, text, draft=True) + return comment.key + + def getThreads(self): + if not hasattr(self, '_threads'): + self.makeThreads() + return self._threads + + def getCommentsForMessage(self, target): + messages = {} + for message in self.messages: + messages[(message.author.id, message.created)] = message + + ret = [] + for revno, revision in enumerate(self.revisions): + for file in revision.files: + for comment in file.comments: + found = messages.get((comment.author.id, comment.created)) + if found is target: + ret.append(comment) + return ret + + def makeThreads(self): + threads = [] + + messages = {} + for message in self.messages: + messages[(message.author.id, message.created)] = message + + for revno, revision in enumerate(self.revisions): + for file in revision.files: + for comment in file.comments: + message = messages.get((comment.author.id, comment.created)) + if not message: + continue + comment_ps = revno + 1 + thread = None + for t in threads: + if comment.in_reply_to in t.ids: + thread = t + if not thread: + thread = Thread() + threads.append(thread) + thread.addComment(comment.file.path, comment_ps, message, comment) + comment._thread = thread + comment._message = message + threads.sort(key=lambda t: t.key) + self._threads = threads + def _updateApprovalCache(self): cat_min = {} cat_max = {} @@ -586,7 +678,7 @@ class Message(object): class Comment(object): def __init__(self, file, id, author, in_reply_to, created, parent, line, message, draft=False, - robot_id=None, robot_run_id=None, url=None): + robot_id=None, robot_run_id=None, url=None, unresolved=False): self.file_key = file.key self.account_key = author.key self.id = id @@ -599,6 +691,7 @@ class Comment(object): self.robot_id = robot_id self.robot_run_id = robot_run_id self.url = url + self.unresolved = unresolved class Label(object): def __init__(self, change, category, value, description): diff --git a/gertty/palette.py b/gertty/palette.py index 7e32c09..ce92f29 100644 --- a/gertty/palette.py +++ b/gertty/palette.py @@ -63,6 +63,23 @@ DEFAULT_PALETTE={ 'focused-revision-commit': ['dark blue,standout', ''], 'focused-revision-comments': ['default,standout', ''], 'focused-revision-drafts': ['dark red,standout', ''], + + 'comment-thread-unresolved': ['yellow', ''], + 'comment-thread-unresolved-name': ['yellow', ''], + 'comment-thread-unresolved-header': ['brown', ''], + 'comment-thread-unresolved-own-name': ['light cyan', ''], + 'comment-thread-unresolved-own-header': ['dark cyan', ''], + 'comment-thread-resolved': ['dark gray', ''], + 'comment-thread-resolved-name': ['dark gray', ''], + 'comment-thread-resolved-header': ['dark gray', ''], + 'comment-thread-resolved-own-name': ['dark gray', ''], + 'comment-thread-resolved-own-header': ['dark gray', ''], + + 'comment-thread-checkbox': ['dark magenta', ''], + 'focused-comment-thread-checkbox': ['light magenta', ''], + 'comment-thread-button': ['dark magenta', ''], + 'focused-comment-thread-button': ['light magenta', ''], + 'change-message-name': ['yellow', ''], 'change-message-own-name': ['light cyan', ''], 'change-message-header': ['brown', ''], diff --git a/gertty/sync.py b/gertty/sync.py index f4d0f1c..b696691 100644 --- a/gertty/sync.py +++ b/gertty/sync.py @@ -798,6 +798,10 @@ class SyncChangeTask(Task): username=remote_comment['author'].get('username'), email=remote_comment['author'].get('email')) comment = session.getCommentByID(remote_comment['id']) + # Unresolved can't really change, but we may + # have old data before we started storing + # unresolved. + unresolved = remote_comment.get('unresolved', False) if not comment: # Normalize updated -> created created = dateutil.parser.parse(remote_comment['updated']) @@ -814,12 +818,15 @@ class SyncChangeTask(Task): remote_comment['message'], robot_id = remote_comment.get('robot_id'), robot_run_id = remote_comment.get('robot_run_id'), - url = remote_comment.get('url')) + url = remote_comment.get('url'), + unresolved = unresolved) self.log.info("Created new comment %s for revision %s in local DB.", comment.key, revision.key) else: if comment.author != account: comment.author = account + if comment.unresolved != unresolved: + comment.unresolved = unresolved if remote_revision.get('_gertty_remote_checks_data'): self._updateChecks(session, change, revision, remote_revision['_gertty_remote_checks_data']) # End revisions @@ -1391,9 +1398,12 @@ class UploadReviewTask(Task): comment_list = [] for comment in file.draft_comments: d = dict(line=comment.line, + unresolved=comment.unresolved, message=comment.message) if comment.parent: d['side'] = 'PARENT' + if comment.in_reply_to: + d['in_reply_to'] = comment.in_reply_to comment_list.append(d) session.delete(comment) comments[file.path] = comment_list diff --git a/gertty/view/change.py b/gertty/view/change.py index 65b51d3..3ee2d6b 100644 --- a/gertty/view/change.py +++ b/gertty/view/change.py @@ -394,6 +394,249 @@ class ChangeButton(urwid.Button): except gertty.view.DisplayError as e: self.change_view.app.error(e.message) +class ThreadCommentBox(mywid.HyperText): + def __init__(self, change_view, change, thread, message, comment): + super().__init__('') + self.change_view = change_view + self.app = change_view.app + self.refresh(change, thread, message, comment) + + def selectable(self): + return len(self.selectable_items) > 0 + + def refresh(self, change, thread, message, comment): + self.comment_key = comment.key + self.message_created = message.created + self.message_author = message.author_name + self.message_text = message.message + created = self.app.time(message.created) + unresolved = thread.unresolved + if thread.has_draft: + unresolved = True + if unresolved: + style = 'comment-thread-unresolved' + else: + style = 'comment-thread-resolved' + if self.app.isOwnAccount(message.author): + name_style = f'{style}-own-name' + header_style = f'{style}-own-header' + reviewer_string = message.author_name + else: + name_style = f'{style}-name' + header_style = f'{style}-header' + if message.author.email: + reviewer_string = "%s <%s>" % ( + message.author_name, + message.author.email) + else: + reviewer_string = message.author_name + + text = [(name_style, reviewer_string), + (header_style, f': Patch Set {message.revision.number}'), + (header_style, created.strftime(' (%Y-%m-%d %H:%M:%S%z)'))] + if message.draft and not message.pending: + text.append(('change-message-draft', ' (draft)')) + + #comment_text = ['\n'.join(lines)] + #for commentlink in self.app.config.commentlinks: + # comment_text = commentlink.run(self.app, comment_text) + + text.append('\n') + text.append(comment.message) + self.set_text(text) + +class ThreadCommentEdit(urwid.WidgetWrap): + def __init__(self, app, thread): + super().__init__(urwid.Pile([])) + self.pile = urwid.Pile([]) + self._w = urwid.AttrMap(self.pile, None, focus_map={}) + self.app = app + + # If we save a comment, the resulting key will be stored here + self.key = None + self.comment = mywid.MyEdit(edit_text='', multiline=True, + ring=self.app.ring) + #comment = urwid.Padding(self.comment, width=80) + self.checkbox = urwid.CheckBox('Resolved', + state=not thread.unresolved) + self.pile.contents.append((urwid.AttrMap(self.comment, 'draft-comment'), + ('pack', None))) + self.pile.contents.append((urwid.AttrMap(self.checkbox, + 'comment-thread-checkbox', + 'focused-comment-thread-checkbox'), + ('pack', None))) + #self.focus_position = 1 + +class ThreadBox(urwid.WidgetWrap): + focus_map={'comment-thread-button': 'focused-comment-thread-button'} + + def __init__(self, change_view, change, thread): + super().__init__(urwid.Pile([])) + self.pile = urwid.Pile([]) + self._w = urwid.AttrMap(self.pile, None, focus_map={}) + self.change_view = change_view + self.app = change_view.app + self.change_key = change.key + self.edit = None + + def x(self, button): + pass + + def deleteComment(self, comment_key): + self.app.log.debug("delete comment %s", comment_key) + with self.app.db.getSession() as session: + comment = session.getComment(comment_key) + session.delete(comment) + + def saveComment(self, text): + orig_comment = self.thread.comments[0] + orig_message = orig_comment._message + in_reply_to = orig_comment.id + self.app.log.debug("save comment") + with self.app.db.getSession() as session: + account = session.getOwnAccount() + revision = session.getRevision(orig_message.revision_key) + change = session.getChange(self.change_key) + fileobj = session.getFile(orig_comment.file_key) + now = datetime.datetime.utcnow() + key = change.createComment( + now, revision, fileobj, + account, in_reply_to, + orig_comment.parent, + orig_comment.line, text) + + """ + draft_message = revision.getPendingMessage() + self.app.log.debug("draft message 1 %s", draft_message) + now = datetime.datetime.utcnow() + if not draft_message: + draft_message = revision.getDraftMessage() + self.app.log.debug("draft message 2 %s", draft_message) + if not draft_message: + draft_message = revision.createMessage(None, account, + now, + '', draft=True) + self.app.log.debug("draft message 3 %s", draft_message) + for other_comment in change.getCommentsForMessage(draft_message): + self.app.log.debug("other comment %s", other_comment) + if other_comment.draft: + other_comment.created = now + draft_message.created = now + message_key = draft_message.key + fileobj = session.getFile(orig_comment.file_key) + comment = fileobj.createComment(None, account, in_reply_to, + now, + orig_comment.parent, + orig_comment.line, text, draft=True) + key = comment.key + self.app.log.debug("save comment %s %s", text, key) + return key + """ + + def cleanupEdit(self): + edit = self.edit + if not edit: + return + text = edit.comment.edit_text.strip() + if text: + if edit.key: + with self.app.db.getSession() as session: + comment = session.getComment(edit.key) + comment.message = text + comment.unresolved = not edit.checkbox.get_state() + else: + edit.key = self.saveComment(text) + else: + if edit.key: + self.deleteComment(edit.key) + edit.key = None + self.closeEdit() + self.app.log.debug("XXX close edit %s", self.thread.comments) + self.change_view.refresh() + + def reply(self, button): + self.openEdit() + self.pile.set_focus(len(self.pile.contents)-3) + + def _addEdit(self): + row = urwid.Padding(self.edit, width=80) + self.pile.contents.insert(-2, + (row, ('pack', None))) + + def openEdit(self): + if self.edit: return False + self.edit = ThreadCommentEdit(self.app, self.thread) + self._addEdit() + return True + + def closeEdit(self): + if not self.edit: return + self.edit = None + self.pile.contents.pop(-3) + + def refresh(self, change, thread): + self.thread = thread + self.pile.contents = [] + path = thread.path + if path == '/PATCHSET_LEVEL': + path = '' + if path == '/COMMIT_MSG': + path = 'Commit message' + location = f'Patchset {thread.patchset} {path} {thread.line}' + + unresolved = thread.unresolved + if thread.has_draft: + unresolved = True + if unresolved: + style = 'comment-thread-unresolved' + else: + style = 'comment-thread-resolved' + self.pile.contents.append((urwid.Text((style, location)), ('pack', None))) + + draft_comment = None + #self.listbox.body.append('thread') + listbox_index = 0 + for comment in thread.comments: + message = comment._message + self.app.log.debug("comment: %s %s", message.key, comment.message) + if comment.draft: + draft_comment = comment + else: + box = ThreadCommentBox(self, change, thread, message, comment) + row = urwid.Padding(box, width=80) + self.pile.contents.append((row, ('pack', None))) + #self.message_rows[message.key] = row + + buttons = [ + mywid.FixedButton(('comment-thread-button', "Reply"), + on_press=self.reply), + mywid.FixedButton(('comment-thread-button', "Quote"), + on_press=self.x), + ] + if unresolved: + buttons.append( + mywid.FixedButton(('comment-thread-button', "Ack"), + on_press=self.x)) + buttons.append( + mywid.FixedButton(('comment-thread-button', "Done"), + on_press=self.x)) + + buttons = [('pack', urwid.AttrMap(b, None, focus_map=self.focus_map)) for b in buttons] + buttons = urwid.Columns(buttons + [urwid.Text('')], dividechars=2) + buttons = urwid.AttrMap(buttons, 'comment-thread-button') + + self.pile.contents.append((buttons, ('pack', None))) + self.pile.contents.append((urwid.Text(''), ('pack', None))) + + if draft_comment: + if not self.openEdit(): + self._addEdit() + self.edit.comment.set_edit_text(draft_comment.message) + self.edit.key = draft_comment.key + self.pile.set_focus(len(self.pile.contents)-3) + else: + self.pile.set_focus(len(self.pile.contents)-2) + class ChangeMessageBox(mywid.HyperText): def __init__(self, change_view, change, message): super(ChangeMessageBox, self).__init__(u'') @@ -495,19 +738,22 @@ class ChangeMessageBox(mywid.HyperText): comment_ps = revno + 1 if comment_ps == message.revision.number: comment_ps = None - inline_comments[path].append((comment_ps or 0, comment.line or 0, comment.message)) + comment_key = (comment_ps or 0, comment.line or 0, comment.message) + inline_comments[path].append((comment_key, comment)) for v in inline_comments.values(): - v.sort() + # Sort on comment_key + v.sort(key=lambda x: x[0]) if inline_comments: comment_text.append(u'\n') - for key, value in inline_comments.items(): - if key == '/PATCHSET_LEVEL': - key = 'Patchset' - if key == '/COMMIT_MSG': - key = 'Commit message' - comment_text.append(('filename-inline-comment', u'%s' % key)) - for patchset, line, comment in value: + for path, comment_list in inline_comments.items(): + if path == '/PATCHSET_LEVEL': + path = 'Patchset' + if path == '/COMMIT_MSG': + path = 'Commit message' + comment_text.append(('filename-inline-comment', u'%s' % path)) + for comment_key, comment in comment_list: + patchset, line, message = comment_key location_str = '' if patchset: location_str += "PS%i" % patchset @@ -516,8 +762,9 @@ class ChangeMessageBox(mywid.HyperText): location_str += str(line) if location_str: location_str += ": " - comment_text.append(u'\n %s%s\n' % (location_str, comment)) - + comment_text.append(u'\n %s%s\n' % (location_str, message)) + comment_text.append(f'{comment.unresolved}\n') + self.app.log.debug(f'unresolved: {comment.unresolved}\n') self.set_text(text+comment_text) class CommitMessageBox(mywid.HyperText): @@ -597,6 +844,7 @@ class ChangeView(urwid.WidgetWrap): self.log = logging.getLogger('gertty.view.change') self.app = app self.change_key = change_key + self.thread_rows = {} self.revision_rows = {} self.message_rows = {} self.last_revision_key = None @@ -838,6 +1086,25 @@ class ChangeView(urwid.WidgetWrap): if len(self.listbox.body) == listbox_index: self.listbox.body.insert(listbox_index, urwid.Divider()) listbox_index += 1 + + # XXX + unseen_keys = set(self.thread_rows.keys()) + for thread in change.getThreads(): + self.app.log.debug("thread: %s %s", thread.key, thread.unresolved) + row = self.thread_rows.get(thread.key) + if not row: + row = ThreadBox(self, change, thread) + self.listbox.body.insert(listbox_index, row) + self.thread_rows[thread.key] = row + row.refresh(change, thread) + listbox_index += 1 + unseen_keys.discard(thread.key) + for key in unseen_keys: + row = self.thread_rows.get(key) + self.listbox.body.remove(row) + del self.thread_rows[key] + listbox_index -= 1 + # Get the set of messages that should be displayed display_messages = [] result_systems = {} @@ -1006,10 +1273,18 @@ class ChangeView(urwid.WidgetWrap): return self.app.toggleHeldChange(self.change_key) def keypress(self, size, key): + old_focus = self.listbox.focus if not self.app.input_buffer: key = super(ChangeView, self).keypress(size, key) + new_focus = self.listbox.focus + keys = self.app.input_buffer + [key] commands = self.app.config.keymap.getCommands(keys) + + if (isinstance(old_focus, ThreadBox) and + (old_focus != new_focus or (keymap.PREV_SCREEN in commands))): + old_focus.cleanupEdit() + if keymap.TOGGLE_REVIEWED in commands: self.toggleReviewed() self.refresh() diff --git a/gertty/view/diff.py b/gertty/view/diff.py index 56de5a4..b742e59 100644 --- a/gertty/view/diff.py +++ b/gertty/view/diff.py @@ -100,7 +100,7 @@ class BaseDiffCommentEdit(urwid.Columns): pass class BaseDiffComment(urwid.Columns): - pass + focus_map={'comment-thread-button': 'focused-comment-thread-button'} class BaseDiffLine(urwid.Button): def selectable(self): @@ -193,6 +193,7 @@ class BaseDiffView(urwid.WidgetWrap, mywid.Searchable): self.searchInit() with self.app.db.getSession() as session: new_revision = session.getRevision(self.new_revision_key) + new_revision.change.makeThreads() old_comments = [] new_comments = [] self.old_file_keys = {} @@ -251,12 +252,12 @@ class BaseDiffView(urwid.WidgetWrap, mywid.Searchable): key += '-' + str(comment.line) key += '-' + path comment_list = comment_lists.get(key, []) - if comment.draft: - message = comment.message - else: - message = [('comment-name', comment.author.name), - ('comment', u': '+comment.message)] - comment_list.append((comment.key, message)) + #if comment.draft: + # message = comment.message + #else: + # message = [('comment-name', comment.author.name), + # ('comment', u': '+comment.message)] + comment_list.append((comment.key, comment)) comment_lists[key] = comment_list comment_filenames.add(path) for comment in old_comments: @@ -271,12 +272,12 @@ class BaseDiffView(urwid.WidgetWrap, mywid.Searchable): key += '-' + str(comment.line) key += '-' + path comment_list = comment_lists.get(key, []) - if comment.draft: - message = comment.message - else: - message = [('comment-name', comment.author.name), - ('comment', u': '+comment.message)] - comment_list.append((comment.key, message)) + #if comment.draft: + # message = comment.message + #else: + # message = [('comment-name', comment.author.name), + # ('comment', u': '+comment.message)] + comment_list.append((comment.key, comment)) comment_lists[key] = comment_list comment_filenames.add(path) repo = gitrepo.get_repo(self.project_name, self.app.config) @@ -502,12 +503,15 @@ class BaseDiffView(urwid.WidgetWrap, mywid.Searchable): self.cleanupEdit(old_focus) return r - def makeCommentEdit(self, edit): + def makeCommentEdit(self, edit, **kw): raise NotImplementedError def onSelect(self, button): + self.createComment() + + def createComment(self, **kw): pos = self.listbox.focus_position - e = self.makeCommentEdit(self.listbox.body[pos]) + e = self.makeCommentEdit(self.listbox.body[pos], **kw) self.listbox.body.insert(pos+1, e) self.listbox.focus_position = pos+1 @@ -519,7 +523,7 @@ class BaseDiffView(urwid.WidgetWrap, mywid.Searchable): comment = session.getComment(comment_key) session.delete(comment) - def saveComment(self, context, text, new=True): + def saveComment(self, context, text, in_reply_to, new=True): if (not new) and (not self.old_revision_num): parent = True else: @@ -527,19 +531,33 @@ class BaseDiffView(urwid.WidgetWrap, mywid.Searchable): if new: line_num = context.new_ln file_key = context.new_file_key + revision_key = self.new_revision_key else: line_num = context.old_ln file_key = context.old_file_key + revision_key = self.old_revision_key or self.new_revision_key if file_key is None: raise Exception("Comment is not associated with a file") with self.app.db.getSession() as session: fileobj = session.getFile(file_key) account = session.getOwnAccount() + revision = session.getRevision(revision_key) + change = session.getChange(self.change_key) + now = datetime.datetime.utcnow() + self.log.debug("XXX create comment %s %s", in_reply_to, text) + key = change.createComment( + now, revision, fileobj, + account, in_reply_to, + parent, + line_num, text) + + """ comment = fileobj.createComment(None, account, None, datetime.datetime.utcnow(), parent, line_num, text, draft=True) key = comment.key + """ return key def reviewKey(self, reviewkey): diff --git a/gertty/view/side_diff.py b/gertty/view/side_diff.py index 5b65033..c70f980 100644 --- a/gertty/view/side_diff.py +++ b/gertty/view/side_diff.py @@ -22,13 +22,16 @@ from gertty.view.diff import BaseFileHeader, BaseFileReminder, BaseDiffView LN_COL_WIDTH = 5 class SideDiffCommentEdit(BaseDiffCommentEdit): - def __init__(self, app, context, old_key=None, new_key=None, old=u'', new=u''): + def __init__(self, app, context, old_key=None, new_key=None, old=u'', new=u'', + old_irp=None, new_irp=None): super(SideDiffCommentEdit, self).__init__([]) self.app = app self.context = context # If we save a comment, the resulting key will be stored here self.old_key = old_key self.new_key = new_key + self.old_irp = old_irp + self.new_irp = new_irp self.old = mywid.MyEdit(edit_text=old, multiline=True, ring=app.ring) self.new = mywid.MyEdit(edit_text=new, multiline=True, ring=app.ring) self.contents.append((urwid.Text(u''), ('given', LN_COL_WIDTH, False))) @@ -67,20 +70,62 @@ class SideDiffCommentEdit(BaseDiffCommentEdit): return key class SideDiffComment(BaseDiffComment): - def __init__(self, context, old, new): + def __init__(self, view, context, old, new, old_irp, new_irp): super(SideDiffComment, self).__init__([]) + self.view = view self.context = context - oldt = urwid.Text(old) - newt = urwid.Text(new) - if old: - oldt = urwid.AttrMap(oldt, 'comment') - if new: - newt = urwid.AttrMap(newt, 'comment') + self.old_irp = old_irp + self.new_irp = new_irp + + oldt = self.formatText(old, old_irp, None) + newt = self.formatText(new, None, new_irp) self.contents.append((urwid.Text(u''), ('given', LN_COL_WIDTH, False))) self.contents.append((oldt, ('weight', 1, False))) self.contents.append((urwid.Text(u''), ('given', LN_COL_WIDTH, False))) self.contents.append((newt, ('weight', 1, False))) + def reply(self, button, user_data): + old_irp, new_irp = user_data + self.view.createComment(old_irp=old_irp, new_irp=new_irp) + + def x(self, button): + pass + + def formatText(self, comment, old_irp, new_irp): + if not comment: + return urwid.Text('') + if comment.draft: + message = comment.message + else: + message = [('comment-name', comment.author.name), + ('comment', u': '+comment.message)] + text = urwid.Text(message) + ret = urwid.AttrMap(text, 'comment') + if comment._thread.comments[-1] is comment: + # Last comment in a thread + + buttons = [ + mywid.FixedButton(('comment-thread-button', "Reply"), + on_press=self.reply, user_data=(old_irp, new_irp)), + mywid.FixedButton(('comment-thread-button', "Quote"), + on_press=self.x), + ] + if comment._thread.unresolved: + buttons.append( + mywid.FixedButton(('comment-thread-button', "Ack"), + on_press=self.x)) + buttons.append( + mywid.FixedButton(('comment-thread-button', "Done"), + on_press=self.x)) + + buttons = [('pack', urwid.AttrMap(b, None, focus_map=self.focus_map)) for b in buttons] + buttons = urwid.Columns(buttons + [urwid.Text('')], dividechars=2) + buttons = urwid.AttrMap(buttons, 'comment-thread-button') + + pile = urwid.Pile([ret, buttons]) + ret = pile + return ret + class SideDiffLine(BaseDiffLine): def __init__(self, app, context, old, new, callback=None): super(SideDiffLine, self).__init__('', on_press=callback) @@ -152,14 +197,22 @@ class SideDiffView(BaseDiffView): old_list = comment_lists.pop(key, []) key = 'new-%s-%s' % (new[0], diff.newname) new_list = comment_lists.pop(key, []) + old_irp = new_irp = None while old_list or new_list: old_comment_key = new_comment_key = None old_comment = new_comment = u'' if old_list: (old_comment_key, old_comment) = old_list.pop(0) + if old_comment._thread.comments[0] is old_comment: + # First comment in thread + old_irp = old_comment.id if new_list: (new_comment_key, new_comment) = new_list.pop(0) - lines.append(SideDiffComment(context, old_comment, new_comment)) + if new_comment._thread.comments[0] is new_comment: + # First comment in thread + new_irp = new_comment.id + lines.append(SideDiffComment(self, context, old_comment, new_comment, + old_irp, new_irp)) # see if there are any draft comments for this line key = 'olddraft-%s-%s' % (old[0], diff.oldname) old_list = comment_lists.pop(key, []) @@ -168,14 +221,20 @@ class SideDiffView(BaseDiffView): while old_list or new_list: old_comment_key = new_comment_key = None old_comment = new_comment = u'' + old_irp = new_irp = None if old_list: (old_comment_key, old_comment) = old_list.pop(0) + old_irp = old_comment.in_reply_to + old_comment = old_comment.message if new_list: (new_comment_key, new_comment) = new_list.pop(0) + new_irp = new_comment.in_reply_to + new_comment = new_comment.message lines.append(SideDiffCommentEdit(self.app, context, old_comment_key, new_comment_key, - old_comment, new_comment)) + old_comment, new_comment, + old_irp, new_irp)) return lines def makeFileReminder(self): @@ -197,8 +256,12 @@ class SideDiffView(BaseDiffView): old_comment = new_comment = u'' if old_list: (old_comment_key, old_comment) = old_list.pop(0) + if not old_list: + pass #XXX last comment if new_list: (new_comment_key, new_comment) = new_list.pop(0) + if not new_list: + pass #XXX last comment lines.append(SideDiffComment(context, old_comment, new_comment)) # see if there are any draft comments for this file key = 'olddraft-None-%s' % (diff.oldname,) @@ -208,18 +271,24 @@ class SideDiffView(BaseDiffView): while old_list or new_list: old_comment_key = new_comment_key = None old_comment = new_comment = u'' + old_irp = new_irp = None if old_list: (old_comment_key, old_comment) = old_list.pop(0) + old_irp = old_comment.in_reply_to + old_comment = old_comment.message if new_list: (new_comment_key, new_comment) = new_list.pop(0) + new_irp = new_comment.in_reply_to + new_comment = new_comment.message lines.append(SideDiffCommentEdit(self.app, context, old_comment_key, new_comment_key, - old_comment, new_comment)) + old_comment, new_comment, + old_irp, new_irp)) return lines - def makeCommentEdit(self, edit): - return SideDiffCommentEdit(self.app, edit.context) + def makeCommentEdit(self, edit, **kw): + return SideDiffCommentEdit(self.app, edit.context, **kw) def cleanupEdit(self, edit): if edit.old_key: @@ -233,9 +302,9 @@ class SideDiffView(BaseDiffView): if old or new: if old: edit.old_key = self.saveComment( - edit.context, old, new=False) + edit.context, old, edit.old_irp, new=False) if new: edit.new_key = self.saveComment( - edit.context, new, new=True) + edit.context, new, edit.new_irp, new=True) else: self.listbox.body.remove(edit) diff --git a/gertty/view/unified_diff.py b/gertty/view/unified_diff.py index 15cf1dc..e9228e3 100644 --- a/gertty/view/unified_diff.py +++ b/gertty/view/unified_diff.py @@ -23,12 +23,13 @@ from gertty.view.diff import BaseFileHeader, BaseFileReminder, BaseDiffView LN_COL_WIDTH = 5 class UnifiedDiffCommentEdit(BaseDiffCommentEdit): - def __init__(self, app, context, oldnew, key=None, comment=u''): + def __init__(self, app, context, oldnew, key=None, comment=u'', irp=None): super(UnifiedDiffCommentEdit, self).__init__([]) self.context = context self.oldnew = oldnew # If we save a comment, the resulting key will be stored here self.key = key + self.irp = irp self.comment = mywid.MyEdit(edit_text=comment, multiline=True, ring=app.ring) self.contents.append((urwid.Text(u''), ('given', 8, False))) @@ -37,10 +38,11 @@ class UnifiedDiffCommentEdit(BaseDiffCommentEdit): self.focus_position = 1 class UnifiedDiffComment(BaseDiffComment): - def __init__(self, context, oldnew, comment): + def __init__(self, context, oldnew, comment, irp): super(UnifiedDiffComment, self).__init__([]) self.context = context - text = urwid.AttrMap(urwid.Text(comment), 'comment') + self.irp = irp + text = urwid.AttrMap(urwid.Text(comment.message), 'comment') self.contents.append((urwid.Text(u''), ('given', 8, False))) self.contents.append((text, ('weight', 1, False))) @@ -140,19 +142,21 @@ class UnifiedDiffView(BaseDiffView): # see if there are any comments for this line key = 'old-%s-%s' % (old[0], diff.oldname) old_list = comment_lists.pop(key, []) + # TODO: add reply buttons + old_irp = None while old_list: (old_comment_key, old_comment) = old_list.pop(0) - old_cache.append(UnifiedDiffComment(context, gitrepo.OLD, old_comment)) + old_cache.append(UnifiedDiffComment(context, gitrepo.OLD, old_comment, old_irp)) # see if there are any draft comments for this line key = 'olddraft-%s-%s' % (old[0], diff.oldname) old_list = comment_lists.pop(key, []) while old_list: (old_comment_key, old_comment) = old_list.pop(0) old_cache.append(UnifiedDiffCommentEdit(self.app, - context, - gitrepo.OLD, - old_comment_key, - old_comment)) + context, + gitrepo.OLD, + old_comment_key, + old_comment.message, old_irp)) # new line if context.new_ln is not None and new[1] != ' ': if old_cache: @@ -164,12 +168,14 @@ class UnifiedDiffView(BaseDiffView): # see if there are any comments for this line key = 'new-%s-%s' % (new[0], diff.newname) new_list = comment_lists.pop(key, []) + # TODO: add reply buttons + new_irp = None while new_list: (new_comment_key, new_comment) = new_list.pop(0) if old_cache: - new_cache.append(UnifiedDiffComment(context, gitrepo.NEW, new_comment)) + new_cache.append(UnifiedDiffComment(context, gitrepo.NEW, new_comment, new_irp)) else: - lines.append(UnifiedDiffComment(context, gitrepo.NEW, new_comment)) + lines.append(UnifiedDiffComment(context, gitrepo.NEW, new_comment, new_irp)) # see if there are any draft comments for this line key = 'newdraft-%s-%s' % (new[0], diff.newname) new_list = comment_lists.pop(key, []) @@ -180,13 +186,13 @@ class UnifiedDiffView(BaseDiffView): context, gitrepo.NEW, new_comment_key, - new_comment)) + new_comment.message, new_irp)) else: lines.append(UnifiedDiffCommentEdit(self.app, context, gitrepo.NEW, new_comment_key, - new_comment)) + new_comment.message, new_irp)) else: if old_cache: lines.extend(old_cache) @@ -208,17 +214,19 @@ class UnifiedDiffView(BaseDiffView): old_list = comment_lists.pop(key, []) while old_list: (old_comment_key, old_comment) = old_list.pop(0) - lines.append(UnifiedDiffComment(context, gitrepo.OLD, old_comment)) + lines.append(UnifiedDiffComment(context, gitrepo.OLD, old_comment, old_irp)) # see if there are any draft comments for this file key = 'olddraft-None-%s' % (diff.oldname,) old_list = comment_lists.pop(key, []) + # TODO: add reply buttons + old_irp = None while old_list: (old_comment_key, old_comment) = old_list.pop(0) lines.append(UnifiedDiffCommentEdit(self.app, context, gitrepo.OLD, old_comment_key, - old_comment)) + old_comment, old_irp)) # new line lines.append(UnifiedFileHeader(self.app, context, gitrepo.NEW, diff.oldname, diff.newname, @@ -227,9 +235,11 @@ class UnifiedDiffView(BaseDiffView): # see if there are any comments for this file key = 'new-None-%s' % (diff.newname,) new_list = comment_lists.pop(key, []) + # TODO: add reply buttons + new_irp = None while new_list: (new_comment_key, new_comment) = new_list.pop(0) - lines.append(UnifiedDiffComment(context, gitrepo.NEW, new_comment)) + lines.append(UnifiedDiffComment(context, gitrepo.NEW, new_comment, new_irp)) # see if there are any draft comments for this file key = 'newdraft-None-%s' % (diff.newname,) new_list = comment_lists.pop(key, []) @@ -239,13 +249,13 @@ class UnifiedDiffView(BaseDiffView): context, gitrepo.NEW, new_comment_key, - new_comment)) + new_comment, new_irp)) return lines - def makeCommentEdit(self, edit): + def makeCommentEdit(self, edit, **kw): return UnifiedDiffCommentEdit(self.app, edit.context, - edit.oldnew) + edit.oldnew, **kw) def cleanupEdit(self, edit): if edit.key: @@ -257,6 +267,6 @@ class UnifiedDiffView(BaseDiffView): if edit.oldnew == gitrepo.NEW: new = True edit.key = self.saveComment( - edit.context, comment, new=new) + edit.context, comment, edit.irp, new=new) else: self.listbox.body.remove(edit)