diff --git a/examples/reference-gertty.yaml b/examples/reference-gertty.yaml index 8432818..256b765 100644 --- a/examples/reference-gertty.yaml +++ b/examples/reference-gertty.yaml @@ -142,6 +142,10 @@ commentlinks: # of the default side-by-side: # diff-view: unified +# Dependent changes are displayed as "threads" in the change list by +# default. To disable this behavior, uncomment the following line: +# thread-changes: false + # Uncomment the following lines to Hide comments by default that match # certain criteria. You can toggle their display with 't'. Currently # the only supported criterion is "author". diff --git a/gertty/config.py b/gertty/config.py index 4f51b71..bce70d0 100644 --- a/gertty/config.py +++ b/gertty/config.py @@ -110,6 +110,7 @@ class ConfigSchema(object): 'change-list-query': str, 'diff-view': str, 'hide-comments': self.hide_comments, + 'thread-changes': bool, }) return schema @@ -200,6 +201,8 @@ class Config(object): for h in self.config.get('hide-comments', []): self.hide_comments.append(re.compile(h['author'])) + self.thread_changes = self.config.get('thread-changes', True) + def getServer(self, name=None): for server in self.config['servers']: if name is None or name == server['name']: diff --git a/gertty/db.py b/gertty/db.py index 381a59f..f8d024d 100644 --- a/gertty/db.py +++ b/gertty/db.py @@ -606,8 +606,10 @@ class DatabaseSession(object): return None def getRevisionsByParent(self, parent): + if isinstance(parent, basestring): + parent = (parent,) try: - return self.session().query(Revision).filter_by(parent=parent).all() + return self.session().query(Revision).filter(Revision.parent.in_(parent)).all() except sqlalchemy.orm.exc.NoResultFound: return [] diff --git a/gertty/view/change.py b/gertty/view/change.py index 6ff3669..06364e2 100644 --- a/gertty/view/change.py +++ b/gertty/view/change.py @@ -745,11 +745,10 @@ class ChangeView(urwid.WidgetWrap): # 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])) + 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:') diff --git a/gertty/view/change_list.py b/gertty/view/change_list.py index 7f5d74c..a913a6a 100644 --- a/gertty/view/change_list.py +++ b/gertty/view/change_list.py @@ -23,6 +23,29 @@ from gertty import sync from gertty.view import change as view_change import gertty.view + +class ThreadStack(object): + def __init__(self): + self.stack = [] + + def push(self, change, children): + self.stack.append([change, children]) + + def pop(self): + change = None + while self.stack: + if self.stack[-1][1]: + # handle children at the tip + return self.stack[-1][1].pop(0) + else: + # current tip has no children, walk up + self.stack.pop() + continue + return None + + def countChildren(self): + return [len(x[1]) for x in self.stack] + class ChangeRow(urwid.Button): change_focus_map = {None: 'focused', 'unreviewed-change': 'focused-unreviewed-change', @@ -60,7 +83,11 @@ class ChangeRow(urwid.Button): else: style = 'unreviewed-change' self.row_style.set_attr_map({None: style}) - self.subject.set_text(change.subject) + if hasattr(change, '_subject'): + subject = change._subject + else: + subject = change.subject + self.subject.set_text(subject) self.number.set_text(str(change.number)) self.project.set_text(change.project.name.split('/')[-1]) self.owner.set_text(change.owner_name) @@ -176,6 +203,15 @@ class ChangeListView(urwid.WidgetWrap): change_list = reversed(lst) else: change_list = lst + if self.app.config.thread_changes: + change_list = self._threadChanges(change_list) + new_rows = [] + if len(self.listbox.body): + focus_pos = self.listbox.focus_position + focus_row = self.listbox.body[focus_pos] + else: + focus_pos = 0 + focus_row = None for change in change_list: row = self.change_rows.get(change.key) if not row: @@ -187,14 +223,78 @@ class ChangeListView(urwid.WidgetWrap): else: row.update(change, self.categories) unseen_keys.remove(change.key) + new_rows.append(row) i += 1 + self.listbox.body[:] = new_rows + if focus_row in self.listbox.body: + pos = self.listbox.body.index(focus_row) + else: + pos = min(focus_pos, len(self.listbox.body)-1) + self.listbox.body.set_focus(pos) if lst: self.header.update(self.categories) for key in unseen_keys: row = self.change_rows[key] - self.listbox.body.remove(row) del self.change_rows[key] + def _threadChanges(self, changes): + ret = [] + stack = ThreadStack() + children = {} + commits = {} + orphans = changes[:] + for change in changes: + for revision in change.revisions: + commits[revision.commit] = change + for change in changes: + revision = change.revisions[-1] + parent = commits.get(revision.parent, None) + if parent: + if parent.revisions[-1].commit != revision.parent: + # Our parent is an outdated revision. This could + # cause a cycle, so skip. This change will not + # appear in the thread, but will still appear in + # the list. TODO: use color to indicate it + # depends on an outdated change. + continue + if change in orphans: + orphans.remove(change) + v = children.get(parent, []) + v.append(change) + children[parent] = v + if orphans: + change = orphans.pop(0) + else: + change = None + while change: + prefix = '' + stack_children = stack.countChildren() + for i, nchildren in enumerate(stack_children): + if nchildren: + if i+1 == len(stack_children): + prefix += u'\u251c' + else: + prefix += u'\u2502' + else: + if i+1 == len(stack_children): + prefix += u'\u2514' + else: + prefix += u' ' + if i+1 == len(stack_children): + prefix += u'\u2500' + else: + prefix += u' ' + subject = '%s%s' % (prefix, change.subject) + change._subject = subject + ret.append(change) + if change in children: + stack.push(change, children[change]) + change = stack.pop() + if (not change) and orphans: + change = orphans.pop(0) + assert len(ret) == len(changes) + return ret + def clearChangeList(self): for key, value in self.change_rows.iteritems(): self.listbox.body.remove(value)