##// END OF EJS Templates
graphlog: simplify ascii drawing to process one cset at a time
Dirkjan Ochtman -
r9371:571a7acb default
parent child Browse files
Show More
@@ -1,375 +1,370 b''
1 1 # ASCII graph log extension for Mercurial
2 2 #
3 3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 '''command to view revision graphs from a shell
9 9
10 10 This extension adds a --graph option to the incoming, outgoing and log
11 11 commands. When this options is given, an ASCII representation of the
12 12 revision graph is also shown.
13 13 '''
14 14
15 15 import os, sys
16 16 from mercurial.cmdutil import revrange, show_changeset
17 17 from mercurial.commands import templateopts
18 18 from mercurial.i18n import _
19 19 from mercurial.node import nullrev
20 20 from mercurial import bundlerepo, changegroup, cmdutil, commands, extensions
21 21 from mercurial import hg, url, util, graphmod
22 22
23 23 ASCIIDATA = 'ASC'
24 24
25 25 def asciiedges(seen, rev, parents):
26 26 """adds edge info to changelog DAG walk suitable for ascii()"""
27 27 if rev not in seen:
28 28 seen.append(rev)
29 29 nodeidx = seen.index(rev)
30 30
31 31 knownparents = []
32 32 newparents = []
33 33 for parent in parents:
34 34 if parent in seen:
35 35 knownparents.append(parent)
36 36 else:
37 37 newparents.append(parent)
38 38
39 39 ncols = len(seen)
40 40 seen[nodeidx:nodeidx + 1] = newparents
41 41 edges = [(nodeidx, seen.index(p)) for p in knownparents]
42 42
43 43 if len(newparents) > 0:
44 44 edges.append((nodeidx, nodeidx))
45 45 if len(newparents) > 1:
46 46 edges.append((nodeidx, nodeidx + 1))
47 47
48 48 nmorecols = len(seen) - ncols
49 49 return nodeidx, edges, ncols, nmorecols
50 50
51 51 def fix_long_right_edges(edges):
52 52 for (i, (start, end)) in enumerate(edges):
53 53 if end > start:
54 54 edges[i] = (start, end + 1)
55 55
56 56 def get_nodeline_edges_tail(
57 57 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
58 58 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
59 59 # Still going in the same non-vertical direction.
60 60 if n_columns_diff == -1:
61 61 start = max(node_index + 1, p_node_index)
62 62 tail = ["|", " "] * (start - node_index - 1)
63 63 tail.extend(["/", " "] * (n_columns - start))
64 64 return tail
65 65 else:
66 66 return ["\\", " "] * (n_columns - node_index - 1)
67 67 else:
68 68 return ["|", " "] * (n_columns - node_index - 1)
69 69
70 70 def draw_edges(edges, nodeline, interline):
71 71 for (start, end) in edges:
72 72 if start == end + 1:
73 73 interline[2 * end + 1] = "/"
74 74 elif start == end - 1:
75 75 interline[2 * start + 1] = "\\"
76 76 elif start == end:
77 77 interline[2 * start] = "|"
78 78 else:
79 79 nodeline[2 * end] = "+"
80 80 if start > end:
81 81 (start, end) = (end, start)
82 82 for i in range(2 * start + 1, 2 * end):
83 83 if nodeline[i] != "+":
84 84 nodeline[i] = "-"
85 85
86 86 def get_padding_line(ni, n_columns, edges):
87 87 line = []
88 88 line.extend(["|", " "] * ni)
89 89 if (ni, ni - 1) in edges or (ni, ni) in edges:
90 90 # (ni, ni - 1) (ni, ni)
91 91 # | | | | | | | |
92 92 # +---o | | o---+
93 93 # | | c | | c | |
94 94 # | |/ / | |/ /
95 95 # | | | | | |
96 96 c = "|"
97 97 else:
98 98 c = " "
99 99 line.extend([c, " "])
100 100 line.extend(["|", " "] * (n_columns - ni - 1))
101 101 return line
102 102
103 def ascii(ui, dag):
103 def ascii(ui, base, type, char, text, coldata):
104 104 """prints an ASCII graph of the DAG
105 105
106 dag is a generator that emits tuples with the following elements:
106 takes the following arguments (one call per node in the graph):
107 107
108 - ui to write to
109 - A list we can keep the needed state in
108 110 - Column of the current node in the set of ongoing edges.
109 111 - Type indicator of node data == ASCIIDATA.
110 112 - Payload: (char, lines):
111 113 - Character to use as node's symbol.
112 114 - List of lines to display as the node's text.
113 115 - Edges; a list of (col, next_col) indicating the edges between
114 116 the current node and its parents.
115 117 - Number of columns (ongoing edges) in the current revision.
116 118 - The difference between the number of columns (ongoing edges)
117 119 in the next revision and the number of columns (ongoing edges)
118 120 in the current revision. That is: -1 means one column removed;
119 121 0 means no columns added or removed; 1 means one column added.
120 122 """
121 123
122 base = [0, 0]
123 for idx, type, (char, text), edges, ncols, coldiff in dag:
124
125 assert -2 < coldiff < 2
126 if coldiff == -1:
127 # Transform
128 #
129 # | | | | | |
130 # o | | into o---+
131 # |X / |/ /
132 # | | | |
133 fix_long_right_edges(edges)
134
135 # add_padding_line says whether to rewrite
124 idx, edges, ncols, coldiff = coldata
125 assert -2 < coldiff < 2
126 if coldiff == -1:
127 # Transform
136 128 #
137 # | | | | | | | |
138 # | o---+ into | o---+
139 # | / / | | | # <--- padding line
140 # o | | | / /
141 # o | |
142 add_padding_line = (len(text) > 2 and
143 coldiff == -1 and
144 [x for (x, y) in edges if x + 1 < y])
129 # | | | | | |
130 # o | | into o---+
131 # |X / |/ /
132 # | | | |
133 fix_long_right_edges(edges)
134
135 # add_padding_line says whether to rewrite
136 #
137 # | | | | | | | |
138 # | o---+ into | o---+
139 # | / / | | | # <--- padding line
140 # o | | | / /
141 # o | |
142 add_padding_line = (len(text) > 2 and coldiff == -1 and
143 [x for (x, y) in edges if x + 1 < y])
145 144
146 # fix_nodeline_tail says whether to rewrite
147 #
148 # | | o | | | | o | |
149 # | | |/ / | | |/ /
150 # | o | | into | o / / # <--- fixed nodeline tail
151 # | |/ / | |/ /
152 # o | | o | |
153 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
145 # fix_nodeline_tail says whether to rewrite
146 #
147 # | | o | | | | o | |
148 # | | |/ / | | |/ /
149 # | o | | into | o / / # <--- fixed nodeline tail
150 # | |/ / | |/ /
151 # o | | o | |
152 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
154 153
155 # nodeline is the line containing the node character (typically o)
156 nodeline = ["|", " "] * idx
157 nodeline.extend([char, " "])
154 # nodeline is the line containing the node character (typically o)
155 nodeline = ["|", " "] * idx
156 nodeline.extend([char, " "])
158 157
159 nodeline.extend(
160 get_nodeline_edges_tail(idx, base[1], ncols, coldiff,
161 base[0], fix_nodeline_tail))
158 nodeline.extend(
159 get_nodeline_edges_tail(idx, base[1], ncols, coldiff,
160 base[0], fix_nodeline_tail))
162 161
163 # shift_interline is the line containing the non-vertical
164 # edges between this entry and the next
165 shift_interline = ["|", " "] * idx
166 if coldiff == -1:
167 n_spaces = 1
168 edge_ch = "/"
169 elif coldiff == 0:
170 n_spaces = 2
171 edge_ch = "|"
172 else:
173 n_spaces = 3
174 edge_ch = "\\"
175 shift_interline.extend(n_spaces * [" "])
176 shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
162 # shift_interline is the line containing the non-vertical
163 # edges between this entry and the next
164 shift_interline = ["|", " "] * idx
165 if coldiff == -1:
166 n_spaces = 1
167 edge_ch = "/"
168 elif coldiff == 0:
169 n_spaces = 2
170 edge_ch = "|"
171 else:
172 n_spaces = 3
173 edge_ch = "\\"
174 shift_interline.extend(n_spaces * [" "])
175 shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
177 176
178 # draw edges from the current node to its parents
179 draw_edges(edges, nodeline, shift_interline)
177 # draw edges from the current node to its parents
178 draw_edges(edges, nodeline, shift_interline)
180 179
181 # lines is the list of all graph lines to print
182 lines = [nodeline]
183 if add_padding_line:
184 lines.append(get_padding_line(idx, ncols, edges))
185 lines.append(shift_interline)
180 # lines is the list of all graph lines to print
181 lines = [nodeline]
182 if add_padding_line:
183 lines.append(get_padding_line(idx, ncols, edges))
184 lines.append(shift_interline)
186 185
187 # make sure that there are as many graph lines as there are
188 # log strings
189 while len(text) < len(lines):
190 text.append("")
191 if len(lines) < len(text):
192 extra_interline = ["|", " "] * (ncols + coldiff)
193 while len(lines) < len(text):
194 lines.append(extra_interline)
186 # make sure that there are as many graph lines as there are
187 # log strings
188 while len(text) < len(lines):
189 text.append("")
190 if len(lines) < len(text):
191 extra_interline = ["|", " "] * (ncols + coldiff)
192 while len(lines) < len(text):
193 lines.append(extra_interline)
195 194
196 # print lines
197 indentation_level = max(ncols, ncols + coldiff)
198 for (line, logstr) in zip(lines, text):
199 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
200 ui.write(ln.rstrip() + '\n')
195 # print lines
196 indentation_level = max(ncols, ncols + coldiff)
197 for (line, logstr) in zip(lines, text):
198 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
199 ui.write(ln.rstrip() + '\n')
201 200
202 # ... and start over
203 base[0] = coldiff
204 base[1] = idx
201 # ... and start over
202 base[0] = coldiff
203 base[1] = idx
205 204
206 205 def get_revs(repo, rev_opt):
207 206 if rev_opt:
208 207 revs = revrange(repo, rev_opt)
209 208 return (max(revs), min(revs))
210 209 else:
211 210 return (len(repo) - 1, 0)
212 211
213 212 def check_unsupported_flags(opts):
214 213 for op in ["follow", "follow_first", "date", "copies", "keyword", "remove",
215 214 "only_merges", "user", "only_branch", "prune", "newest_first",
216 215 "no_merges", "include", "exclude"]:
217 216 if op in opts and opts[op]:
218 217 raise util.Abort(_("--graph option is incompatible with --%s") % op)
219 218
220 def generate(dag, displayer, showparents, edgefn):
221 seen = []
219 def generate(ui, dag, displayer, showparents, edgefn):
220 seen, base = [], [0, 0]
222 221 for rev, type, ctx, parents in dag:
223 222 char = ctx.node() in showparents and '@' or 'o'
224 223 displayer.show(ctx)
225 224 lines = displayer.hunk.pop(rev).split('\n')[:-1]
226 cols = edgefn(seen, rev, parents)
227 yield cols[0], type, (char, lines), cols[1], cols[2], cols[3]
225 ascii(ui, base, type, char, lines, edgefn(seen, rev, parents))
228 226
229 227 def graphlog(ui, repo, path=None, **opts):
230 228 """show revision history alongside an ASCII revision graph
231 229
232 230 Print a revision history alongside a revision graph drawn with
233 231 ASCII characters.
234 232
235 233 Nodes printed as an @ character are parents of the working
236 234 directory.
237 235 """
238 236
239 237 check_unsupported_flags(opts)
240 238 limit = cmdutil.loglimit(opts)
241 239 start, stop = get_revs(repo, opts["rev"])
242 240 stop = max(stop, start - limit + 1)
243 241 if start == nullrev:
244 242 return
245 243
246 244 if path:
247 245 path = util.canonpath(repo.root, os.getcwd(), path)
248 246 if path: # could be reset in canonpath
249 247 revdag = graphmod.filerevs(repo, path, start, stop)
250 248 else:
251 249 revdag = graphmod.revisions(repo, start, stop)
252 250
253 251 displayer = show_changeset(ui, repo, opts, buffered=True)
254 252 showparents = [ctx.node() for ctx in repo[None].parents()]
255 gen = generate(revdag, displayer, showparents, asciiedges)
256 ascii(ui, gen)
253 generate(ui, revdag, displayer, showparents, asciiedges)
257 254
258 255 def graphrevs(repo, nodes, opts):
259 256 limit = cmdutil.loglimit(opts)
260 257 nodes.reverse()
261 258 if limit < sys.maxint:
262 259 nodes = nodes[:limit]
263 260 return graphmod.nodes(repo, nodes)
264 261
265 262 def goutgoing(ui, repo, dest=None, **opts):
266 263 """show the outgoing changesets alongside an ASCII revision graph
267 264
268 265 Print the outgoing changesets alongside a revision graph drawn with
269 266 ASCII characters.
270 267
271 268 Nodes printed as an @ character are parents of the working
272 269 directory.
273 270 """
274 271
275 272 check_unsupported_flags(opts)
276 273 dest, revs, checkout = hg.parseurl(
277 274 ui.expandpath(dest or 'default-push', dest or 'default'),
278 275 opts.get('rev'))
279 276 if revs:
280 277 revs = [repo.lookup(rev) for rev in revs]
281 278 other = hg.repository(cmdutil.remoteui(ui, opts), dest)
282 279 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
283 280 o = repo.findoutgoing(other, force=opts.get('force'))
284 281 if not o:
285 282 ui.status(_("no changes found\n"))
286 283 return
287 284
288 285 o = repo.changelog.nodesbetween(o, revs)[0]
289 286 revdag = graphrevs(repo, o, opts)
290 287 displayer = show_changeset(ui, repo, opts, buffered=True)
291 288 showparents = [ctx.node() for ctx in repo[None].parents()]
292 gen = generate(revdag, displayer, showparents, asciiedges)
293 ascii(ui, gen)
289 generate(ui, revdag, displayer, showparents, asciiedges)
294 290
295 291 def gincoming(ui, repo, source="default", **opts):
296 292 """show the incoming changesets alongside an ASCII revision graph
297 293
298 294 Print the incoming changesets alongside a revision graph drawn with
299 295 ASCII characters.
300 296
301 297 Nodes printed as an @ character are parents of the working
302 298 directory.
303 299 """
304 300
305 301 check_unsupported_flags(opts)
306 302 source, revs, checkout = hg.parseurl(ui.expandpath(source), opts.get('rev'))
307 303 other = hg.repository(cmdutil.remoteui(repo, opts), source)
308 304 ui.status(_('comparing with %s\n') % url.hidepassword(source))
309 305 if revs:
310 306 revs = [other.lookup(rev) for rev in revs]
311 307 incoming = repo.findincoming(other, heads=revs, force=opts["force"])
312 308 if not incoming:
313 309 try:
314 310 os.unlink(opts["bundle"])
315 311 except:
316 312 pass
317 313 ui.status(_("no changes found\n"))
318 314 return
319 315
320 316 cleanup = None
321 317 try:
322 318
323 319 fname = opts["bundle"]
324 320 if fname or not other.local():
325 321 # create a bundle (uncompressed if other repo is not local)
326 322 if revs is None:
327 323 cg = other.changegroup(incoming, "incoming")
328 324 else:
329 325 cg = other.changegroupsubset(incoming, revs, 'incoming')
330 326 bundletype = other.local() and "HG10BZ" or "HG10UN"
331 327 fname = cleanup = changegroup.writebundle(cg, fname, bundletype)
332 328 # keep written bundle?
333 329 if opts["bundle"]:
334 330 cleanup = None
335 331 if not other.local():
336 332 # use the created uncompressed bundlerepo
337 333 other = bundlerepo.bundlerepository(ui, repo.root, fname)
338 334
339 335 chlist = other.changelog.nodesbetween(incoming, revs)[0]
340 336 revdag = graphrevs(other, chlist, opts)
341 337 displayer = show_changeset(ui, other, opts, buffered=True)
342 338 showparents = [ctx.node() for ctx in repo[None].parents()]
343 gen = generate(revdag, displayer, showparents, asciiedges)
344 ascii(ui, gen)
339 generate(ui, revdag, displayer, showparents, asciiedges)
345 340
346 341 finally:
347 342 if hasattr(other, 'close'):
348 343 other.close()
349 344 if cleanup:
350 345 os.unlink(cleanup)
351 346
352 347 def uisetup(ui):
353 348 '''Initialize the extension.'''
354 349 _wrapcmd(ui, 'log', commands.table, graphlog)
355 350 _wrapcmd(ui, 'incoming', commands.table, gincoming)
356 351 _wrapcmd(ui, 'outgoing', commands.table, goutgoing)
357 352
358 353 def _wrapcmd(ui, cmd, table, wrapfn):
359 354 '''wrap the command'''
360 355 def graph(orig, *args, **kwargs):
361 356 if kwargs['graph']:
362 357 return wrapfn(*args, **kwargs)
363 358 return orig(*args, **kwargs)
364 359 entry = extensions.wrapcommand(table, cmd, graph)
365 360 entry[1].append(('G', 'graph', None, _("show the revision DAG")))
366 361
367 362 cmdtable = {
368 363 "glog":
369 364 (graphlog,
370 365 [('l', 'limit', '', _('limit number of changes displayed')),
371 366 ('p', 'patch', False, _('show patch')),
372 367 ('r', 'rev', [], _('show the specified revision or range')),
373 368 ] + templateopts,
374 369 _('hg glog [OPTION]... [FILE]')),
375 370 }
General Comments 0
You need to be logged in to leave comments. Login now