|
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 } |