hgext/graphlog.py
changeset 17179 0849d725e2f9
parent 17164 8299a9ad48dd
child 17180 ae0629161090
equal deleted inserted replaced
17178:8308f6284640 17179:0849d725e2f9
    18 from mercurial import hg, util, graphmod, templatekw, revset
    18 from mercurial import hg, util, graphmod, templatekw, revset
    19 
    19 
    20 cmdtable = {}
    20 cmdtable = {}
    21 command = cmdutil.command(cmdtable)
    21 command = cmdutil.command(cmdtable)
    22 testedwith = 'internal'
    22 testedwith = 'internal'
    23 
       
    24 def asciiedges(type, char, lines, seen, rev, parents):
       
    25     """adds edge info to changelog DAG walk suitable for ascii()"""
       
    26     if rev not in seen:
       
    27         seen.append(rev)
       
    28     nodeidx = seen.index(rev)
       
    29 
       
    30     knownparents = []
       
    31     newparents = []
       
    32     for parent in parents:
       
    33         if parent in seen:
       
    34             knownparents.append(parent)
       
    35         else:
       
    36             newparents.append(parent)
       
    37 
       
    38     ncols = len(seen)
       
    39     nextseen = seen[:]
       
    40     nextseen[nodeidx:nodeidx + 1] = newparents
       
    41     edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
       
    42 
       
    43     while len(newparents) > 2:
       
    44         # ascii() only knows how to add or remove a single column between two
       
    45         # calls. Nodes with more than two parents break this constraint so we
       
    46         # introduce intermediate expansion lines to grow the active node list
       
    47         # slowly.
       
    48         edges.append((nodeidx, nodeidx))
       
    49         edges.append((nodeidx, nodeidx + 1))
       
    50         nmorecols = 1
       
    51         yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
       
    52         char = '\\'
       
    53         lines = []
       
    54         nodeidx += 1
       
    55         ncols += 1
       
    56         edges = []
       
    57         del newparents[0]
       
    58 
       
    59     if len(newparents) > 0:
       
    60         edges.append((nodeidx, nodeidx))
       
    61     if len(newparents) > 1:
       
    62         edges.append((nodeidx, nodeidx + 1))
       
    63     nmorecols = len(nextseen) - ncols
       
    64     seen[:] = nextseen
       
    65     yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
       
    66 
       
    67 def _fixlongrightedges(edges):
       
    68     for (i, (start, end)) in enumerate(edges):
       
    69         if end > start:
       
    70             edges[i] = (start, end + 1)
       
    71 
       
    72 def _getnodelineedgestail(
       
    73         node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
       
    74     if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
       
    75         # Still going in the same non-vertical direction.
       
    76         if n_columns_diff == -1:
       
    77             start = max(node_index + 1, p_node_index)
       
    78             tail = ["|", " "] * (start - node_index - 1)
       
    79             tail.extend(["/", " "] * (n_columns - start))
       
    80             return tail
       
    81         else:
       
    82             return ["\\", " "] * (n_columns - node_index - 1)
       
    83     else:
       
    84         return ["|", " "] * (n_columns - node_index - 1)
       
    85 
       
    86 def _drawedges(edges, nodeline, interline):
       
    87     for (start, end) in edges:
       
    88         if start == end + 1:
       
    89             interline[2 * end + 1] = "/"
       
    90         elif start == end - 1:
       
    91             interline[2 * start + 1] = "\\"
       
    92         elif start == end:
       
    93             interline[2 * start] = "|"
       
    94         else:
       
    95             if 2 * end >= len(nodeline):
       
    96                 continue
       
    97             nodeline[2 * end] = "+"
       
    98             if start > end:
       
    99                 (start, end) = (end, start)
       
   100             for i in range(2 * start + 1, 2 * end):
       
   101                 if nodeline[i] != "+":
       
   102                     nodeline[i] = "-"
       
   103 
       
   104 def _getpaddingline(ni, n_columns, edges):
       
   105     line = []
       
   106     line.extend(["|", " "] * ni)
       
   107     if (ni, ni - 1) in edges or (ni, ni) in edges:
       
   108         # (ni, ni - 1)      (ni, ni)
       
   109         # | | | |           | | | |
       
   110         # +---o |           | o---+
       
   111         # | | c |           | c | |
       
   112         # | |/ /            | |/ /
       
   113         # | | |             | | |
       
   114         c = "|"
       
   115     else:
       
   116         c = " "
       
   117     line.extend([c, " "])
       
   118     line.extend(["|", " "] * (n_columns - ni - 1))
       
   119     return line
       
   120 
       
   121 def asciistate():
       
   122     """returns the initial value for the "state" argument to ascii()"""
       
   123     return [0, 0]
       
   124 
       
   125 def ascii(ui, state, type, char, text, coldata):
       
   126     """prints an ASCII graph of the DAG
       
   127 
       
   128     takes the following arguments (one call per node in the graph):
       
   129 
       
   130       - ui to write to
       
   131       - Somewhere to keep the needed state in (init to asciistate())
       
   132       - Column of the current node in the set of ongoing edges.
       
   133       - Type indicator of node data, usually 'C' for changesets.
       
   134       - Payload: (char, lines):
       
   135         - Character to use as node's symbol.
       
   136         - List of lines to display as the node's text.
       
   137       - Edges; a list of (col, next_col) indicating the edges between
       
   138         the current node and its parents.
       
   139       - Number of columns (ongoing edges) in the current revision.
       
   140       - The difference between the number of columns (ongoing edges)
       
   141         in the next revision and the number of columns (ongoing edges)
       
   142         in the current revision. That is: -1 means one column removed;
       
   143         0 means no columns added or removed; 1 means one column added.
       
   144     """
       
   145 
       
   146     idx, edges, ncols, coldiff = coldata
       
   147     assert -2 < coldiff < 2
       
   148     if coldiff == -1:
       
   149         # Transform
       
   150         #
       
   151         #     | | |        | | |
       
   152         #     o | |  into  o---+
       
   153         #     |X /         |/ /
       
   154         #     | |          | |
       
   155         _fixlongrightedges(edges)
       
   156 
       
   157     # add_padding_line says whether to rewrite
       
   158     #
       
   159     #     | | | |        | | | |
       
   160     #     | o---+  into  | o---+
       
   161     #     |  / /         |   | |  # <--- padding line
       
   162     #     o | |          |  / /
       
   163     #                    o | |
       
   164     add_padding_line = (len(text) > 2 and coldiff == -1 and
       
   165                         [x for (x, y) in edges if x + 1 < y])
       
   166 
       
   167     # fix_nodeline_tail says whether to rewrite
       
   168     #
       
   169     #     | | o | |        | | o | |
       
   170     #     | | |/ /         | | |/ /
       
   171     #     | o | |    into  | o / /   # <--- fixed nodeline tail
       
   172     #     | |/ /           | |/ /
       
   173     #     o | |            o | |
       
   174     fix_nodeline_tail = len(text) <= 2 and not add_padding_line
       
   175 
       
   176     # nodeline is the line containing the node character (typically o)
       
   177     nodeline = ["|", " "] * idx
       
   178     nodeline.extend([char, " "])
       
   179 
       
   180     nodeline.extend(
       
   181         _getnodelineedgestail(idx, state[1], ncols, coldiff,
       
   182                               state[0], fix_nodeline_tail))
       
   183 
       
   184     # shift_interline is the line containing the non-vertical
       
   185     # edges between this entry and the next
       
   186     shift_interline = ["|", " "] * idx
       
   187     if coldiff == -1:
       
   188         n_spaces = 1
       
   189         edge_ch = "/"
       
   190     elif coldiff == 0:
       
   191         n_spaces = 2
       
   192         edge_ch = "|"
       
   193     else:
       
   194         n_spaces = 3
       
   195         edge_ch = "\\"
       
   196     shift_interline.extend(n_spaces * [" "])
       
   197     shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
       
   198 
       
   199     # draw edges from the current node to its parents
       
   200     _drawedges(edges, nodeline, shift_interline)
       
   201 
       
   202     # lines is the list of all graph lines to print
       
   203     lines = [nodeline]
       
   204     if add_padding_line:
       
   205         lines.append(_getpaddingline(idx, ncols, edges))
       
   206     lines.append(shift_interline)
       
   207 
       
   208     # make sure that there are as many graph lines as there are
       
   209     # log strings
       
   210     while len(text) < len(lines):
       
   211         text.append("")
       
   212     if len(lines) < len(text):
       
   213         extra_interline = ["|", " "] * (ncols + coldiff)
       
   214         while len(lines) < len(text):
       
   215             lines.append(extra_interline)
       
   216 
       
   217     # print lines
       
   218     indentation_level = max(ncols, ncols + coldiff)
       
   219     for (line, logstr) in zip(lines, text):
       
   220         ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
       
   221         ui.write(ln.rstrip() + '\n')
       
   222 
       
   223     # ... and start over
       
   224     state[0] = coldiff
       
   225     state[1] = idx
       
   226 
    23 
   227 def _checkunsupportedflags(pats, opts):
    24 def _checkunsupportedflags(pats, opts):
   228     for op in ["newest_first"]:
    25     for op in ["newest_first"]:
   229         if op in opts and opts[op]:
    26         if op in opts and opts[op]:
   230             raise util.Abort(_("-G/--graph option is incompatible with --%s")
    27             raise util.Abort(_("-G/--graph option is incompatible with --%s")
   439         revs = iter(revs)
   236         revs = iter(revs)
   440     return revs, expr, filematcher
   237     return revs, expr, filematcher
   441 
   238 
   442 def generate(ui, dag, displayer, showparents, edgefn, getrenamed=None,
   239 def generate(ui, dag, displayer, showparents, edgefn, getrenamed=None,
   443              filematcher=None):
   240              filematcher=None):
   444     seen, state = [], asciistate()
   241     seen, state = [], graphmod.asciistate()
   445     for rev, type, ctx, parents in dag:
   242     for rev, type, ctx, parents in dag:
   446         char = 'o'
   243         char = 'o'
   447         if ctx.node() in showparents:
   244         if ctx.node() in showparents:
   448             char = '@'
   245             char = '@'
   449         elif ctx.obsolete():
   246         elif ctx.obsolete():
   463         if not lines[-1]:
   260         if not lines[-1]:
   464             del lines[-1]
   261             del lines[-1]
   465         displayer.flush(rev)
   262         displayer.flush(rev)
   466         edges = edgefn(type, char, lines, seen, rev, parents)
   263         edges = edgefn(type, char, lines, seen, rev, parents)
   467         for type, char, lines, coldata in edges:
   264         for type, char, lines, coldata in edges:
   468             ascii(ui, state, type, char, lines, coldata)
   265             graphmod.ascii(ui, state, type, char, lines, coldata)
   469     displayer.close()
   266     displayer.close()
   470 
   267 
   471 @command('glog',
   268 @command('glog',
   472     [('f', 'follow', None,
   269     [('f', 'follow', None,
   473      _('follow changeset history, or file history across copies and renames')),
   270      _('follow changeset history, or file history across copies and renames')),
   514         if opts.get('rev'):
   311         if opts.get('rev'):
   515             endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
   312             endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
   516         getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
   313         getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
   517     displayer = show_changeset(ui, repo, opts, buffered=True)
   314     displayer = show_changeset(ui, repo, opts, buffered=True)
   518     showparents = [ctx.node() for ctx in repo[None].parents()]
   315     showparents = [ctx.node() for ctx in repo[None].parents()]
   519     generate(ui, revdag, displayer, showparents, asciiedges, getrenamed,
   316     generate(ui, revdag, displayer, showparents, graphmod.asciiedges,
   520              filematcher)
   317              getrenamed, filematcher)
   521 
   318 
   522 def graphrevs(repo, nodes, opts):
   319 def graphrevs(repo, nodes, opts):
   523     limit = cmdutil.loglimit(opts)
   320     limit = cmdutil.loglimit(opts)
   524     nodes.reverse()
   321     nodes.reverse()
   525     if limit is not None:
   322     if limit is not None:
   542         return
   339         return
   543 
   340 
   544     revdag = graphrevs(repo, o, opts)
   341     revdag = graphrevs(repo, o, opts)
   545     displayer = show_changeset(ui, repo, opts, buffered=True)
   342     displayer = show_changeset(ui, repo, opts, buffered=True)
   546     showparents = [ctx.node() for ctx in repo[None].parents()]
   343     showparents = [ctx.node() for ctx in repo[None].parents()]
   547     generate(ui, revdag, displayer, showparents, asciiedges)
   344     generate(ui, revdag, displayer, showparents, graphmod.asciiedges)
   548 
   345 
   549 def gincoming(ui, repo, source="default", **opts):
   346 def gincoming(ui, repo, source="default", **opts):
   550     """show the incoming changesets alongside an ASCII revision graph
   347     """show the incoming changesets alongside an ASCII revision graph
   551 
   348 
   552     Print the incoming changesets alongside a revision graph drawn with
   349     Print the incoming changesets alongside a revision graph drawn with
   560 
   357 
   561     _checkunsupportedflags([], opts)
   358     _checkunsupportedflags([], opts)
   562     def display(other, chlist, displayer):
   359     def display(other, chlist, displayer):
   563         revdag = graphrevs(other, chlist, opts)
   360         revdag = graphrevs(other, chlist, opts)
   564         showparents = [ctx.node() for ctx in repo[None].parents()]
   361         showparents = [ctx.node() for ctx in repo[None].parents()]
   565         generate(ui, revdag, displayer, showparents, asciiedges)
   362         generate(ui, revdag, displayer, showparents, graphmod.asciiedges)
   566 
   363 
   567     hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
   364     hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
   568 
   365 
   569 def uisetup(ui):
   366 def uisetup(ui):
   570     '''Initialize the extension.'''
   367     '''Initialize the extension.'''