1 # ASCII graph log extension for Mercurial
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
8 '''command to view revision graphs from a shell
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.
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 bundlerepo, changegroup, cmdutil, commands, extensions
21 from mercurial import hg, url, util, graphmod
25 def asciiformat(ui, repo, revdag, opts, parentrepo=None):
26 """formats a changelog DAG walk for ASCII output"""
27 if parentrepo is None:
29 showparents = [ctx.node() for ctx in parentrepo[None].parents()]
30 displayer = show_changeset(ui, repo, opts, buffered=True)
31 for (id, type, ctx, parentids) in revdag:
32 if type != graphmod.CHANGESET:
35 lines = displayer.hunk.pop(ctx.rev()).split('\n')[:-1]
36 char = ctx.node() in showparents and '@' or 'o'
37 yield (id, ASCIIDATA, (char, lines), parentids)
39 def asciiedges(nodes):
40 """adds edge info to changelog DAG walk suitable for ascii()"""
42 for node, type, data, parents in nodes:
45 nodeidx = seen.index(node)
49 for parent in parents:
51 knownparents.append(parent)
53 newparents.append(parent)
57 nextseen[nodeidx:nodeidx + 1] = newparents
58 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
60 if len(newparents) > 0:
61 edges.append((nodeidx, nodeidx))
62 if len(newparents) > 1:
63 edges.append((nodeidx, nodeidx + 1))
64 nmorecols = len(nextseen) - ncols
66 yield (nodeidx, type, data, edges, ncols, nmorecols)
68 def fix_long_right_edges(edges):
69 for (i, (start, end)) in enumerate(edges):
71 edges[i] = (start, end + 1)
73 def get_nodeline_edges_tail(
74 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
75 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
76 # Still going in the same non-vertical direction.
77 if n_columns_diff == -1:
78 start = max(node_index + 1, p_node_index)
79 tail = ["|", " "] * (start - node_index - 1)
80 tail.extend(["/", " "] * (n_columns - start))
83 return ["\\", " "] * (n_columns - node_index - 1)
85 return ["|", " "] * (n_columns - node_index - 1)
87 def draw_edges(edges, nodeline, interline):
88 for (start, end) in edges:
90 interline[2 * end + 1] = "/"
91 elif start == end - 1:
92 interline[2 * start + 1] = "\\"
94 interline[2 * start] = "|"
96 nodeline[2 * end] = "+"
98 (start, end) = (end, start)
99 for i in range(2 * start + 1, 2 * end):
100 if nodeline[i] != "+":
103 def get_padding_line(ni, n_columns, edges):
105 line.extend(["|", " "] * ni)
106 if (ni, ni - 1) in edges or (ni, ni) in edges:
107 # (ni, ni - 1) (ni, ni)
116 line.extend([c, " "])
117 line.extend(["|", " "] * (n_columns - ni - 1))
121 """prints an ASCII graph of the DAG
123 dag is a generator that emits tuples with the following elements:
125 - Column of the current node in the set of ongoing edges.
126 - Type indicator of node data == ASCIIDATA.
127 - Payload: (char, lines):
128 - Character to use as node's symbol.
129 - List of lines to display as the node's text.
130 - Edges; a list of (col, next_col) indicating the edges between
131 the current node and its parents.
132 - Number of columns (ongoing edges) in the current revision.
133 - The difference between the number of columns (ongoing edges)
134 in the next revision and the number of columns (ongoing edges)
135 in the current revision. That is: -1 means one column removed;
136 0 means no columns added or removed; 1 means one column added.
138 prev_n_columns_diff = 0
140 for (node_index, type, (node_ch, node_lines), edges, n_columns, n_columns_diff) in dag:
142 assert -2 < n_columns_diff < 2
143 if n_columns_diff == -1:
150 fix_long_right_edges(edges)
152 # add_padding_line says whether to rewrite
155 # | o---+ into | o---+
156 # | / / | | | # <--- padding line
159 add_padding_line = (len(node_lines) > 2 and
160 n_columns_diff == -1 and
161 [x for (x, y) in edges if x + 1 < y])
163 # fix_nodeline_tail says whether to rewrite
165 # | | o | | | | o | |
167 # | o | | into | o / / # <--- fixed nodeline tail
170 fix_nodeline_tail = len(node_lines) <= 2 and not add_padding_line
172 # nodeline is the line containing the node character (typically o)
173 nodeline = ["|", " "] * node_index
174 nodeline.extend([node_ch, " "])
177 get_nodeline_edges_tail(
178 node_index, prev_node_index, n_columns, n_columns_diff,
179 prev_n_columns_diff, fix_nodeline_tail))
181 # shift_interline is the line containing the non-vertical
182 # edges between this entry and the next
183 shift_interline = ["|", " "] * node_index
184 if n_columns_diff == -1:
187 elif n_columns_diff == 0:
193 shift_interline.extend(n_spaces * [" "])
194 shift_interline.extend([edge_ch, " "] * (n_columns - node_index - 1))
196 # draw edges from the current node to its parents
197 draw_edges(edges, nodeline, shift_interline)
199 # lines is the list of all graph lines to print
202 lines.append(get_padding_line(node_index, n_columns, edges))
203 lines.append(shift_interline)
205 # make sure that there are as many graph lines as there are
207 while len(node_lines) < len(lines):
208 node_lines.append("")
209 if len(lines) < len(node_lines):
210 extra_interline = ["|", " "] * (n_columns + n_columns_diff)
211 while len(lines) < len(node_lines):
212 lines.append(extra_interline)
215 indentation_level = max(n_columns, n_columns + n_columns_diff)
216 for (line, logstr) in zip(lines, node_lines):
217 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
218 ui.write(ln.rstrip() + '\n')
221 prev_node_index = node_index
222 prev_n_columns_diff = n_columns_diff
224 def get_revs(repo, rev_opt):
226 revs = revrange(repo, rev_opt)
227 return (max(revs), min(revs))
229 return (len(repo) - 1, 0)
231 def check_unsupported_flags(opts):
232 for op in ["follow", "follow_first", "date", "copies", "keyword", "remove",
233 "only_merges", "user", "only_branch", "prune", "newest_first",
234 "no_merges", "include", "exclude"]:
235 if op in opts and opts[op]:
236 raise util.Abort(_("--graph option is incompatible with --%s") % op)
238 def graphlog(ui, repo, path=None, **opts):
239 """show revision history alongside an ASCII revision graph
241 Print a revision history alongside a revision graph drawn with
244 Nodes printed as an @ character are parents of the working
248 check_unsupported_flags(opts)
249 limit = cmdutil.loglimit(opts)
250 start, stop = get_revs(repo, opts["rev"])
251 stop = max(stop, start - limit + 1)
256 path = util.canonpath(repo.root, os.getcwd(), path)
257 if path: # could be reset in canonpath
258 revdag = graphmod.filerevs(repo, path, start, stop)
260 revdag = graphmod.revisions(repo, start, stop)
262 fmtdag = asciiformat(ui, repo, revdag, opts)
263 ascii(ui, asciiedges(fmtdag))
265 def graphrevs(repo, nodes, opts):
266 limit = cmdutil.loglimit(opts)
268 if limit < sys.maxint:
269 nodes = nodes[:limit]
270 return graphmod.nodes(repo, nodes)
272 def goutgoing(ui, repo, dest=None, **opts):
273 """show the outgoing changesets alongside an ASCII revision graph
275 Print the outgoing changesets alongside a revision graph drawn with
278 Nodes printed as an @ character are parents of the working
282 check_unsupported_flags(opts)
283 dest, revs, checkout = hg.parseurl(
284 ui.expandpath(dest or 'default-push', dest or 'default'),
287 revs = [repo.lookup(rev) for rev in revs]
288 other = hg.repository(cmdutil.remoteui(ui, opts), dest)
289 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
290 o = repo.findoutgoing(other, force=opts.get('force'))
292 ui.status(_("no changes found\n"))
295 o = repo.changelog.nodesbetween(o, revs)[0]
296 revdag = graphrevs(repo, o, opts)
297 fmtdag = asciiformat(ui, repo, revdag, opts)
298 ascii(ui, asciiedges(fmtdag))
300 def gincoming(ui, repo, source="default", **opts):
301 """show the incoming changesets alongside an ASCII revision graph
303 Print the incoming changesets alongside a revision graph drawn with
306 Nodes printed as an @ character are parents of the working
310 check_unsupported_flags(opts)
311 source, revs, checkout = hg.parseurl(ui.expandpath(source), opts.get('rev'))
312 other = hg.repository(cmdutil.remoteui(repo, opts), source)
313 ui.status(_('comparing with %s\n') % url.hidepassword(source))
315 revs = [other.lookup(rev) for rev in revs]
316 incoming = repo.findincoming(other, heads=revs, force=opts["force"])
319 os.unlink(opts["bundle"])
322 ui.status(_("no changes found\n"))
328 fname = opts["bundle"]
329 if fname or not other.local():
330 # create a bundle (uncompressed if other repo is not local)
332 cg = other.changegroup(incoming, "incoming")
334 cg = other.changegroupsubset(incoming, revs, 'incoming')
335 bundletype = other.local() and "HG10BZ" or "HG10UN"
336 fname = cleanup = changegroup.writebundle(cg, fname, bundletype)
337 # keep written bundle?
340 if not other.local():
341 # use the created uncompressed bundlerepo
342 other = bundlerepo.bundlerepository(ui, repo.root, fname)
344 chlist = other.changelog.nodesbetween(incoming, revs)[0]
345 revdag = graphrevs(other, chlist, opts)
346 fmtdag = asciiformat(ui, other, revdag, opts, parentrepo=repo)
347 ascii(ui, asciiedges(fmtdag))
350 if hasattr(other, 'close'):
356 '''Initialize the extension.'''
357 _wrapcmd(ui, 'log', commands.table, graphlog)
358 _wrapcmd(ui, 'incoming', commands.table, gincoming)
359 _wrapcmd(ui, 'outgoing', commands.table, goutgoing)
361 def _wrapcmd(ui, cmd, table, wrapfn):
362 '''wrap the command'''
363 def graph(orig, *args, **kwargs):
365 return wrapfn(*args, **kwargs)
366 return orig(*args, **kwargs)
367 entry = extensions.wrapcommand(table, cmd, graph)
368 entry[1].append(('G', 'graph', None, _("show the revision DAG")))
373 [('l', 'limit', '', _('limit number of changes displayed')),
374 ('p', 'patch', False, _('show patch')),
375 ('r', 'rev', [], _('show the specified revision or range')),
377 _('hg glog [OPTION]... [FILE]')),