comparison hgext/graphlog.py @ 7370:7bc62ebe7693

graphlog: refactor common grapher code Extracts the column and edge determination code into a separate function usable on generic DAGs with at most 2 parents per node. grapher() is very similar to graphmod.graph(). I shall look into merging them when I try visualizing patch branches in hgweb. Started using contexts and renamed a bunch of variables (fewer underscores).
author Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
date Fri, 14 Nov 2008 13:44:10 +0100
parents 87158be081b8
children 6ad2b914acbd
comparison
equal deleted inserted replaced
7369:87158be081b8 7370:7bc62ebe7693
11 from mercurial.cmdutil import revrange, show_changeset 11 from mercurial.cmdutil import revrange, show_changeset
12 from mercurial.commands import templateopts 12 from mercurial.commands import templateopts
13 from mercurial.i18n import _ 13 from mercurial.i18n import _
14 from mercurial.node import nullrev 14 from mercurial.node import nullrev
15 from mercurial.util import Abort, canonpath 15 from mercurial.util import Abort, canonpath
16 from mercurial import util 16
17 17 def revisions(repo, start, stop):
18 def get_rev_parents(repo, rev): 18 """cset DAG generator yielding (rev, node, [parents]) tuples
19 return [x for x in repo.changelog.parentrevs(rev) if x != nullrev] 19
20 20 This generator function walks through the revision history from revision
21 def revision_grapher(repo, start_rev, stop_rev): 21 start to revision stop (which must be less than or equal to start).
22 """incremental revision grapher 22 """
23 23 assert start >= stop
24 This generator function walks through the revision history from 24 cur = start
25 revision start_rev to revision stop_rev (which must be less than 25 while cur >= stop:
26 or equal to start_rev) and for each revision emits tuples with the 26 ctx = repo[cur]
27 following elements: 27 parents = sorted(p.rev() for p in ctx.parents() if p.rev() != nullrev)
28 28 yield (ctx, parents)
29 - Current revision. 29 cur -= 1
30 - Current node. 30
31 - Column of the current node in the set of ongoing edges. 31 def filerevs(repo, path, start, stop):
32 - Edges; a list of (col, next_col) indicating the edges between 32 """file cset DAG generator yielding (rev, node, [parents]) tuples
33 the current node and its parents. 33
34 - Number of columns (ongoing edges) in the current revision. 34 This generator function walks through the revision history of a single
35 - The difference between the number of columns (ongoing edges) 35 file from revision start to revision stop (which must be less than or
36 in the next revision and the number of columns (ongoing edges) 36 equal to start).
37 in the current revision. That is: -1 means one column removed; 37 """
38 0 means no columns added or removed; 1 means one column added. 38 assert start >= stop
39 """
40
41 assert start_rev >= stop_rev
42 curr_rev = start_rev
43 revs = []
44 while curr_rev >= stop_rev:
45 node = repo.changelog.node(curr_rev)
46
47 # Compute revs and next_revs.
48 if curr_rev not in revs:
49 # New head.
50 revs.append(curr_rev)
51 rev_index = revs.index(curr_rev)
52 next_revs = revs[:]
53
54 # Add parents to next_revs.
55 parents = get_rev_parents(repo, curr_rev)
56 parents_to_add = []
57 for parent in parents:
58 if parent not in next_revs:
59 parents_to_add.append(parent)
60 next_revs[rev_index:rev_index + 1] = util.sort(parents_to_add)
61
62 edges = []
63 for parent in parents:
64 edges.append((rev_index, next_revs.index(parent)))
65
66 n_columns_diff = len(next_revs) - len(revs)
67 yield (curr_rev, node, rev_index, edges, len(revs), n_columns_diff)
68
69 revs = next_revs
70 curr_rev -= 1
71
72 def filelog_grapher(repo, path, start_rev, stop_rev):
73 """incremental file log grapher
74
75 This generator function walks through the revision history of a
76 single file from revision start_rev to revision stop_rev (which must
77 be less than or equal to start_rev) and for each revision emits
78 tuples with the following elements:
79
80 - Current revision.
81 - Current node.
82 - Column of the current node in the set of ongoing edges.
83 - Edges; a list of (col, next_col) indicating the edges between
84 the current node and its parents.
85 - Number of columns (ongoing edges) in the current revision.
86 - The difference between the number of columns (ongoing edges)
87 in the next revision and the number of columns (ongoing edges)
88 in the current revision. That is: -1 means one column removed;
89 0 means no columns added or removed; 1 means one column added.
90 """
91
92 assert start_rev >= stop_rev
93 revs = []
94 filerev = len(repo.file(path)) - 1 39 filerev = len(repo.file(path)) - 1
95 while filerev >= 0: 40 while filerev >= 0:
96 fctx = repo.filectx(path, fileid=filerev) 41 fctx = repo.filectx(path, fileid=filerev)
97
98 # Compute revs and next_revs.
99 if filerev not in revs:
100 revs.append(filerev)
101 rev_index = revs.index(filerev)
102 next_revs = revs[:]
103
104 # Add parents to next_revs.
105 parents = [f.filerev() for f in fctx.parents() if f.path() == path] 42 parents = [f.filerev() for f in fctx.parents() if f.path() == path]
106 parents_to_add = [] 43 parents.sort()
44 if fctx.rev() <= start:
45 yield (fctx, parents)
46 if fctx.rev() <= stop:
47 break
48 filerev -= 1
49
50 def grapher(nodes):
51 """grapher for asciigraph on a list of nodes and their parents
52
53 nodes must generate tuples (node, parents, char, lines) where
54
55 - parents must generate the parents of node, in sorted order,
56 and max length 2,
57 - char is the char to print as the node symbol, and
58 - lines are the lines to display next to the node.
59 """
60 seen = []
61 for node, parents, char, lines in nodes:
62 if node not in seen:
63 seen.append(node)
64 nodeidx = seen.index(node)
65
66 knownparents = []
67 newparents = []
107 for parent in parents: 68 for parent in parents:
108 if parent not in next_revs: 69 if parent in seen:
109 parents_to_add.append(parent) 70 knownparents.append(parent)
110 next_revs[rev_index:rev_index + 1] = util.sort(parents_to_add) 71 else:
111 72 newparents.append(parent)
112 edges = [] 73
113 for parent in parents: 74 ncols = len(seen)
114 edges.append((rev_index, next_revs.index(parent))) 75 nextseen = seen[:]
115 76 nextseen[nodeidx:nodeidx + 1] = newparents
116 changerev = fctx.linkrev() 77 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
117 if changerev <= start_rev: 78
118 node = repo.changelog.node(changerev) 79 if len(newparents) > 0:
119 n_columns_diff = len(next_revs) - len(revs) 80 edges.append((nodeidx, nodeidx))
120 yield (changerev, node, rev_index, edges, len(revs), n_columns_diff) 81 if len(newparents) > 1:
121 if changerev <= stop_rev: 82 edges.append((nodeidx, nodeidx + 1))
122 break 83 nmorecols = len(nextseen) - ncols
123 revs = next_revs 84 seen = nextseen
124 filerev -= 1 85 yield (char, lines, nodeidx, edges, ncols, nmorecols)
125 86
126 def fix_long_right_edges(edges): 87 def fix_long_right_edges(edges):
127 for (i, (start, end)) in enumerate(edges): 88 for (i, (start, end)) in enumerate(edges):
128 if end > start: 89 if end > start:
129 edges[i] = (start, end + 1) 90 edges[i] = (start, end + 1)
305 Nodes printed as an @ character are parents of the working 266 Nodes printed as an @ character are parents of the working
306 directory. 267 directory.
307 """ 268 """
308 269
309 limit = get_limit(opts["limit"]) 270 limit = get_limit(opts["limit"])
310 (start_rev, stop_rev) = get_revs(repo, opts["rev"]) 271 start, stop = get_revs(repo, opts["rev"])
311 stop_rev = max(stop_rev, start_rev - limit + 1) 272 stop = max(stop, start - limit + 1)
312 if start_rev == nullrev: 273 if start == nullrev:
313 return 274 return
275
314 if path: 276 if path:
315 path = canonpath(repo.root, os.getcwd(), path) 277 path = canonpath(repo.root, os.getcwd(), path)
316 if path: 278 if path: # could be reset in canonpath
317 revgrapher = filelog_grapher(repo, path, start_rev, stop_rev) 279 revdag = filerevs(repo, path, start, stop)
318 else: 280 else:
319 revgrapher = revision_grapher(repo, start_rev, stop_rev) 281 revdag = revisions(repo, start, stop)
320 282
321 repo_parents = repo.dirstate.parents() 283 repo_parents = repo.dirstate.parents()
322 cs_printer = show_changeset(ui, repo, opts) 284 displayer = show_changeset(ui, repo, opts)
323 def grapher(): 285 def graphabledag():
324 for (rev, node, node_index, edges, n_columns, n_columns_diff) in revgrapher: 286 for (ctx, parents) in revdag:
325 # log_strings is the list of all log strings to draw alongside 287 # log_strings is the list of all log strings to draw alongside
326 # the graph. 288 # the graph.
327 ui.pushbuffer() 289 ui.pushbuffer()
328 cs_printer.show(repo[rev]) 290 displayer.show(ctx)
329 log_strings = ui.popbuffer().split("\n")[:-1] 291 log_strings = ui.popbuffer().split("\n")[:-1]
330 if node in repo_parents: 292 char = ctx.node() in repo_parents and '@' or 'o'
331 node_ch = "@" 293 yield (ctx.rev(), parents, char, log_strings)
332 else: 294
333 node_ch = "o" 295 ascii(ui, grapher(graphabledag()))
334 yield (node_ch, log_strings, node_index, edges, n_columns, n_columns_diff)
335
336 ascii(ui, grapher())
337 296
338 cmdtable = { 297 cmdtable = {
339 "glog": 298 "glog":
340 (graphlog, 299 (graphlog,
341 [('l', 'limit', '', _('limit number of changes displayed')), 300 [('l', 'limit', '', _('limit number of changes displayed')),