eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/graphlog.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     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 the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 '''command to view revision graphs from a shell
       
     9 
       
    10 This extension adds a --graph option to the incoming, outgoing and log
       
    11 commands. When this options is given, an ASCII representation of the
       
    12 revision graph is also shown.
       
    13 '''
       
    14 
       
    15 import os
       
    16 from mercurial.cmdutil import revrange, show_changeset
       
    17 from mercurial.commands import templateopts
       
    18 from mercurial.i18n import _
       
    19 from mercurial.node import nullrev
       
    20 from mercurial import cmdutil, commands, extensions
       
    21 from mercurial import hg, util, graphmod
       
    22 
       
    23 ASCIIDATA = 'ASC'
       
    24 
       
    25 def asciiedges(seen, rev, parents):
       
    26     """adds edge info to changelog DAG walk suitable for ascii()"""
       
    27     if rev not in seen:
       
    28         seen.append(rev)
       
    29     nodeidx = seen.index(rev)
       
    30 
       
    31     knownparents = []
       
    32     newparents = []
       
    33     for parent in parents:
       
    34         if parent in seen:
       
    35             knownparents.append(parent)
       
    36         else:
       
    37             newparents.append(parent)
       
    38 
       
    39     ncols = len(seen)
       
    40     seen[nodeidx:nodeidx + 1] = newparents
       
    41     edges = [(nodeidx, seen.index(p)) for p in knownparents]
       
    42 
       
    43     if len(newparents) > 0:
       
    44         edges.append((nodeidx, nodeidx))
       
    45     if len(newparents) > 1:
       
    46         edges.append((nodeidx, nodeidx + 1))
       
    47 
       
    48     nmorecols = len(seen) - ncols
       
    49     return nodeidx, edges, ncols, nmorecols
       
    50 
       
    51 def fix_long_right_edges(edges):
       
    52     for (i, (start, end)) in enumerate(edges):
       
    53         if end > start:
       
    54             edges[i] = (start, end + 1)
       
    55 
       
    56 def get_nodeline_edges_tail(
       
    57         node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
       
    58     if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
       
    59         # Still going in the same non-vertical direction.
       
    60         if n_columns_diff == -1:
       
    61             start = max(node_index + 1, p_node_index)
       
    62             tail = ["|", " "] * (start - node_index - 1)
       
    63             tail.extend(["/", " "] * (n_columns - start))
       
    64             return tail
       
    65         else:
       
    66             return ["\\", " "] * (n_columns - node_index - 1)
       
    67     else:
       
    68         return ["|", " "] * (n_columns - node_index - 1)
       
    69 
       
    70 def draw_edges(edges, nodeline, interline):
       
    71     for (start, end) in edges:
       
    72         if start == end + 1:
       
    73             interline[2 * end + 1] = "/"
       
    74         elif start == end - 1:
       
    75             interline[2 * start + 1] = "\\"
       
    76         elif start == end:
       
    77             interline[2 * start] = "|"
       
    78         else:
       
    79             nodeline[2 * end] = "+"
       
    80             if start > end:
       
    81                 (start, end) = (end, start)
       
    82             for i in range(2 * start + 1, 2 * end):
       
    83                 if nodeline[i] != "+":
       
    84                     nodeline[i] = "-"
       
    85 
       
    86 def get_padding_line(ni, n_columns, edges):
       
    87     line = []
       
    88     line.extend(["|", " "] * ni)
       
    89     if (ni, ni - 1) in edges or (ni, ni) in edges:
       
    90         # (ni, ni - 1)      (ni, ni)
       
    91         # | | | |           | | | |
       
    92         # +---o |           | o---+
       
    93         # | | c |           | c | |
       
    94         # | |/ /            | |/ /
       
    95         # | | |             | | |
       
    96         c = "|"
       
    97     else:
       
    98         c = " "
       
    99     line.extend([c, " "])
       
   100     line.extend(["|", " "] * (n_columns - ni - 1))
       
   101     return line
       
   102 
       
   103 def asciistate():
       
   104     """returns the initial value for the "state" argument to ascii()"""
       
   105     return [0, 0]
       
   106 
       
   107 def ascii(ui, state, type, char, text, coldata):
       
   108     """prints an ASCII graph of the DAG
       
   109 
       
   110     takes the following arguments (one call per node in the graph):
       
   111 
       
   112       - ui to write to
       
   113       - Somewhere to keep the needed state in (init to asciistate())
       
   114       - Column of the current node in the set of ongoing edges.
       
   115       - Type indicator of node data == ASCIIDATA.
       
   116       - Payload: (char, lines):
       
   117         - Character to use as node's symbol.
       
   118         - List of lines to display as the node's text.
       
   119       - Edges; a list of (col, next_col) indicating the edges between
       
   120         the current node and its parents.
       
   121       - Number of columns (ongoing edges) in the current revision.
       
   122       - The difference between the number of columns (ongoing edges)
       
   123         in the next revision and the number of columns (ongoing edges)
       
   124         in the current revision. That is: -1 means one column removed;
       
   125         0 means no columns added or removed; 1 means one column added.
       
   126     """
       
   127 
       
   128     idx, edges, ncols, coldiff = coldata
       
   129     assert -2 < coldiff < 2
       
   130     if coldiff == -1:
       
   131         # Transform
       
   132         #
       
   133         #     | | |        | | |
       
   134         #     o | |  into  o---+
       
   135         #     |X /         |/ /
       
   136         #     | |          | |
       
   137         fix_long_right_edges(edges)
       
   138 
       
   139     # add_padding_line says whether to rewrite
       
   140     #
       
   141     #     | | | |        | | | |
       
   142     #     | o---+  into  | o---+
       
   143     #     |  / /         |   | |  # <--- padding line
       
   144     #     o | |          |  / /
       
   145     #                    o | |
       
   146     add_padding_line = (len(text) > 2 and coldiff == -1 and
       
   147                         [x for (x, y) in edges if x + 1 < y])
       
   148 
       
   149     # fix_nodeline_tail says whether to rewrite
       
   150     #
       
   151     #     | | o | |        | | o | |
       
   152     #     | | |/ /         | | |/ /
       
   153     #     | o | |    into  | o / /   # <--- fixed nodeline tail
       
   154     #     | |/ /           | |/ /
       
   155     #     o | |            o | |
       
   156     fix_nodeline_tail = len(text) <= 2 and not add_padding_line
       
   157 
       
   158     # nodeline is the line containing the node character (typically o)
       
   159     nodeline = ["|", " "] * idx
       
   160     nodeline.extend([char, " "])
       
   161 
       
   162     nodeline.extend(
       
   163         get_nodeline_edges_tail(idx, state[1], ncols, coldiff,
       
   164                                 state[0], fix_nodeline_tail))
       
   165 
       
   166     # shift_interline is the line containing the non-vertical
       
   167     # edges between this entry and the next
       
   168     shift_interline = ["|", " "] * idx
       
   169     if coldiff == -1:
       
   170         n_spaces = 1
       
   171         edge_ch = "/"
       
   172     elif coldiff == 0:
       
   173         n_spaces = 2
       
   174         edge_ch = "|"
       
   175     else:
       
   176         n_spaces = 3
       
   177         edge_ch = "\\"
       
   178     shift_interline.extend(n_spaces * [" "])
       
   179     shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
       
   180 
       
   181     # draw edges from the current node to its parents
       
   182     draw_edges(edges, nodeline, shift_interline)
       
   183 
       
   184     # lines is the list of all graph lines to print
       
   185     lines = [nodeline]
       
   186     if add_padding_line:
       
   187         lines.append(get_padding_line(idx, ncols, edges))
       
   188     lines.append(shift_interline)
       
   189 
       
   190     # make sure that there are as many graph lines as there are
       
   191     # log strings
       
   192     while len(text) < len(lines):
       
   193         text.append("")
       
   194     if len(lines) < len(text):
       
   195         extra_interline = ["|", " "] * (ncols + coldiff)
       
   196         while len(lines) < len(text):
       
   197             lines.append(extra_interline)
       
   198 
       
   199     # print lines
       
   200     indentation_level = max(ncols, ncols + coldiff)
       
   201     for (line, logstr) in zip(lines, text):
       
   202         ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
       
   203         ui.write(ln.rstrip() + '\n')
       
   204 
       
   205     # ... and start over
       
   206     state[0] = coldiff
       
   207     state[1] = idx
       
   208 
       
   209 def get_revs(repo, rev_opt):
       
   210     if rev_opt:
       
   211         revs = revrange(repo, rev_opt)
       
   212         if len(revs) == 0:
       
   213             return (nullrev, nullrev)
       
   214         return (max(revs), min(revs))
       
   215     else:
       
   216         return (len(repo) - 1, 0)
       
   217 
       
   218 def check_unsupported_flags(opts):
       
   219     for op in ["follow", "follow_first", "date", "copies", "keyword", "remove",
       
   220                "only_merges", "user", "branch", "only_branch", "prune",
       
   221                "newest_first", "no_merges", "include", "exclude"]:
       
   222         if op in opts and opts[op]:
       
   223             raise util.Abort(_("--graph option is incompatible with --%s")
       
   224                              % op.replace("_", "-"))
       
   225 
       
   226 def generate(ui, dag, displayer, showparents, edgefn):
       
   227     seen, state = [], asciistate()
       
   228     for rev, type, ctx, parents in dag:
       
   229         char = ctx.node() in showparents and '@' or 'o'
       
   230         displayer.show(ctx)
       
   231         lines = displayer.hunk.pop(rev).split('\n')[:-1]
       
   232         displayer.flush(rev)
       
   233         ascii(ui, state, type, char, lines, edgefn(seen, rev, parents))
       
   234     displayer.close()
       
   235 
       
   236 def graphlog(ui, repo, path=None, **opts):
       
   237     """show revision history alongside an ASCII revision graph
       
   238 
       
   239     Print a revision history alongside a revision graph drawn with
       
   240     ASCII characters.
       
   241 
       
   242     Nodes printed as an @ character are parents of the working
       
   243     directory.
       
   244     """
       
   245 
       
   246     check_unsupported_flags(opts)
       
   247     limit = cmdutil.loglimit(opts)
       
   248     start, stop = get_revs(repo, opts["rev"])
       
   249     if start == nullrev:
       
   250         return
       
   251 
       
   252     if path:
       
   253         path = util.canonpath(repo.root, os.getcwd(), path)
       
   254     if path: # could be reset in canonpath
       
   255         revdag = graphmod.filerevs(repo, path, start, stop, limit)
       
   256     else:
       
   257         if limit is not None:
       
   258             stop = max(stop, start - limit + 1)
       
   259         revdag = graphmod.revisions(repo, start, stop)
       
   260 
       
   261     displayer = show_changeset(ui, repo, opts, buffered=True)
       
   262     showparents = [ctx.node() for ctx in repo[None].parents()]
       
   263     generate(ui, revdag, displayer, showparents, asciiedges)
       
   264 
       
   265 def graphrevs(repo, nodes, opts):
       
   266     limit = cmdutil.loglimit(opts)
       
   267     nodes.reverse()
       
   268     if limit is not None:
       
   269         nodes = nodes[:limit]
       
   270     return graphmod.nodes(repo, nodes)
       
   271 
       
   272 def goutgoing(ui, repo, dest=None, **opts):
       
   273     """show the outgoing changesets alongside an ASCII revision graph
       
   274 
       
   275     Print the outgoing changesets alongside a revision graph drawn with
       
   276     ASCII characters.
       
   277 
       
   278     Nodes printed as an @ character are parents of the working
       
   279     directory.
       
   280     """
       
   281 
       
   282     check_unsupported_flags(opts)
       
   283     o = hg._outgoing(ui, repo, dest, opts)
       
   284     if o is None:
       
   285         return
       
   286 
       
   287     revdag = graphrevs(repo, o, opts)
       
   288     displayer = show_changeset(ui, repo, opts, buffered=True)
       
   289     showparents = [ctx.node() for ctx in repo[None].parents()]
       
   290     generate(ui, revdag, displayer, showparents, asciiedges)
       
   291 
       
   292 def gincoming(ui, repo, source="default", **opts):
       
   293     """show the incoming changesets alongside an ASCII revision graph
       
   294 
       
   295     Print the incoming changesets alongside a revision graph drawn with
       
   296     ASCII characters.
       
   297 
       
   298     Nodes printed as an @ character are parents of the working
       
   299     directory.
       
   300     """
       
   301     def subreporecurse():
       
   302         return 1
       
   303 
       
   304     check_unsupported_flags(opts)
       
   305     def display(other, chlist, displayer):
       
   306         revdag = graphrevs(other, chlist, opts)
       
   307         showparents = [ctx.node() for ctx in repo[None].parents()]
       
   308         generate(ui, revdag, displayer, showparents, asciiedges)
       
   309 
       
   310     hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
       
   311 
       
   312 def uisetup(ui):
       
   313     '''Initialize the extension.'''
       
   314     _wrapcmd(ui, 'log', commands.table, graphlog)
       
   315     _wrapcmd(ui, 'incoming', commands.table, gincoming)
       
   316     _wrapcmd(ui, 'outgoing', commands.table, goutgoing)
       
   317 
       
   318 def _wrapcmd(ui, cmd, table, wrapfn):
       
   319     '''wrap the command'''
       
   320     def graph(orig, *args, **kwargs):
       
   321         if kwargs['graph']:
       
   322             return wrapfn(*args, **kwargs)
       
   323         return orig(*args, **kwargs)
       
   324     entry = extensions.wrapcommand(table, cmd, graph)
       
   325     entry[1].append(('G', 'graph', None, _("show the revision DAG")))
       
   326 
       
   327 cmdtable = {
       
   328     "glog":
       
   329         (graphlog,
       
   330          [('l', 'limit', '',
       
   331            _('limit number of changes displayed'), _('NUM')),
       
   332           ('p', 'patch', False, _('show patch')),
       
   333           ('r', 'rev', [],
       
   334            _('show the specified revision or range'), _('REV')),
       
   335          ] + templateopts,
       
   336          _('hg glog [OPTION]... [FILE]')),
       
   337 }