]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/hgext/graphlog.py
hgwebfs: simplify retry loop construction
[plan9front.git] / sys / lib / python / hgext / graphlog.py
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, incorporated herein by reference.
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, sys
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
22
23 ASCIIDATA = 'ASC'
24
25 def asciiformat(ui, repo, revdag, opts, parentrepo=None):
26     """formats a changelog DAG walk for ASCII output"""
27     if parentrepo is None:
28         parentrepo = repo
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:
33             continue
34         displayer.show(ctx)
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)
38
39 def asciiedges(nodes):
40     """adds edge info to changelog DAG walk suitable for ascii()"""
41     seen = []
42     for node, type, data, parents in nodes:
43         if node not in seen:
44             seen.append(node)
45         nodeidx = seen.index(node)
46
47         knownparents = []
48         newparents = []
49         for parent in parents:
50             if parent in seen:
51                 knownparents.append(parent)
52             else:
53                 newparents.append(parent)
54
55         ncols = len(seen)
56         nextseen = seen[:]
57         nextseen[nodeidx:nodeidx + 1] = newparents
58         edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
59
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
65         seen = nextseen
66         yield (nodeidx, type, data, edges, ncols, nmorecols)
67
68 def fix_long_right_edges(edges):
69     for (i, (start, end)) in enumerate(edges):
70         if end > start:
71             edges[i] = (start, end + 1)
72
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))
81             return tail
82         else:
83             return ["\\", " "] * (n_columns - node_index - 1)
84     else:
85         return ["|", " "] * (n_columns - node_index - 1)
86
87 def draw_edges(edges, nodeline, interline):
88     for (start, end) in edges:
89         if start == end + 1:
90             interline[2 * end + 1] = "/"
91         elif start == end - 1:
92             interline[2 * start + 1] = "\\"
93         elif start == end:
94             interline[2 * start] = "|"
95         else:
96             nodeline[2 * end] = "+"
97             if start > end:
98                 (start, end) = (end, start)
99             for i in range(2 * start + 1, 2 * end):
100                 if nodeline[i] != "+":
101                     nodeline[i] = "-"
102
103 def get_padding_line(ni, n_columns, edges):
104     line = []
105     line.extend(["|", " "] * ni)
106     if (ni, ni - 1) in edges or (ni, ni) in edges:
107         # (ni, ni - 1)      (ni, ni)
108         # | | | |           | | | |
109         # +---o |           | o---+
110         # | | c |           | c | |
111         # | |/ /            | |/ /
112         # | | |             | | |
113         c = "|"
114     else:
115         c = " "
116     line.extend([c, " "])
117     line.extend(["|", " "] * (n_columns - ni - 1))
118     return line
119
120 def ascii(ui, dag):
121     """prints an ASCII graph of the DAG
122
123     dag is a generator that emits tuples with the following elements:
124
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.
137     """
138     prev_n_columns_diff = 0
139     prev_node_index = 0
140     for (node_index, type, (node_ch, node_lines), edges, n_columns, n_columns_diff) in dag:
141
142         assert -2 < n_columns_diff < 2
143         if n_columns_diff == -1:
144             # Transform
145             #
146             #     | | |        | | |
147             #     o | |  into  o---+
148             #     |X /         |/ /
149             #     | |          | |
150             fix_long_right_edges(edges)
151
152         # add_padding_line says whether to rewrite
153         #
154         #     | | | |        | | | |
155         #     | o---+  into  | o---+
156         #     |  / /         |   | |  # <--- padding line
157         #     o | |          |  / /
158         #                    o | |
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])
162
163         # fix_nodeline_tail says whether to rewrite
164         #
165         #     | | o | |        | | o | |
166         #     | | |/ /         | | |/ /
167         #     | o | |    into  | o / /   # <--- fixed nodeline tail
168         #     | |/ /           | |/ /
169         #     o | |            o | |
170         fix_nodeline_tail = len(node_lines) <= 2 and not add_padding_line
171
172         # nodeline is the line containing the node character (typically o)
173         nodeline = ["|", " "] * node_index
174         nodeline.extend([node_ch, " "])
175
176         nodeline.extend(
177             get_nodeline_edges_tail(
178                 node_index, prev_node_index, n_columns, n_columns_diff,
179                 prev_n_columns_diff, fix_nodeline_tail))
180
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:
185             n_spaces = 1
186             edge_ch = "/"
187         elif n_columns_diff == 0:
188             n_spaces = 2
189             edge_ch = "|"
190         else:
191             n_spaces = 3
192             edge_ch = "\\"
193         shift_interline.extend(n_spaces * [" "])
194         shift_interline.extend([edge_ch, " "] * (n_columns - node_index - 1))
195
196         # draw edges from the current node to its parents
197         draw_edges(edges, nodeline, shift_interline)
198
199         # lines is the list of all graph lines to print
200         lines = [nodeline]
201         if add_padding_line:
202             lines.append(get_padding_line(node_index, n_columns, edges))
203         lines.append(shift_interline)
204
205         # make sure that there are as many graph lines as there are
206         # log strings
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)
213
214         # print lines
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')
219
220         # ... and start over
221         prev_node_index = node_index
222         prev_n_columns_diff = n_columns_diff
223
224 def get_revs(repo, rev_opt):
225     if rev_opt:
226         revs = revrange(repo, rev_opt)
227         return (max(revs), min(revs))
228     else:
229         return (len(repo) - 1, 0)
230
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)
237
238 def graphlog(ui, repo, path=None, **opts):
239     """show revision history alongside an ASCII revision graph
240
241     Print a revision history alongside a revision graph drawn with
242     ASCII characters.
243
244     Nodes printed as an @ character are parents of the working
245     directory.
246     """
247
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)
252     if start == nullrev:
253         return
254
255     if path:
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)
259     else:
260         revdag = graphmod.revisions(repo, start, stop)
261
262     fmtdag = asciiformat(ui, repo, revdag, opts)
263     ascii(ui, asciiedges(fmtdag))
264
265 def graphrevs(repo, nodes, opts):
266     limit = cmdutil.loglimit(opts)
267     nodes.reverse()
268     if limit < sys.maxint:
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     dest, revs, checkout = hg.parseurl(
284         ui.expandpath(dest or 'default-push', dest or 'default'),
285         opts.get('rev'))
286     if revs:
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'))
291     if not o:
292         ui.status(_("no changes found\n"))
293         return
294
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))
299
300 def gincoming(ui, repo, source="default", **opts):
301     """show the incoming changesets alongside an ASCII revision graph
302
303     Print the incoming changesets alongside a revision graph drawn with
304     ASCII characters.
305
306     Nodes printed as an @ character are parents of the working
307     directory.
308     """
309
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))
314     if revs:
315         revs = [other.lookup(rev) for rev in revs]
316     incoming = repo.findincoming(other, heads=revs, force=opts["force"])
317     if not incoming:
318         try:
319             os.unlink(opts["bundle"])
320         except:
321             pass
322         ui.status(_("no changes found\n"))
323         return
324
325     cleanup = None
326     try:
327
328         fname = opts["bundle"]
329         if fname or not other.local():
330             # create a bundle (uncompressed if other repo is not local)
331             if revs is None:
332                 cg = other.changegroup(incoming, "incoming")
333             else:
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?
338             if opts["bundle"]:
339                 cleanup = None
340             if not other.local():
341                 # use the created uncompressed bundlerepo
342                 other = bundlerepo.bundlerepository(ui, repo.root, fname)
343
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))
348
349     finally:
350         if hasattr(other, 'close'):
351             other.close()
352         if cleanup:
353             os.unlink(cleanup)
354
355 def uisetup(ui):
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)
360
361 def _wrapcmd(ui, cmd, table, wrapfn):
362     '''wrap the command'''
363     def graph(orig, *args, **kwargs):
364         if kwargs['graph']:
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")))
369
370 cmdtable = {
371     "glog":
372         (graphlog,
373          [('l', 'limit', '', _('limit number of changes displayed')),
374           ('p', 'patch', False, _('show patch')),
375           ('r', 'rev', [], _('show the specified revision or range')),
376          ] + templateopts,
377          _('hg glog [OPTION]... [FILE]')),
378 }