Add graphlog extension
authorJoel Rosdahl <joel@rosdahl.net>
Mon, 09 Apr 2007 10:39:28 +0200
changeset 4344 345ed833854d
parent 4343 077aafddd35f
child 4345 ec64f263e49a
Add graphlog extension
contrib/sample.hgrc
hgext/graphlog.py
tests/test-glog
tests/test-glog.out
--- a/contrib/sample.hgrc	Thu Apr 12 15:07:05 2007 -0700
+++ b/contrib/sample.hgrc	Mon Apr 09 10:39:28 2007 +0200
@@ -43,6 +43,11 @@
 
 # hgext.gpg =
 
+### graphlog - ASCII graph log
+### hg help glog
+
+# hgext.graphlog =
+
 ### hgk - GUI repository browser
 ### hg help view
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/graphlog.py	Mon Apr 09 10:39:28 2007 +0200
@@ -0,0 +1,266 @@
+# ASCII graph log extension for Mercurial
+#
+# Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
+# 
+# This software may be used and distributed according to the terms of
+# the GNU General Public License, incorporated herein by reference.
+
+import sys
+from mercurial.cmdutil import revrange, show_changeset
+from mercurial.i18n import _
+from mercurial.node import nullid, nullrev
+from mercurial.util import Abort
+
+def revision_grapher(repo, start_rev, stop_rev):
+    """incremental revision grapher
+
+    This generator function walks through the revision history from
+    revision start_rev to revision stop_rev (which must be less than
+    or equal to start_rev) and for each revision emits tuples with the
+    following elements:
+
+      - Current revision.
+      - Current node.
+      - Column of the current node in the set of ongoing edges.
+      - Edges; a list of (col, next_col) indicating the edges between
+        the current node and its parents.
+      - Number of columns (ongoing edges) in the current revision.
+      - The difference between the number of columns (ongoing edges)
+        in the next revision and the number of columns (ongoing edges)
+        in the current revision. That is: -1 means one column removed;
+        0 means no columns added or removed; 1 means one column added.
+    """
+
+    assert start_rev >= stop_rev
+    curr_rev = start_rev
+    revs = []
+    while curr_rev >= stop_rev:
+        node = repo.changelog.node(curr_rev)
+
+        # Compute revs and next_revs.
+        if curr_rev not in revs:
+            # New head.
+            revs.append(curr_rev)
+        rev_index = revs.index(curr_rev)
+        next_revs = revs[:]
+
+        # Add parents to next_revs.
+        parents = get_rev_parents(repo, curr_rev)
+        parents_to_add = []
+        for parent in parents:
+            if parent not in next_revs:
+                parents_to_add.append(parent)
+        parents_to_add.sort()
+        next_revs[rev_index:rev_index + 1] = parents_to_add
+
+        edges = []
+        for parent in parents:
+            edges.append((rev_index, next_revs.index(parent)))
+
+        n_columns_diff = len(next_revs) - len(revs)
+        yield (curr_rev, node, rev_index, edges, len(revs), n_columns_diff)
+
+        revs = next_revs
+        curr_rev -= 1
+
+def get_rev_parents(repo, rev):
+    return [x for x in repo.changelog.parentrevs(rev) if x != nullrev]
+
+def fix_long_right_edges(edges):
+    for (i, (start, end)) in enumerate(edges):
+        if end > start:
+            edges[i] = (start, end + 1)
+
+def draw_edges(edges, nodeline, interline):
+    for (start, end) in edges:
+        if start == end + 1:
+            interline[2 * end + 1] = "/"
+        elif start == end - 1:
+            interline[2 * start + 1] = "\\"
+        elif start == end:
+            interline[2 * start] = "|"
+        else:
+            nodeline[2 * end] = "+"
+            if start > end:
+                (start, end) = (end,start)
+            for i in range(2 * start + 1, 2 * end):
+                if nodeline[i] != "+":
+                    nodeline[i] = "-"
+
+def format_line(line, level, logstr):
+    text = "%-*s %s" % (2 * level, "".join(line), logstr)
+    return "%s\n" % text.rstrip()
+
+def get_nodeline_edges_tail(
+        node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
+    if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
+        # Still going in the same non-vertical direction.
+        if n_columns_diff == -1:
+            start = max(node_index + 1, p_node_index)
+            tail = ["|", " "] * (start - node_index - 1)
+            tail.extend(["/", " "] * (n_columns - start))
+            return tail
+        else:
+            return ["\\", " "] * (n_columns - node_index - 1)
+    else:
+        return ["|", " "] * (n_columns - node_index - 1)
+
+def get_padding_line(ni, n_columns, edges):
+    line = []
+    line.extend(["|", " "] * ni)
+    if (ni, ni - 1) in edges or (ni, ni) in edges:
+        # (ni, ni - 1)      (ni, ni)
+        # | | | |           | | | |
+        # +---o |           | o---+
+        # | | c |           | c | |
+        # | |/ /            | |/ /
+        # | | |             | | |
+        c = "|"
+    else:
+        c = " "
+    line.extend([c, " "])
+    line.extend(["|", " "] * (n_columns - ni - 1))
+    return line
+
+def get_limit(limit_opt):
+    if limit_opt:
+        try:
+            limit = int(limit_opt)
+        except ValueError:
+            raise Abort(_("limit must be a positive integer"))
+        if limit <= 0:
+            raise Abort(_("limit must be positive"))
+    else:
+        limit = sys.maxint
+    return limit
+
+def get_revs(repo, rev_opt):
+    if rev_opt:
+        revs = revrange(repo, rev_opt)
+        return (max(revs), min(revs))
+    else:
+        return (repo.changelog.count() - 1, 0)
+
+def graphlog(ui, repo, *args, **opts):
+    """show revision history alongside an ASCII revision graph
+
+    Print a revision history alongside a revision graph drawn with
+    ASCII characters.
+
+    Nodes printed as an @ character are parents of the working
+    directory.
+    """
+
+    limit = get_limit(opts["limit"])
+    (start_rev, stop_rev) = get_revs(repo, opts["rev"])
+    stop_rev = max(stop_rev, start_rev - limit + 1)
+    if start_rev == nullrev:
+        return
+    cs_printer = show_changeset(ui, repo, opts)
+    grapher = revision_grapher(repo, start_rev, stop_rev)
+    repo_parents = repo.dirstate.parents()
+    prev_n_columns_diff = 0
+    prev_node_index = 0
+
+    for (rev, node, node_index, edges, n_columns, n_columns_diff) in grapher:
+        # log_strings is the list of all log strings to draw alongside
+        # the graph.
+        ui.pushbuffer()
+        cs_printer.show(rev, node)
+        log_strings = ui.popbuffer().split("\n")[:-1]
+
+        if n_columns_diff == -1:
+            # Transform
+            #
+            #     | | |        | | |
+            #     o | |  into  o---+
+            #     |X /         |/ /
+            #     | |          | |
+            fix_long_right_edges(edges)
+
+        # add_padding_line says whether to rewrite
+        #
+        #     | | | |        | | | |
+        #     | o---+  into  | o---+
+        #     |  / /         |   | |  # <--- padding line
+        #     o | |          |  / /
+        #                    o | |
+        add_padding_line = \
+            len(log_strings) > 2 and \
+            n_columns_diff == -1 and \
+            [x for (x, y) in edges if x + 1 < y]
+
+        # fix_nodeline_tail says whether to rewrite
+        #
+        #     | | o | |        | | o | |
+        #     | | |/ /         | | |/ /
+        #     | o | |    into  | o / /   # <--- fixed nodeline tail
+        #     | |/ /           | |/ /
+        #     o | |            o | |
+        fix_nodeline_tail = len(log_strings) <= 2 and not add_padding_line
+
+        # nodeline is the line containing the node character (@ or o).
+        nodeline = ["|", " "] * node_index
+        if node in repo_parents:
+            node_ch = "@"
+        else:
+            node_ch = "o"
+        nodeline.extend([node_ch, " "])
+
+        nodeline.extend(
+            get_nodeline_edges_tail(
+                node_index, prev_node_index, n_columns, n_columns_diff,
+                prev_n_columns_diff, fix_nodeline_tail))
+
+        # shift_interline is the line containing the non-vertical
+        # edges between this entry and the next.
+        shift_interline = ["|", " "] * node_index
+        if n_columns_diff == -1:
+            n_spaces = 1
+            edge_ch = "/"
+        elif n_columns_diff == 0:
+            n_spaces = 2
+            edge_ch = "|"
+        else:
+            n_spaces = 3
+            edge_ch = "\\"
+        shift_interline.extend(n_spaces * [" "])
+        shift_interline.extend([edge_ch, " "] * (n_columns - node_index - 1))
+
+        # Draw edges from the current node to its parents.
+        draw_edges(edges, nodeline, shift_interline)
+
+        # lines is the list of all graph lines to print.
+        lines = [nodeline]
+        if add_padding_line:
+            lines.append(get_padding_line(node_index, n_columns, edges))
+        lines.append(shift_interline)
+
+        # Make sure that there are as many graph lines as there are
+        # log strings.
+        while len(log_strings) < len(lines):
+            log_strings.append("")
+        if len(lines) < len(log_strings):
+            extra_interline = ["|", " "] * (n_columns + n_columns_diff)
+            while len(lines) < len(log_strings):
+                lines.append(extra_interline)
+
+        # Print lines.
+        indentation_level = max(n_columns, n_columns + n_columns_diff)
+        for (line, logstr) in zip(lines, log_strings):
+            ui.write(format_line(line, indentation_level, logstr))
+
+        # ...and start over.
+        prev_node_index = node_index
+        prev_n_columns_diff = n_columns_diff
+
+cmdtable = {
+    "glog":
+    (graphlog,
+     [("l", "limit", "", _("limit number of changes displayed")),
+      ("p", "patch", False, _("show patch")),
+      ("r", "rev", [], _("show the specified revision or range")),
+      ("", "style", "", _("display using template map file")),
+      ("", "template", "", _("display with template"))],
+     "hg glog [OPTIONS]"),
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-glog	Mon Apr 09 10:39:28 2007 +0200
@@ -0,0 +1,140 @@
+#!/bin/sh
+
+# @  (34) head
+# |
+# | o  (33) head
+# | |
+# o |    (32) expand
+# |\ \
+# | o \    (31) expand
+# | |\ \
+# | | o \    (30) expand
+# | | |\ \
+# | | | o |  (29) regular commit
+# | | | | |
+# | | o | |    (28) merge zero known
+# | | |\ \ \
+# o | | | | |  (27) collapse
+# |/ / / / /
+# | | o---+  (26) merge one known; far right
+# | | | | |
+# +---o | |  (25) merge one known; far left
+# | | | | |
+# | | o | |  (24) merge one known; immediate right
+# | | |\| |
+# | | o | |  (23) merge one known; immediate left
+# | |/| | |
+# +---o---+  (22) merge two known; one far left, one far right
+# | |  / /
+# o | | |    (21) expand
+# |\ \ \ \
+# | o---+-+  (20) merge two known; two far right
+# |  / / /
+# o | | |    (19) expand
+# |\ \ \ \
+# +---+---o  (18) merge two known; two far left
+# | | | |
+# | o | |    (17) expand
+# | |\ \ \
+# | | o---+  (16) merge two known; one immediate right, one near right
+# | | |/ /
+# o | | |    (15) expand
+# |\ \ \ \
+# | o-----+  (14) merge two known; one immediate right, one far right
+# | |/ / /
+# o | | |    (13) expand
+# |\ \ \ \
+# +---o | |  (12) merge two known; one immediate right, one far left
+# | | |/ /
+# | o | |    (11) expand
+# | |\ \ \
+# | | o---+  (10) merge two known; one immediate left, one near right
+# | |/ / /
+# o | | |    (9) expand
+# |\ \ \ \
+# | o-----+  (8) merge two known; one immediate left, one far right
+# |/ / / /
+# o | | |    (7) expand
+# |\ \ \ \
+# +---o | |  (6) merge two known; one immediate left, one far left
+# | |/ / /
+# | o | |    (5) expand
+# | |\ \ \
+# | | o | |  (4) merge two known; one immediate left, one immediate right
+# | |/|/ /
+# | o / /  (3) collapse
+# |/ / /
+# o / /  (2) collapse
+# |/ /
+# o /  (1) collapse
+# |/
+# o  (0) root
+
+set -e
+
+commit()
+{
+    rev=$1
+    msg=$2
+    shift 2
+    if [ "$#" -gt 0 ]; then
+        hg debugsetparents "$@"
+    fi
+    echo $rev > $rev
+    hg add $rev
+    hg ci -d "$rev 0" -m "($rev) $msg"
+}
+
+echo "[extensions]" >> $HGRCPATH
+echo "graphlog=" >> $HGRCPATH
+
+echo % init
+hg init repo
+
+cd repo
+
+echo % empty repo
+hg glog
+
+echo % building tree
+commit 0 "root"
+commit 1 "collapse" 0
+commit 2 "collapse" 1
+commit 3 "collapse" 2
+commit 4 "merge two known; one immediate left, one immediate right" 1 3
+commit 5 "expand" 3 4
+commit 6 "merge two known; one immediate left, one far left" 2 5
+commit 7 "expand" 2 5
+commit 8 "merge two known; one immediate left, one far right" 0 7
+commit 9 "expand" 7 8
+commit 10 "merge two known; one immediate left, one near right" 0 6
+commit 11 "expand" 6 10
+commit 12 "merge two known; one immediate right, one far left" 1 9
+commit 13 "expand" 9 11
+commit 14 "merge two known; one immediate right, one far right" 0 12
+commit 15 "expand" 13 14
+commit 16 "merge two known; one immediate right, one near right" 0 1
+commit 17 "expand" 12 16
+commit 18 "merge two known; two far left" 1 15
+commit 19 "expand" 15 17
+commit 20 "merge two known; two far right" 0 18
+commit 21 "expand" 19 20
+commit 22 "merge two known; one far left, one far right" 18 21
+commit 23 "merge one known; immediate left" 1 22
+commit 24 "merge one known; immediate right" 0 23
+commit 25 "merge one known; far left" 21 24
+commit 26 "merge one known; far right" 18 25
+commit 27 "collapse" 21
+commit 28 "merge zero known" 1 26
+commit 29 "regular commit" 0
+commit 30 "expand" 28 29
+commit 31 "expand" 21 30
+commit 32 "expand" 27 31
+commit 33 "head" 18
+commit 34 "head" 32
+
+echo % glog -q
+hg glog -q
+
+echo % glog
+hg glog
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-glog.out	Mon Apr 09 10:39:28 2007 +0200
@@ -0,0 +1,309 @@
+% init
+% empty repo
+% building tree
+% glog -q
+@  34:0eed7cd895e0
+|
+| o  33:2e9d1b521374
+| |
+o |    32:77f7d8438a3c
+|\ \
+| o \    31:82ee55204a79
+| |\ \
+| | o \    30:777dfc428649
+| | |\ \
+| | | o |  29:f8e7fee63353
+| | | | |
+| | o | |    28:4b6e9bd48cf9
+| | |\ \ \
+o | | | | |  27:e9e08174cd30
+|/ / / / /
+| | o---+  26:720dc079a855
+| | | | |
++---o | |  25:9d4ed048d013
+| | | | |
+| | o | |  24:4a68967db00d
+| | |\| |
+| | o | |  23:bc31393cabdf
+| |/| | |
++---o---+  22:a37f2ea6ebc6
+| |  / /
+o | | |    21:e758e8f4ace9
+|\ \ \ \
+| o---+-+  20:aeccadad74b4
+|  / / /
+o | | |    19:138069b5dad7
+|\ \ \ \
++---+---o  18:5a8c9a29ef81
+| | | |
+| o | |    17:43e52b935494
+| |\ \ \
+| | o---+  16:449a2f9562a4
+| | |/ /
+o | | |    15:c0b4283d4c1d
+|\ \ \ \
+| o-----+  14:9d533950abf0
+| |/ / /
+o | | |    13:c39d0a2b8165
+|\ \ \ \
++---o | |  12:74dc7aea4494
+| | |/ /
+| o | |    11:c3c395dd8b98
+| |\ \ \
+| | o---+  10:8094c50149ef
+| |/ / /
+o | | |    9:79ab1812f961
+|\ \ \ \
+| o-----+  8:d7aa38594334
+|/ / / /
+o | | |    7:699392d1259e
+|\ \ \ \
++---o | |  6:0ca7c061cf45
+| |/ / /
+| o | |    5:3589c3c477ab
+| |\ \ \
+| | o | |  4:e2cad8233c77
+| |/|/ /
+| o / /  3:02173ffbf857
+|/ / /
+o / /  2:e8ea2256f9ec
+|/ /
+o /  1:3cae7826a707
+|/
+o  0:7aa22e58e8c1
+
+% glog
+@  changeset:   34:0eed7cd895e0
+|  tag:         tip
+|  parent:      32:77f7d8438a3c
+|  user:        test
+|  date:        Thu Jan 01 00:00:34 1970 +0000
+|  summary:     (34) head
+|
+| o  changeset:   33:2e9d1b521374
+| |  parent:      18:5a8c9a29ef81
+| |  user:        test
+| |  date:        Thu Jan 01 00:00:33 1970 +0000
+| |  summary:     (33) head
+| |
+o |    changeset:   32:77f7d8438a3c
+|\ \   parent:      27:e9e08174cd30
+| | |  parent:      31:82ee55204a79
+| | |  user:        test
+| | |  date:        Thu Jan 01 00:00:32 1970 +0000
+| | |  summary:     (32) expand
+| | |
+| o |    changeset:   31:82ee55204a79
+| |\ \   parent:      21:e758e8f4ace9
+| | | |  parent:      30:777dfc428649
+| | | |  user:        test
+| | | |  date:        Thu Jan 01 00:00:31 1970 +0000
+| | | |  summary:     (31) expand
+| | | |
+| | o |    changeset:   30:777dfc428649
+| | |\ \   parent:      28:4b6e9bd48cf9
+| | | | |  parent:      29:f8e7fee63353
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:30 1970 +0000
+| | | | |  summary:     (30) expand
+| | | | |
+| | | o |  changeset:   29:f8e7fee63353
+| | | | |  parent:      0:7aa22e58e8c1
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:29 1970 +0000
+| | | | |  summary:     (29) regular commit
+| | | | |
+| | o | |    changeset:   28:4b6e9bd48cf9
+| | |\ \ \   parent:      1:3cae7826a707
+| | | | | |  parent:      26:720dc079a855
+| | | | | |  user:        test
+| | | | | |  date:        Thu Jan 01 00:00:28 1970 +0000
+| | | | | |  summary:     (28) merge zero known
+| | | | | |
+o | | | | |  changeset:   27:e9e08174cd30
+|/ / / / /   parent:      21:e758e8f4ace9
+| | | | |    user:        test
+| | | | |    date:        Thu Jan 01 00:00:27 1970 +0000
+| | | | |    summary:     (27) collapse
+| | | | |
+| | o---+  changeset:   26:720dc079a855
+| | | | |  parent:      18:5a8c9a29ef81
+| | | | |  parent:      25:9d4ed048d013
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:26 1970 +0000
+| | | | |  summary:     (26) merge one known; far right
+| | | | |
++---o | |  changeset:   25:9d4ed048d013
+| | | | |  parent:      21:e758e8f4ace9
+| | | | |  parent:      24:4a68967db00d
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:25 1970 +0000
+| | | | |  summary:     (25) merge one known; far left
+| | | | |
+| | o | |  changeset:   24:4a68967db00d
+| | |\| |  parent:      0:7aa22e58e8c1
+| | | | |  parent:      23:bc31393cabdf
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:24 1970 +0000
+| | | | |  summary:     (24) merge one known; immediate right
+| | | | |
+| | o | |  changeset:   23:bc31393cabdf
+| |/| | |  parent:      1:3cae7826a707
+| | | | |  parent:      22:a37f2ea6ebc6
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:23 1970 +0000
+| | | | |  summary:     (23) merge one known; immediate left
+| | | | |
++---o---+  changeset:   22:a37f2ea6ebc6
+| |   | |  parent:      18:5a8c9a29ef81
+| |  / /   parent:      21:e758e8f4ace9
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:22 1970 +0000
+| | | |    summary:     (22) merge two known; one far left, one far right
+| | | |
+o | | |    changeset:   21:e758e8f4ace9
+|\ \ \ \   parent:      19:138069b5dad7
+| | | | |  parent:      20:aeccadad74b4
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:21 1970 +0000
+| | | | |  summary:     (21) expand
+| | | | |
+| o---+-+  changeset:   20:aeccadad74b4
+|   | | |  parent:      0:7aa22e58e8c1
+|  / / /   parent:      18:5a8c9a29ef81
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:20 1970 +0000
+| | | |    summary:     (20) merge two known; two far right
+| | | |
+o | | |    changeset:   19:138069b5dad7
+|\ \ \ \   parent:      15:c0b4283d4c1d
+| | | | |  parent:      17:43e52b935494
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:19 1970 +0000
+| | | | |  summary:     (19) expand
+| | | | |
++---+---o  changeset:   18:5a8c9a29ef81
+| | | |    parent:      1:3cae7826a707
+| | | |    parent:      15:c0b4283d4c1d
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:18 1970 +0000
+| | | |    summary:     (18) merge two known; two far left
+| | | |
+| o | |    changeset:   17:43e52b935494
+| |\ \ \   parent:      12:74dc7aea4494
+| | | | |  parent:      16:449a2f9562a4
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:17 1970 +0000
+| | | | |  summary:     (17) expand
+| | | | |
+| | o---+  changeset:   16:449a2f9562a4
+| | | | |  parent:      0:7aa22e58e8c1
+| | |/ /   parent:      1:3cae7826a707
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:16 1970 +0000
+| | | |    summary:     (16) merge two known; one immediate right, one near right
+| | | |
+o | | |    changeset:   15:c0b4283d4c1d
+|\ \ \ \   parent:      13:c39d0a2b8165
+| | | | |  parent:      14:9d533950abf0
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:15 1970 +0000
+| | | | |  summary:     (15) expand
+| | | | |
+| o-----+  changeset:   14:9d533950abf0
+| | | | |  parent:      0:7aa22e58e8c1
+| |/ / /   parent:      12:74dc7aea4494
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:14 1970 +0000
+| | | |    summary:     (14) merge two known; one immediate right, one far right
+| | | |
+o | | |    changeset:   13:c39d0a2b8165
+|\ \ \ \   parent:      9:79ab1812f961
+| | | | |  parent:      11:c3c395dd8b98
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:13 1970 +0000
+| | | | |  summary:     (13) expand
+| | | | |
++---o | |  changeset:   12:74dc7aea4494
+| | |/ /   parent:      1:3cae7826a707
+| | | |    parent:      9:79ab1812f961
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:12 1970 +0000
+| | | |    summary:     (12) merge two known; one immediate right, one far left
+| | | |
+| o | |    changeset:   11:c3c395dd8b98
+| |\ \ \   parent:      6:0ca7c061cf45
+| | | | |  parent:      10:8094c50149ef
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:11 1970 +0000
+| | | | |  summary:     (11) expand
+| | | | |
+| | o---+  changeset:   10:8094c50149ef
+| | | | |  parent:      0:7aa22e58e8c1
+| |/ / /   parent:      6:0ca7c061cf45
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:10 1970 +0000
+| | | |    summary:     (10) merge two known; one immediate left, one near right
+| | | |
+o | | |    changeset:   9:79ab1812f961
+|\ \ \ \   parent:      7:699392d1259e
+| | | | |  parent:      8:d7aa38594334
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:09 1970 +0000
+| | | | |  summary:     (9) expand
+| | | | |
+| o-----+  changeset:   8:d7aa38594334
+| | | | |  parent:      0:7aa22e58e8c1
+|/ / / /   parent:      7:699392d1259e
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:08 1970 +0000
+| | | |    summary:     (8) merge two known; one immediate left, one far right
+| | | |
+o | | |    changeset:   7:699392d1259e
+|\ \ \ \   parent:      2:e8ea2256f9ec
+| | | | |  parent:      5:3589c3c477ab
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:07 1970 +0000
+| | | | |  summary:     (7) expand
+| | | | |
++---o | |  changeset:   6:0ca7c061cf45
+| |/ / /   parent:      2:e8ea2256f9ec
+| | | |    parent:      5:3589c3c477ab
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:06 1970 +0000
+| | | |    summary:     (6) merge two known; one immediate left, one far left
+| | | |
+| o | |    changeset:   5:3589c3c477ab
+| |\ \ \   parent:      3:02173ffbf857
+| | | | |  parent:      4:e2cad8233c77
+| | | | |  user:        test
+| | | | |  date:        Thu Jan 01 00:00:05 1970 +0000
+| | | | |  summary:     (5) expand
+| | | | |
+| | o | |  changeset:   4:e2cad8233c77
+| |/|/ /   parent:      1:3cae7826a707
+| | | |    parent:      3:02173ffbf857
+| | | |    user:        test
+| | | |    date:        Thu Jan 01 00:00:04 1970 +0000
+| | | |    summary:     (4) merge two known; one immediate left, one immediate right
+| | | |
+| o | |  changeset:   3:02173ffbf857
+|/ / /   user:        test
+| | |    date:        Thu Jan 01 00:00:03 1970 +0000
+| | |    summary:     (3) collapse
+| | |
+o | |  changeset:   2:e8ea2256f9ec
+|/ /   user:        test
+| |    date:        Thu Jan 01 00:00:02 1970 +0000
+| |    summary:     (2) collapse
+| |
+o |  changeset:   1:3cae7826a707
+|/   user:        test
+|    date:        Thu Jan 01 00:00:01 1970 +0000
+|    summary:     (1) collapse
+|
+o  changeset:   0:7aa22e58e8c1
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     (0) root
+