hgext/graphlog.py
changeset 4344 345ed833854d
child 4509 9d1380e5c8c5
equal deleted inserted replaced
4343:077aafddd35f 4344:345ed833854d
       
     1 # ASCII graph log extension for Mercurial
       
     2 #
       
     3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
       
     4 # 
       
     5 # This software may be used and distributed according to the terms of
       
     6 # the GNU General Public License, incorporated herein by reference.
       
     7 
       
     8 import sys
       
     9 from mercurial.cmdutil import revrange, show_changeset
       
    10 from mercurial.i18n import _
       
    11 from mercurial.node import nullid, nullrev
       
    12 from mercurial.util import Abort
       
    13 
       
    14 def revision_grapher(repo, start_rev, stop_rev):
       
    15     """incremental revision grapher
       
    16 
       
    17     This generator function walks through the revision history from
       
    18     revision start_rev to revision stop_rev (which must be less than
       
    19     or equal to start_rev) and for each revision emits tuples with the
       
    20     following elements:
       
    21 
       
    22       - Current revision.
       
    23       - Current node.
       
    24       - Column of the current node in the set of ongoing edges.
       
    25       - Edges; a list of (col, next_col) indicating the edges between
       
    26         the current node and its parents.
       
    27       - Number of columns (ongoing edges) in the current revision.
       
    28       - The difference between the number of columns (ongoing edges)
       
    29         in the next revision and the number of columns (ongoing edges)
       
    30         in the current revision. That is: -1 means one column removed;
       
    31         0 means no columns added or removed; 1 means one column added.
       
    32     """
       
    33 
       
    34     assert start_rev >= stop_rev
       
    35     curr_rev = start_rev
       
    36     revs = []
       
    37     while curr_rev >= stop_rev:
       
    38         node = repo.changelog.node(curr_rev)
       
    39 
       
    40         # Compute revs and next_revs.
       
    41         if curr_rev not in revs:
       
    42             # New head.
       
    43             revs.append(curr_rev)
       
    44         rev_index = revs.index(curr_rev)
       
    45         next_revs = revs[:]
       
    46 
       
    47         # Add parents to next_revs.
       
    48         parents = get_rev_parents(repo, curr_rev)
       
    49         parents_to_add = []
       
    50         for parent in parents:
       
    51             if parent not in next_revs:
       
    52                 parents_to_add.append(parent)
       
    53         parents_to_add.sort()
       
    54         next_revs[rev_index:rev_index + 1] = parents_to_add
       
    55 
       
    56         edges = []
       
    57         for parent in parents:
       
    58             edges.append((rev_index, next_revs.index(parent)))
       
    59 
       
    60         n_columns_diff = len(next_revs) - len(revs)
       
    61         yield (curr_rev, node, rev_index, edges, len(revs), n_columns_diff)
       
    62 
       
    63         revs = next_revs
       
    64         curr_rev -= 1
       
    65 
       
    66 def get_rev_parents(repo, rev):
       
    67     return [x for x in repo.changelog.parentrevs(rev) if x != nullrev]
       
    68 
       
    69 def fix_long_right_edges(edges):
       
    70     for (i, (start, end)) in enumerate(edges):
       
    71         if end > start:
       
    72             edges[i] = (start, end + 1)
       
    73 
       
    74 def draw_edges(edges, nodeline, interline):
       
    75     for (start, end) in edges:
       
    76         if start == end + 1:
       
    77             interline[2 * end + 1] = "/"
       
    78         elif start == end - 1:
       
    79             interline[2 * start + 1] = "\\"
       
    80         elif start == end:
       
    81             interline[2 * start] = "|"
       
    82         else:
       
    83             nodeline[2 * end] = "+"
       
    84             if start > end:
       
    85                 (start, end) = (end,start)
       
    86             for i in range(2 * start + 1, 2 * end):
       
    87                 if nodeline[i] != "+":
       
    88                     nodeline[i] = "-"
       
    89 
       
    90 def format_line(line, level, logstr):
       
    91     text = "%-*s %s" % (2 * level, "".join(line), logstr)
       
    92     return "%s\n" % text.rstrip()
       
    93 
       
    94 def get_nodeline_edges_tail(
       
    95         node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
       
    96     if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
       
    97         # Still going in the same non-vertical direction.
       
    98         if n_columns_diff == -1:
       
    99             start = max(node_index + 1, p_node_index)
       
   100             tail = ["|", " "] * (start - node_index - 1)
       
   101             tail.extend(["/", " "] * (n_columns - start))
       
   102             return tail
       
   103         else:
       
   104             return ["\\", " "] * (n_columns - node_index - 1)
       
   105     else:
       
   106         return ["|", " "] * (n_columns - node_index - 1)
       
   107 
       
   108 def get_padding_line(ni, n_columns, edges):
       
   109     line = []
       
   110     line.extend(["|", " "] * ni)
       
   111     if (ni, ni - 1) in edges or (ni, ni) in edges:
       
   112         # (ni, ni - 1)      (ni, ni)
       
   113         # | | | |           | | | |
       
   114         # +---o |           | o---+
       
   115         # | | c |           | c | |
       
   116         # | |/ /            | |/ /
       
   117         # | | |             | | |
       
   118         c = "|"
       
   119     else:
       
   120         c = " "
       
   121     line.extend([c, " "])
       
   122     line.extend(["|", " "] * (n_columns - ni - 1))
       
   123     return line
       
   124 
       
   125 def get_limit(limit_opt):
       
   126     if limit_opt:
       
   127         try:
       
   128             limit = int(limit_opt)
       
   129         except ValueError:
       
   130             raise Abort(_("limit must be a positive integer"))
       
   131         if limit <= 0:
       
   132             raise Abort(_("limit must be positive"))
       
   133     else:
       
   134         limit = sys.maxint
       
   135     return limit
       
   136 
       
   137 def get_revs(repo, rev_opt):
       
   138     if rev_opt:
       
   139         revs = revrange(repo, rev_opt)
       
   140         return (max(revs), min(revs))
       
   141     else:
       
   142         return (repo.changelog.count() - 1, 0)
       
   143 
       
   144 def graphlog(ui, repo, *args, **opts):
       
   145     """show revision history alongside an ASCII revision graph
       
   146 
       
   147     Print a revision history alongside a revision graph drawn with
       
   148     ASCII characters.
       
   149 
       
   150     Nodes printed as an @ character are parents of the working
       
   151     directory.
       
   152     """
       
   153 
       
   154     limit = get_limit(opts["limit"])
       
   155     (start_rev, stop_rev) = get_revs(repo, opts["rev"])
       
   156     stop_rev = max(stop_rev, start_rev - limit + 1)
       
   157     if start_rev == nullrev:
       
   158         return
       
   159     cs_printer = show_changeset(ui, repo, opts)
       
   160     grapher = revision_grapher(repo, start_rev, stop_rev)
       
   161     repo_parents = repo.dirstate.parents()
       
   162     prev_n_columns_diff = 0
       
   163     prev_node_index = 0
       
   164 
       
   165     for (rev, node, node_index, edges, n_columns, n_columns_diff) in grapher:
       
   166         # log_strings is the list of all log strings to draw alongside
       
   167         # the graph.
       
   168         ui.pushbuffer()
       
   169         cs_printer.show(rev, node)
       
   170         log_strings = ui.popbuffer().split("\n")[:-1]
       
   171 
       
   172         if n_columns_diff == -1:
       
   173             # Transform
       
   174             #
       
   175             #     | | |        | | |
       
   176             #     o | |  into  o---+
       
   177             #     |X /         |/ /
       
   178             #     | |          | |
       
   179             fix_long_right_edges(edges)
       
   180 
       
   181         # add_padding_line says whether to rewrite
       
   182         #
       
   183         #     | | | |        | | | |
       
   184         #     | o---+  into  | o---+
       
   185         #     |  / /         |   | |  # <--- padding line
       
   186         #     o | |          |  / /
       
   187         #                    o | |
       
   188         add_padding_line = \
       
   189             len(log_strings) > 2 and \
       
   190             n_columns_diff == -1 and \
       
   191             [x for (x, y) in edges if x + 1 < y]
       
   192 
       
   193         # fix_nodeline_tail says whether to rewrite
       
   194         #
       
   195         #     | | o | |        | | o | |
       
   196         #     | | |/ /         | | |/ /
       
   197         #     | o | |    into  | o / /   # <--- fixed nodeline tail
       
   198         #     | |/ /           | |/ /
       
   199         #     o | |            o | |
       
   200         fix_nodeline_tail = len(log_strings) <= 2 and not add_padding_line
       
   201 
       
   202         # nodeline is the line containing the node character (@ or o).
       
   203         nodeline = ["|", " "] * node_index
       
   204         if node in repo_parents:
       
   205             node_ch = "@"
       
   206         else:
       
   207             node_ch = "o"
       
   208         nodeline.extend([node_ch, " "])
       
   209 
       
   210         nodeline.extend(
       
   211             get_nodeline_edges_tail(
       
   212                 node_index, prev_node_index, n_columns, n_columns_diff,
       
   213                 prev_n_columns_diff, fix_nodeline_tail))
       
   214 
       
   215         # shift_interline is the line containing the non-vertical
       
   216         # edges between this entry and the next.
       
   217         shift_interline = ["|", " "] * node_index
       
   218         if n_columns_diff == -1:
       
   219             n_spaces = 1
       
   220             edge_ch = "/"
       
   221         elif n_columns_diff == 0:
       
   222             n_spaces = 2
       
   223             edge_ch = "|"
       
   224         else:
       
   225             n_spaces = 3
       
   226             edge_ch = "\\"
       
   227         shift_interline.extend(n_spaces * [" "])
       
   228         shift_interline.extend([edge_ch, " "] * (n_columns - node_index - 1))
       
   229 
       
   230         # Draw edges from the current node to its parents.
       
   231         draw_edges(edges, nodeline, shift_interline)
       
   232 
       
   233         # lines is the list of all graph lines to print.
       
   234         lines = [nodeline]
       
   235         if add_padding_line:
       
   236             lines.append(get_padding_line(node_index, n_columns, edges))
       
   237         lines.append(shift_interline)
       
   238 
       
   239         # Make sure that there are as many graph lines as there are
       
   240         # log strings.
       
   241         while len(log_strings) < len(lines):
       
   242             log_strings.append("")
       
   243         if len(lines) < len(log_strings):
       
   244             extra_interline = ["|", " "] * (n_columns + n_columns_diff)
       
   245             while len(lines) < len(log_strings):
       
   246                 lines.append(extra_interline)
       
   247 
       
   248         # Print lines.
       
   249         indentation_level = max(n_columns, n_columns + n_columns_diff)
       
   250         for (line, logstr) in zip(lines, log_strings):
       
   251             ui.write(format_line(line, indentation_level, logstr))
       
   252 
       
   253         # ...and start over.
       
   254         prev_node_index = node_index
       
   255         prev_n_columns_diff = n_columns_diff
       
   256 
       
   257 cmdtable = {
       
   258     "glog":
       
   259     (graphlog,
       
   260      [("l", "limit", "", _("limit number of changes displayed")),
       
   261       ("p", "patch", False, _("show patch")),
       
   262       ("r", "rev", [], _("show the specified revision or range")),
       
   263       ("", "style", "", _("display using template map file")),
       
   264       ("", "template", "", _("display with template"))],
       
   265      "hg glog [OPTIONS]"),
       
   266 }