diff --git a/hgext/graphlog.py b/hgext/graphlog.py new file mode 100644 --- /dev/null +++ b/hgext/graphlog.py @@ -0,0 +1,266 @@ +# ASCII graph log extension for Mercurial +# +# Copyright 2007 Joel Rosdahl +# +# 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]"), +}