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