graphlog.py
359 lines
| 11.7 KiB
| text/x-python
|
PythonLexer
/ hgext / graphlog.py
Joel Rosdahl
|
r4344 | # ASCII graph log extension for Mercurial | ||
# | ||||
# Copyright 2007 Joel Rosdahl <joel@rosdahl.net> | ||||
Thomas Arendsen Hein
|
r4516 | # | ||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Martin Geisler
|
r8228 | |||
Dirkjan Ochtman
|
r8934 | '''command to view revision graphs from a shell | ||
Alpar Juttner
|
r7426 | |||
This extension adds a --graph option to the incoming, outgoing and log | ||||
Martin Geisler
|
r9259 | commands. When this options is given, an ASCII representation of the | ||
revision graph is also shown. | ||||
Alpar Juttner
|
r7426 | ''' | ||
Joel Rosdahl
|
r4344 | |||
from mercurial.cmdutil import revrange, show_changeset | ||||
Peter Arrenbrecht
|
r7873 | from mercurial.commands import templateopts | ||
Joel Rosdahl
|
r4344 | from mercurial.i18n import _ | ||
Joel Rosdahl
|
r6212 | from mercurial.node import nullrev | ||
Nicolas Dumazet
|
r12730 | from mercurial import cmdutil, commands, extensions | ||
Adrian Buehlmann
|
r13971 | from mercurial import hg, scmutil, util, graphmod | ||
Steve Borho
|
r5938 | |||
Peter Arrenbrecht
|
r8840 | ASCIIDATA = 'ASC' | ||
Dirkjan Ochtman
|
r9369 | def asciiedges(seen, rev, parents): | ||
Peter Arrenbrecht
|
r8840 | """adds edge info to changelog DAG walk suitable for ascii()""" | ||
Dirkjan Ochtman
|
r9369 | if rev not in seen: | ||
seen.append(rev) | ||||
nodeidx = seen.index(rev) | ||||
Steve Borho
|
r5938 | |||
Dirkjan Ochtman
|
r9369 | knownparents = [] | ||
newparents = [] | ||||
for parent in parents: | ||||
if parent in seen: | ||||
knownparents.append(parent) | ||||
else: | ||||
newparents.append(parent) | ||||
Steve Borho
|
r5938 | |||
Dirkjan Ochtman
|
r9369 | ncols = len(seen) | ||
seen[nodeidx:nodeidx + 1] = newparents | ||||
edges = [(nodeidx, seen.index(p)) for p in knownparents] | ||||
Peter Arrenbrecht
|
r7370 | |||
Dirkjan Ochtman
|
r9369 | if len(newparents) > 0: | ||
edges.append((nodeidx, nodeidx)) | ||||
if len(newparents) > 1: | ||||
edges.append((nodeidx, nodeidx + 1)) | ||||
nmorecols = len(seen) - ncols | ||||
return nodeidx, edges, ncols, nmorecols | ||||
Steve Borho
|
r5938 | |||
Joel Rosdahl
|
r4344 | def fix_long_right_edges(edges): | ||
for (i, (start, end)) in enumerate(edges): | ||||
if end > start: | ||||
edges[i] = (start, end + 1) | ||||
Dirkjan Ochtman
|
r7326 | 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) | ||||
Joel Rosdahl
|
r4344 | 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: | ||||
Martin Geisler
|
r9198 | (start, end) = (end, start) | ||
Joel Rosdahl
|
r4344 | for i in range(2 * start + 1, 2 * end): | ||
if nodeline[i] != "+": | ||||
nodeline[i] = "-" | ||||
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 | ||||
Peter Arrenbrecht
|
r9631 | def asciistate(): | ||
"""returns the initial value for the "state" argument to ascii()""" | ||||
return [0, 0] | ||||
def ascii(ui, state, type, char, text, coldata): | ||||
Peter Arrenbrecht
|
r8839 | """prints an ASCII graph of the DAG | ||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | takes the following arguments (one call per node in the graph): | ||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | - ui to write to | ||
Peter Arrenbrecht
|
r9631 | - Somewhere to keep the needed state in (init to asciistate()) | ||
Peter Arrenbrecht
|
r7325 | - Column of the current node in the set of ongoing edges. | ||
Peter Arrenbrecht
|
r8840 | - Type indicator of node data == ASCIIDATA. | ||
- Payload: (char, lines): | ||||
- Character to use as node's symbol. | ||||
- List of lines to display as the node's text. | ||||
Peter Arrenbrecht
|
r7325 | - 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. | ||||
""" | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | idx, edges, ncols, coldiff = coldata | ||
assert -2 < coldiff < 2 | ||||
if coldiff == -1: | ||||
# Transform | ||||
Joel Rosdahl
|
r4344 | # | ||
Dirkjan Ochtman
|
r9371 | # | | | | | | | ||
# 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(text) > 2 and coldiff == -1 and | ||||
[x for (x, y) in edges if x + 1 < y]) | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | # fix_nodeline_tail says whether to rewrite | ||
# | ||||
# | | o | | | | o | | | ||||
# | | |/ / | | |/ / | ||||
# | o | | into | o / / # <--- fixed nodeline tail | ||||
# | |/ / | |/ / | ||||
# o | | o | | | ||||
fix_nodeline_tail = len(text) <= 2 and not add_padding_line | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | # nodeline is the line containing the node character (typically o) | ||
nodeline = ["|", " "] * idx | ||||
nodeline.extend([char, " "]) | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | nodeline.extend( | ||
Peter Arrenbrecht
|
r9631 | get_nodeline_edges_tail(idx, state[1], ncols, coldiff, | ||
state[0], fix_nodeline_tail)) | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | # shift_interline is the line containing the non-vertical | ||
# edges between this entry and the next | ||||
shift_interline = ["|", " "] * idx | ||||
if coldiff == -1: | ||||
n_spaces = 1 | ||||
edge_ch = "/" | ||||
elif coldiff == 0: | ||||
n_spaces = 2 | ||||
edge_ch = "|" | ||||
else: | ||||
n_spaces = 3 | ||||
edge_ch = "\\" | ||||
shift_interline.extend(n_spaces * [" "]) | ||||
shift_interline.extend([edge_ch, " "] * (ncols - idx - 1)) | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | # draw edges from the current node to its parents | ||
draw_edges(edges, nodeline, shift_interline) | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | # lines is the list of all graph lines to print | ||
lines = [nodeline] | ||||
if add_padding_line: | ||||
lines.append(get_padding_line(idx, ncols, edges)) | ||||
lines.append(shift_interline) | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | # make sure that there are as many graph lines as there are | ||
# log strings | ||||
while len(text) < len(lines): | ||||
text.append("") | ||||
if len(lines) < len(text): | ||||
extra_interline = ["|", " "] * (ncols + coldiff) | ||||
while len(lines) < len(text): | ||||
lines.append(extra_interline) | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | # print lines | ||
indentation_level = max(ncols, ncols + coldiff) | ||||
for (line, logstr) in zip(lines, text): | ||||
ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr) | ||||
ui.write(ln.rstrip() + '\n') | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r9371 | # ... and start over | ||
Peter Arrenbrecht
|
r9631 | state[0] = coldiff | ||
state[1] = idx | ||||
Joel Rosdahl
|
r4344 | |||
Dirkjan Ochtman
|
r7326 | def get_revs(repo, rev_opt): | ||
if rev_opt: | ||||
revs = revrange(repo, rev_opt) | ||||
Eric Eisner
|
r11448 | if len(revs) == 0: | ||
return (nullrev, nullrev) | ||||
Dirkjan Ochtman
|
r7326 | return (max(revs), min(revs)) | ||
else: | ||||
return (len(repo) - 1, 0) | ||||
Alpar Juttner
|
r7426 | def check_unsupported_flags(opts): | ||
Alexander Solovyov
|
r14043 | for op in ["follow_first", "copies", "newest_first"]: | ||
Alpar Juttner
|
r7426 | if op in opts and opts[op]: | ||
Alexander Solovyov
|
r14043 | raise util.Abort(_("-G/--graph option is incompatible with --%s") | ||
Greg Ward
|
r10097 | % op.replace("_", "-")) | ||
Alpar Juttner
|
r7426 | |||
Alexander Solovyov
|
r14043 | def revset(pats, opts): | ||
"""Return revset str built of revisions, log options and file patterns. | ||||
""" | ||||
opt2revset = dict(only_merges='merge', | ||||
only_branch='branch', | ||||
no_merges='not merge', | ||||
include='file', | ||||
exclude='not file', | ||||
prune='not follow') | ||||
revset = [] | ||||
for op, val in opts.iteritems(): | ||||
if not val: | ||||
continue | ||||
revop = opt2revset.get(op, op) | ||||
if op in ('follow', 'only_merges', 'no_merges'): | ||||
revset.append('%s()' % revop) | ||||
elif op in ("date", "keyword", "remove", "user", "branch", | ||||
"only_branch", "prune"): | ||||
revset.append('%s(%s)' % (op, val)) | ||||
elif op in ('include', 'exclude'): | ||||
for f in val: | ||||
revset.append('%s(%r)' % (op, f)) | ||||
elif op == 'rev': | ||||
revset.extend(val) | ||||
for path in pats: | ||||
revset.append('file(%r)' % path) | ||||
revset = ' and '.join(revset) or 'all()' | ||||
# we want reverted revset to build graph | ||||
revset = 'reverse(%s)' % revset | ||||
if opts['limit']: | ||||
revset = 'limit(%s, %s)' % (revset, opts['limit']) | ||||
return revset | ||||
Dirkjan Ochtman
|
r9371 | def generate(ui, dag, displayer, showparents, edgefn): | ||
Peter Arrenbrecht
|
r9631 | seen, state = [], asciistate() | ||
Dirkjan Ochtman
|
r9369 | for rev, type, ctx, parents in dag: | ||
char = ctx.node() in showparents and '@' or 'o' | ||||
displayer.show(ctx) | ||||
lines = displayer.hunk.pop(rev).split('\n')[:-1] | ||||
Mads Kiilerich
|
r12579 | displayer.flush(rev) | ||
Peter Arrenbrecht
|
r9631 | ascii(ui, state, type, char, lines, edgefn(seen, rev, parents)) | ||
Mads Kiilerich
|
r12579 | displayer.close() | ||
Dirkjan Ochtman
|
r9369 | |||
Alexander Solovyov
|
r14043 | def graphlog(ui, repo, *pats, **opts): | ||
Peter Arrenbrecht
|
r7325 | """show revision history alongside an ASCII revision graph | ||
Martin Geisler
|
r9259 | Print a revision history alongside a revision graph drawn with | ||
ASCII characters. | ||||
Peter Arrenbrecht
|
r7325 | |||
Martin Geisler
|
r9259 | Nodes printed as an @ character are parents of the working | ||
directory. | ||||
Peter Arrenbrecht
|
r7325 | """ | ||
Alpar Juttner
|
r7426 | check_unsupported_flags(opts) | ||
Peter Arrenbrecht
|
r7370 | |||
Alexander Solovyov
|
r14043 | revs = revrange(repo, [revset(pats, opts)]) | ||
revdag = graphmod.dagwalker(repo, revs) | ||||
Peter Arrenbrecht
|
r7325 | |||
Dirkjan Ochtman
|
r9368 | displayer = show_changeset(ui, repo, opts, buffered=True) | ||
showparents = [ctx.node() for ctx in repo[None].parents()] | ||||
Dirkjan Ochtman
|
r9371 | generate(ui, revdag, displayer, showparents, asciiedges) | ||
Dirkjan Ochtman
|
r7716 | |||
def graphrevs(repo, nodes, opts): | ||||
limit = cmdutil.loglimit(opts) | ||||
Peter Arrenbrecht
|
r8837 | nodes.reverse() | ||
Nicolas Dumazet
|
r10111 | if limit is not None: | ||
Peter Arrenbrecht
|
r8837 | nodes = nodes[:limit] | ||
return graphmod.nodes(repo, nodes) | ||||
Dirkjan Ochtman
|
r7716 | |||
def goutgoing(ui, repo, dest=None, **opts): | ||||
"""show the outgoing changesets alongside an ASCII revision graph | ||||
Peter Arrenbrecht
|
r7325 | |||
Dirkjan Ochtman
|
r7716 | Print the outgoing changesets alongside a revision graph drawn with | ||
ASCII characters. | ||||
Alpar Juttner
|
r7426 | |||
Dirkjan Ochtman
|
r7716 | Nodes printed as an @ character are parents of the working | ||
directory. | ||||
Alpar Juttner
|
r7426 | """ | ||
Dirkjan Ochtman
|
r7716 | |||
check_unsupported_flags(opts) | ||||
Nicolas Dumazet
|
r12735 | o = hg._outgoing(ui, repo, dest, opts) | ||
if o is None: | ||||
Alpar Juttner
|
r7426 | return | ||
Dirkjan Ochtman
|
r7716 | |||
revdag = graphrevs(repo, o, opts) | ||||
Dirkjan Ochtman
|
r9368 | displayer = show_changeset(ui, repo, opts, buffered=True) | ||
showparents = [ctx.node() for ctx in repo[None].parents()] | ||||
Dirkjan Ochtman
|
r9371 | generate(ui, revdag, displayer, showparents, asciiedges) | ||
Alpar Juttner
|
r7426 | |||
def gincoming(ui, repo, source="default", **opts): | ||||
"""show the incoming changesets alongside an ASCII revision graph | ||||
Martin Geisler
|
r9259 | Print the incoming changesets alongside a revision graph drawn with | ||
ASCII characters. | ||||
Alpar Juttner
|
r7426 | |||
Martin Geisler
|
r9259 | Nodes printed as an @ character are parents of the working | ||
directory. | ||||
Alpar Juttner
|
r7426 | """ | ||
Nicolas Dumazet
|
r12730 | def subreporecurse(): | ||
return 1 | ||||
Alpar Juttner
|
r7426 | |||
check_unsupported_flags(opts) | ||||
Nicolas Dumazet
|
r12730 | def display(other, chlist, displayer): | ||
Dirkjan Ochtman
|
r7716 | revdag = graphrevs(other, chlist, opts) | ||
Dirkjan Ochtman
|
r9368 | showparents = [ctx.node() for ctx in repo[None].parents()] | ||
Dirkjan Ochtman
|
r9371 | generate(ui, revdag, displayer, showparents, asciiedges) | ||
Alpar Juttner
|
r7426 | |||
Nicolas Dumazet
|
r12730 | hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True) | ||
Alpar Juttner
|
r7426 | |||
def uisetup(ui): | ||||
'''Initialize the extension.''' | ||||
_wrapcmd(ui, 'log', commands.table, graphlog) | ||||
_wrapcmd(ui, 'incoming', commands.table, gincoming) | ||||
_wrapcmd(ui, 'outgoing', commands.table, goutgoing) | ||||
def _wrapcmd(ui, cmd, table, wrapfn): | ||||
'''wrap the command''' | ||||
def graph(orig, *args, **kwargs): | ||||
if kwargs['graph']: | ||||
Alexander Solovyov
|
r14043 | return wrapfn(*args, **kwargs) | ||
Alpar Juttner
|
r7426 | return orig(*args, **kwargs) | ||
entry = extensions.wrapcommand(table, cmd, graph) | ||||
Jim Correia
|
r7763 | entry[1].append(('G', 'graph', None, _("show the revision DAG"))) | ||
Alpar Juttner
|
r7426 | |||
Joel Rosdahl
|
r4344 | cmdtable = { | ||
"glog": | ||||
Thomas Arendsen Hein
|
r4730 | (graphlog, | ||
FUJIWARA Katsunori
|
r11321 | [('l', 'limit', '', | ||
_('limit number of changes displayed'), _('NUM')), | ||||
Thomas Arendsen Hein
|
r4730 | ('p', 'patch', False, _('show patch')), | ||
FUJIWARA Katsunori
|
r11321 | ('r', 'rev', [], | ||
_('show the specified revision or range'), _('REV')), | ||||
Thomas Arendsen Hein
|
r6192 | ] + templateopts, | ||
Thomas Arendsen Hein
|
r5942 | _('hg glog [OPTION]... [FILE]')), | ||
Joel Rosdahl
|
r4344 | } | ||