Mercurial > hg
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')), |