##// END OF EJS Templates
graphmod: add a function for topological iteration...
Pierre-Yves David -
r23564:f7ce0837 default
parent child Browse files
Show More
@@ -1,367 +1,529
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 or any later version.
8 8
9 9 """supports walking the history as DAGs suitable for graphical output
10 10
11 11 The most basic format we use is that of::
12 12
13 13 (id, type, data, [parentids])
14 14
15 15 The node and parent ids are arbitrary integers which identify a node in the
16 16 context of the graph returned. Type is a constant specifying the node type.
17 17 Data depends on type.
18 18 """
19 19
20 20 from mercurial.node import nullrev
21 21 import util
22 22
23 23 CHANGESET = 'C'
24 24
25 def groupbranchiter(revs, parentsfunc):
26 """yield revision from heads to roots one (topo) branch after the other.
27
28 This function aims to be used by a graph generator that wishes to minimize
29 the amount of parallel branches and their interleaving.
30
31 Example iteration order:
32
33 o 4
34 |
35 o 1
36 |
37 | o 3
38 | |
39 | o 2
40 |/
41 o 0
42
43 Currently does not handle non-contiguous <revs> input.
44
45 Currently consider every changeset under a merge to be on the same branch
46 using revision number to sort them.
47
48 Could be easily extend to give priority to an initial branch."""
49 ### Quick summary of the algorithm
50 #
51 # This function is based around a "retention" principle. We keep revisions
52 # in memory until we are ready to emit a whole branch that immediately
53 # "merge" into an existing one. This reduce the number of branch "ongoing"
54 # at the same time.
55 #
56 # During iteration revs are split into two groups:
57 # A) revision already emitted
58 # B) revision in "retention". They are stored as different subgroups.
59 #
60 # for each REV, we do the follow logic:
61 #
62 # a) if REV is a parent of (A), we will emit it. But before emitting it,
63 # we'll "free" all the revs from subgroup in (B) that were waiting for
64 # REV to be available. So we emit all revision of such subgroup before
65 # emitting REV
66 #
67 # b) else, we'll search for a subgroup in (B) awaiting for REV to be
68 # available, if such subgroup exist, we add REV to it and the subgroup is
69 # now awaiting for REV.parents() to be available.
70 #
71 # c) finally if no such group existed in (B), we create a new subgroup.
72 #
73 #
74 # To bootstrap the algorithm, we emit the tipmost revision.
75
76 revs.sort(reverse=True)
77
78 # Set of parents of revision that have been yield. They can be considered
79 # unblocked as the graph generator is already aware of them so there is no
80 # need to delay the one that reference them.
81 unblocked = set()
82
83 # list of group waiting to be displayed, each group is defined by:
84 #
85 # (revs: lists of revs waiting to be displayed,
86 # blocked: set of that cannot be displayed before those in 'revs')
87 #
88 # The second value ('blocked') correspond to parents of any revision in the
89 # group ('revs') that is not itself contained in the group. The main idea
90 # of this algorithm is to delay as much as possible the emission of any
91 # revision. This means waiting for the moment we are about to display
92 # theses parents to display the revs in a group.
93 #
94 # This first implementation is smart until it meet a merge: it will emit
95 # revs as soon as any parents is about to be emitted and can grow an
96 # arbitrary number of revs in 'blocked'. In practice this mean we properly
97 # retains new branches but give up on any special ordering for ancestors of
98 # merges. The implementation can be improved to handle this better.
99 #
100 # The first subgroup is special. It correspond to all the revision that
101 # were already emitted. The 'revs' lists is expected to be empty and the
102 # 'blocked' set contains the parents revisions of already emitted revision.
103 #
104 # You could pre-seed the <parents> set of groups[0] to a specific
105 # changesets to select what the first emitted branch should be.
106 #
107 # We do not support revisions will hole yet, but adding such support would
108 # be easy. The iteration will have to be done using both input revision and
109 # parents (see cl.ancestors function + a few tweaks) but only revisions
110 # parts of the initial set should be emitted.
111 groups = [([], unblocked)]
112 for current in revs:
113 # Look for a subgroup blocked, waiting for the current revision.
114 matching = [i for i, g in enumerate(groups) if current in g[1]]
115
116 if matching:
117 # The main idea is to gather together all sets that await on the
118 # same revision.
119 #
120 # This merging is done at the time we are about to add this common
121 # awaited to the subgroup for simplicity purpose. Such merge could
122 # happen sooner when we update the "blocked" set of revision.
123 #
124 # We also always keep the oldest subgroup first. We can probably
125 # improve the behavior by having the longuest set first. That way,
126 # graph algorythms could minimise the length of parallele lines
127 # their draw. This is currently not done.
128 targetidx = matching.pop(0)
129 trevs, tparents = groups[targetidx]
130 for i in matching:
131 gr = groups[i]
132 trevs.extend(gr[0])
133 tparents |= gr[1]
134 # delete all merged subgroups (but the one we keep)
135 # (starting from the last subgroup for performance and sanity reason)
136 for i in reversed(matching):
137 del groups[i]
138 else:
139 # This is a new head. We create a new subgroup for it.
140 targetidx = len(groups)
141 groups.append(([], set([current])))
142
143 gr = groups[targetidx]
144
145 # We now adds the current nodes to this subgroups. This is done after
146 # the subgroup merging because all elements from a subgroup that relied
147 # on this rev must preceed it.
148 #
149 # we also update the <parents> set to includes the parents on the
150 # new nodes.
151 gr[0].append(current)
152 gr[1].remove(current)
153 gr[1].update([p for p in parentsfunc(current) if p > nullrev])
154
155 # Look for a subgroup to display
156 #
157 # When unblocked is empty (if clause), We are not waiting over any
158 # revision during the first iteration (if no priority was given) or if
159 # we outputed a whole disconnected sets of the graph (reached a root).
160 # In that case we arbitrarily takes the oldest known subgroup. The
161 # heuristique could probably be better.
162 #
163 # Otherwise (elif clause) this mean we have some emitted revision. if
164 # the subgroup awaits on the same revision that the outputed ones, we
165 # can safely output it.
166 if not unblocked:
167 if len(groups) > 1: # display other subset
168 targetidx = 1
169 gr = groups[1]
170 elif not gr[1] & unblocked:
171 gr = None
172
173 if gr is not None:
174 # update the set of awaited revisions with the one from the
175 # subgroup
176 unblocked |= gr[1]
177 # output all revisions in the subgroup
178 for r in gr[0]:
179 yield r
180 # delete the subgroup that you just output
181 # unless it is groups[0] in which case you just empty it.
182 if targetidx:
183 del groups[targetidx]
184 else:
185 gr[0][:] = []
186
25 187 def dagwalker(repo, revs):
26 188 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples
27 189
28 190 This generator function walks through revisions (which should be ordered
29 191 from bigger to lower). It returns a tuple for each node. The node and parent
30 192 ids are arbitrary integers which identify a node in the context of the graph
31 193 returned.
32 194 """
33 195 if not revs:
34 196 return
35 197
36 198 cl = repo.changelog
37 199 lowestrev = revs.min()
38 200 gpcache = {}
39 201
40 202 for rev in revs:
41 203 ctx = repo[rev]
42 204 parents = sorted(set([p.rev() for p in ctx.parents()
43 205 if p.rev() in revs]))
44 206 mpars = [p.rev() for p in ctx.parents() if
45 207 p.rev() != nullrev and p.rev() not in parents]
46 208
47 209 for mpar in mpars:
48 210 gp = gpcache.get(mpar)
49 211 if gp is None:
50 212 gp = gpcache[mpar] = grandparent(cl, lowestrev, revs, mpar)
51 213 if not gp:
52 214 parents.append(mpar)
53 215 else:
54 216 parents.extend(g for g in gp if g not in parents)
55 217
56 218 yield (ctx.rev(), CHANGESET, ctx, parents)
57 219
58 220 def nodes(repo, nodes):
59 221 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples
60 222
61 223 This generator function walks the given nodes. It only returns parents
62 224 that are in nodes, too.
63 225 """
64 226 include = set(nodes)
65 227 for node in nodes:
66 228 ctx = repo[node]
67 229 parents = set([p.rev() for p in ctx.parents() if p.node() in include])
68 230 yield (ctx.rev(), CHANGESET, ctx, sorted(parents))
69 231
70 232 def colored(dag, repo):
71 233 """annotates a DAG with colored edge information
72 234
73 235 For each DAG node this function emits tuples::
74 236
75 237 (id, type, data, (col, color), [(col, nextcol, color)])
76 238
77 239 with the following new elements:
78 240
79 241 - Tuple (col, color) with column and color index for the current node
80 242 - A list of tuples indicating the edges between the current node and its
81 243 parents.
82 244 """
83 245 seen = []
84 246 colors = {}
85 247 newcolor = 1
86 248 config = {}
87 249
88 250 for key, val in repo.ui.configitems('graph'):
89 251 if '.' in key:
90 252 branch, setting = key.rsplit('.', 1)
91 253 # Validation
92 254 if setting == "width" and val.isdigit():
93 255 config.setdefault(branch, {})[setting] = int(val)
94 256 elif setting == "color" and val.isalnum():
95 257 config.setdefault(branch, {})[setting] = val
96 258
97 259 if config:
98 260 getconf = util.lrucachefunc(
99 261 lambda rev: config.get(repo[rev].branch(), {}))
100 262 else:
101 263 getconf = lambda rev: {}
102 264
103 265 for (cur, type, data, parents) in dag:
104 266
105 267 # Compute seen and next
106 268 if cur not in seen:
107 269 seen.append(cur) # new head
108 270 colors[cur] = newcolor
109 271 newcolor += 1
110 272
111 273 col = seen.index(cur)
112 274 color = colors.pop(cur)
113 275 next = seen[:]
114 276
115 277 # Add parents to next
116 278 addparents = [p for p in parents if p not in next]
117 279 next[col:col + 1] = addparents
118 280
119 281 # Set colors for the parents
120 282 for i, p in enumerate(addparents):
121 283 if not i:
122 284 colors[p] = color
123 285 else:
124 286 colors[p] = newcolor
125 287 newcolor += 1
126 288
127 289 # Add edges to the graph
128 290 edges = []
129 291 for ecol, eid in enumerate(seen):
130 292 if eid in next:
131 293 bconf = getconf(eid)
132 294 edges.append((
133 295 ecol, next.index(eid), colors[eid],
134 296 bconf.get('width', -1),
135 297 bconf.get('color', '')))
136 298 elif eid == cur:
137 299 for p in parents:
138 300 bconf = getconf(p)
139 301 edges.append((
140 302 ecol, next.index(p), color,
141 303 bconf.get('width', -1),
142 304 bconf.get('color', '')))
143 305
144 306 # Yield and move on
145 307 yield (cur, type, data, (col, color), edges)
146 308 seen = next
147 309
148 310 def grandparent(cl, lowestrev, roots, head):
149 311 """Return all ancestors of head in roots which revision is
150 312 greater or equal to lowestrev.
151 313 """
152 314 pending = set([head])
153 315 seen = set()
154 316 kept = set()
155 317 llowestrev = max(nullrev, lowestrev)
156 318 while pending:
157 319 r = pending.pop()
158 320 if r >= llowestrev and r not in seen:
159 321 if r in roots:
160 322 kept.add(r)
161 323 else:
162 324 pending.update([p for p in cl.parentrevs(r)])
163 325 seen.add(r)
164 326 return sorted(kept)
165 327
166 328 def asciiedges(type, char, lines, seen, rev, parents):
167 329 """adds edge info to changelog DAG walk suitable for ascii()"""
168 330 if rev not in seen:
169 331 seen.append(rev)
170 332 nodeidx = seen.index(rev)
171 333
172 334 knownparents = []
173 335 newparents = []
174 336 for parent in parents:
175 337 if parent in seen:
176 338 knownparents.append(parent)
177 339 else:
178 340 newparents.append(parent)
179 341
180 342 ncols = len(seen)
181 343 nextseen = seen[:]
182 344 nextseen[nodeidx:nodeidx + 1] = newparents
183 345 edges = [(nodeidx, nextseen.index(p)) for p in knownparents if p != nullrev]
184 346
185 347 while len(newparents) > 2:
186 348 # ascii() only knows how to add or remove a single column between two
187 349 # calls. Nodes with more than two parents break this constraint so we
188 350 # introduce intermediate expansion lines to grow the active node list
189 351 # slowly.
190 352 edges.append((nodeidx, nodeidx))
191 353 edges.append((nodeidx, nodeidx + 1))
192 354 nmorecols = 1
193 355 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
194 356 char = '\\'
195 357 lines = []
196 358 nodeidx += 1
197 359 ncols += 1
198 360 edges = []
199 361 del newparents[0]
200 362
201 363 if len(newparents) > 0:
202 364 edges.append((nodeidx, nodeidx))
203 365 if len(newparents) > 1:
204 366 edges.append((nodeidx, nodeidx + 1))
205 367 nmorecols = len(nextseen) - ncols
206 368 seen[:] = nextseen
207 369 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
208 370
209 371 def _fixlongrightedges(edges):
210 372 for (i, (start, end)) in enumerate(edges):
211 373 if end > start:
212 374 edges[i] = (start, end + 1)
213 375
214 376 def _getnodelineedgestail(
215 377 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
216 378 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
217 379 # Still going in the same non-vertical direction.
218 380 if n_columns_diff == -1:
219 381 start = max(node_index + 1, p_node_index)
220 382 tail = ["|", " "] * (start - node_index - 1)
221 383 tail.extend(["/", " "] * (n_columns - start))
222 384 return tail
223 385 else:
224 386 return ["\\", " "] * (n_columns - node_index - 1)
225 387 else:
226 388 return ["|", " "] * (n_columns - node_index - 1)
227 389
228 390 def _drawedges(edges, nodeline, interline):
229 391 for (start, end) in edges:
230 392 if start == end + 1:
231 393 interline[2 * end + 1] = "/"
232 394 elif start == end - 1:
233 395 interline[2 * start + 1] = "\\"
234 396 elif start == end:
235 397 interline[2 * start] = "|"
236 398 else:
237 399 if 2 * end >= len(nodeline):
238 400 continue
239 401 nodeline[2 * end] = "+"
240 402 if start > end:
241 403 (start, end) = (end, start)
242 404 for i in range(2 * start + 1, 2 * end):
243 405 if nodeline[i] != "+":
244 406 nodeline[i] = "-"
245 407
246 408 def _getpaddingline(ni, n_columns, edges):
247 409 line = []
248 410 line.extend(["|", " "] * ni)
249 411 if (ni, ni - 1) in edges or (ni, ni) in edges:
250 412 # (ni, ni - 1) (ni, ni)
251 413 # | | | | | | | |
252 414 # +---o | | o---+
253 415 # | | c | | c | |
254 416 # | |/ / | |/ /
255 417 # | | | | | |
256 418 c = "|"
257 419 else:
258 420 c = " "
259 421 line.extend([c, " "])
260 422 line.extend(["|", " "] * (n_columns - ni - 1))
261 423 return line
262 424
263 425 def asciistate():
264 426 """returns the initial value for the "state" argument to ascii()"""
265 427 return [0, 0]
266 428
267 429 def ascii(ui, state, type, char, text, coldata):
268 430 """prints an ASCII graph of the DAG
269 431
270 432 takes the following arguments (one call per node in the graph):
271 433
272 434 - ui to write to
273 435 - Somewhere to keep the needed state in (init to asciistate())
274 436 - Column of the current node in the set of ongoing edges.
275 437 - Type indicator of node data, usually 'C' for changesets.
276 438 - Payload: (char, lines):
277 439 - Character to use as node's symbol.
278 440 - List of lines to display as the node's text.
279 441 - Edges; a list of (col, next_col) indicating the edges between
280 442 the current node and its parents.
281 443 - Number of columns (ongoing edges) in the current revision.
282 444 - The difference between the number of columns (ongoing edges)
283 445 in the next revision and the number of columns (ongoing edges)
284 446 in the current revision. That is: -1 means one column removed;
285 447 0 means no columns added or removed; 1 means one column added.
286 448 """
287 449
288 450 idx, edges, ncols, coldiff = coldata
289 451 assert -2 < coldiff < 2
290 452 if coldiff == -1:
291 453 # Transform
292 454 #
293 455 # | | | | | |
294 456 # o | | into o---+
295 457 # |X / |/ /
296 458 # | | | |
297 459 _fixlongrightedges(edges)
298 460
299 461 # add_padding_line says whether to rewrite
300 462 #
301 463 # | | | | | | | |
302 464 # | o---+ into | o---+
303 465 # | / / | | | # <--- padding line
304 466 # o | | | / /
305 467 # o | |
306 468 add_padding_line = (len(text) > 2 and coldiff == -1 and
307 469 [x for (x, y) in edges if x + 1 < y])
308 470
309 471 # fix_nodeline_tail says whether to rewrite
310 472 #
311 473 # | | o | | | | o | |
312 474 # | | |/ / | | |/ /
313 475 # | o | | into | o / / # <--- fixed nodeline tail
314 476 # | |/ / | |/ /
315 477 # o | | o | |
316 478 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
317 479
318 480 # nodeline is the line containing the node character (typically o)
319 481 nodeline = ["|", " "] * idx
320 482 nodeline.extend([char, " "])
321 483
322 484 nodeline.extend(
323 485 _getnodelineedgestail(idx, state[1], ncols, coldiff,
324 486 state[0], fix_nodeline_tail))
325 487
326 488 # shift_interline is the line containing the non-vertical
327 489 # edges between this entry and the next
328 490 shift_interline = ["|", " "] * idx
329 491 if coldiff == -1:
330 492 n_spaces = 1
331 493 edge_ch = "/"
332 494 elif coldiff == 0:
333 495 n_spaces = 2
334 496 edge_ch = "|"
335 497 else:
336 498 n_spaces = 3
337 499 edge_ch = "\\"
338 500 shift_interline.extend(n_spaces * [" "])
339 501 shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
340 502
341 503 # draw edges from the current node to its parents
342 504 _drawedges(edges, nodeline, shift_interline)
343 505
344 506 # lines is the list of all graph lines to print
345 507 lines = [nodeline]
346 508 if add_padding_line:
347 509 lines.append(_getpaddingline(idx, ncols, edges))
348 510 lines.append(shift_interline)
349 511
350 512 # make sure that there are as many graph lines as there are
351 513 # log strings
352 514 while len(text) < len(lines):
353 515 text.append("")
354 516 if len(lines) < len(text):
355 517 extra_interline = ["|", " "] * (ncols + coldiff)
356 518 while len(lines) < len(text):
357 519 lines.append(extra_interline)
358 520
359 521 # print lines
360 522 indentation_level = max(ncols, ncols + coldiff)
361 523 for (line, logstr) in zip(lines, text):
362 524 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
363 525 ui.write(ln.rstrip() + '\n')
364 526
365 527 # ... and start over
366 528 state[0] = coldiff
367 529 state[1] = idx
General Comments 0
You need to be logged in to leave comments. Login now