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