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