Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/graphmod.py @ 28600:0d6137891114
graphmod: allow for different styles for different edge types
Rather than draw all edges as solid lines, allow for using different styles for
different edge types. For example you could use dotted lines for edges that
do not connect to a parent, and dashed lines when connecting to a grandparent
(implying missing nodes in between).
For example, setting the following configuration:
[ui]
graphstyle.grandparent = :
graphstyle.missing = .
would result in a graph like this:
o changeset: 32:d06dffa21a31
|\ parent: 27:886ed638191b
| : parent: 31:621d83e11f67
| :
o : changeset: 31:621d83e11f67
|\: parent: 21:d42a756af44d
| : parent: 30:6e11cd4b648f
| :
o : changeset: 30:6e11cd4b648f
|\ \ parent: 28:44ecd0b9ae99
| . : parent: 29:cd9bb2be7593
| . :
o . : changeset: 28:44ecd0b9ae99
|\ \ \ parent: 1:6db2ef61d156
| . . : parent: 26:7f25b6c2f0b9
| . . :
o . . : changeset: 26:7f25b6c2f0b9
|\ \ \ \ parent: 18:1aa84d96232a
| | . . : parent: 25:91da8ed57247
| | . . :
| o-----+ changeset: 25:91da8ed57247
| | . . : parent: 21:d42a756af44d
| | . . : parent: 24:a9c19a3d96b7
| | . . :
| o . . : changeset: 24:a9c19a3d96b7
| |\ \ \ \ parent: 0:e6eb3150255d
| | . . . : parent: 23:a01cddf0766d
| | . . . :
| o---+ . : changeset: 23:a01cddf0766d
| | . . . : parent: 1:6db2ef61d156
| | . . . : parent: 22:e0d9cccacb5d
| | . . . :
| o-------+ changeset: 22:e0d9cccacb5d
| . . . . : parent: 18:1aa84d96232a
|/ / / / / parent: 21:d42a756af44d
| . . . :
| . . . o changeset: 21:d42a756af44d
| . . . |\ parent: 19:31ddc2c1573b
| . . . | | parent: 20:d30ed6450e32
| . . . | |
+-+-------o changeset: 20:d30ed6450e32
| . . . | parent: 0:e6eb3150255d
| . . . | parent: 18:1aa84d96232a
| . . . |
| . . . o changeset: 19:31ddc2c1573b
| . . . .\ parent: 15:1dda3f72782d
| . . . . | parent: 17:44765d7c06e0
| . . . . |
o---+---+ | changeset: 18:1aa84d96232a
. . . . | parent: 1:6db2ef61d156
/ / / / / parent: 15:1dda3f72782d
. . . . .
Edge styles can be altered by setting the following one-character config options::
[ui]
graphstyle.parent = |
graphstyle.grandparent = :
graphstyle.missing = .
The default configuration leaves all 3 types set to |, leaving graph styles
unaffected.
This is part of the work towards moving smartlog upstream; currently smartlog
injects extra nodes into the graph to indicate grandparent relationships (nodes
elided).
author | Martijn Pieters <mjpieters@fb.com> |
---|---|
date | Sat, 19 Mar 2016 16:46:15 -0700 |
parents | fa2cd0c9a567 |
children | cd10171d6c71 |
comparison
equal
deleted
inserted
replaced
28599:0e7a929754aa | 28600:0d6137891114 |
---|---|
29 | 29 |
30 CHANGESET = 'C' | 30 CHANGESET = 'C' |
31 PARENT = 'P' | 31 PARENT = 'P' |
32 GRANDPARENT = 'G' | 32 GRANDPARENT = 'G' |
33 MISSINGPARENT = 'M' | 33 MISSINGPARENT = 'M' |
34 EDGES = {PARENT: '|', GRANDPARENT: '|', MISSINGPARENT: '|'} | |
34 | 35 |
35 def groupbranchiter(revs, parentsfunc, firstbranch=()): | 36 def groupbranchiter(revs, parentsfunc, firstbranch=()): |
36 """Yield revisions from heads to roots one (topo) branch at a time. | 37 """Yield revisions from heads to roots one (topo) branch at a time. |
37 | 38 |
38 This function aims to be used by a graph generator that wishes to minimize | 39 This function aims to be used by a graph generator that wishes to minimize |
388 for ptype, parent in parents: | 389 for ptype, parent in parents: |
389 if parent in seen: | 390 if parent in seen: |
390 knownparents.append(parent) | 391 knownparents.append(parent) |
391 else: | 392 else: |
392 newparents.append(parent) | 393 newparents.append(parent) |
394 state['edges'][parent] = state['styles'].get(ptype, '|') | |
393 | 395 |
394 ncols = len(seen) | 396 ncols = len(seen) |
395 nextseen = seen[:] | 397 nextseen = seen[:] |
396 nextseen[nodeidx:nodeidx + 1] = newparents | 398 nextseen[nodeidx:nodeidx + 1] = newparents |
397 edges = [(nodeidx, nextseen.index(p)) for p in knownparents if p != nullrev] | 399 edges = [(nodeidx, nextseen.index(p)) |
400 for p in knownparents if p != nullrev] | |
398 | 401 |
399 while len(newparents) > 2: | 402 while len(newparents) > 2: |
400 # ascii() only knows how to add or remove a single column between two | 403 # ascii() only knows how to add or remove a single column between two |
401 # calls. Nodes with more than two parents break this constraint so we | 404 # calls. Nodes with more than two parents break this constraint so we |
402 # introduce intermediate expansion lines to grow the active node list | 405 # introduce intermediate expansion lines to grow the active node list |
416 edges.append((nodeidx, nodeidx)) | 419 edges.append((nodeidx, nodeidx)) |
417 if len(newparents) > 1: | 420 if len(newparents) > 1: |
418 edges.append((nodeidx, nodeidx + 1)) | 421 edges.append((nodeidx, nodeidx + 1)) |
419 nmorecols = len(nextseen) - ncols | 422 nmorecols = len(nextseen) - ncols |
420 seen[:] = nextseen | 423 seen[:] = nextseen |
424 # remove current node from edge characters, no longer needed | |
425 state['edges'].pop(rev, None) | |
421 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols)) | 426 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols)) |
422 | 427 |
423 def _fixlongrightedges(edges): | 428 def _fixlongrightedges(edges): |
424 for (i, (start, end)) in enumerate(edges): | 429 for (i, (start, end)) in enumerate(edges): |
425 if end > start: | 430 if end > start: |
426 edges[i] = (start, end + 1) | 431 edges[i] = (start, end + 1) |
427 | 432 |
428 def _getnodelineedgestail( | 433 def _getnodelineedgestail( |
429 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail): | 434 echars, idx, pidx, ncols, coldiff, pdiff, fix_tail): |
430 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0: | 435 if fix_tail and coldiff == pdiff and coldiff != 0: |
431 # Still going in the same non-vertical direction. | 436 # Still going in the same non-vertical direction. |
432 if n_columns_diff == -1: | 437 if coldiff == -1: |
433 start = max(node_index + 1, p_node_index) | 438 start = max(idx + 1, pidx) |
434 tail = ["|", " "] * (start - node_index - 1) | 439 tail = echars[idx * 2:(start - 1) * 2] |
435 tail.extend(["/", " "] * (n_columns - start)) | 440 tail.extend(["/", " "] * (ncols - start)) |
436 return tail | 441 return tail |
437 else: | 442 else: |
438 return ["\\", " "] * (n_columns - node_index - 1) | 443 return ["\\", " "] * (ncols - idx - 1) |
439 else: | 444 else: |
440 return ["|", " "] * (n_columns - node_index - 1) | 445 remainder = (ncols - idx - 1) |
441 | 446 return echars[-(remainder * 2):] if remainder > 0 else [] |
442 def _drawedges(edges, nodeline, interline): | 447 |
448 def _drawedges(echars, edges, nodeline, interline): | |
443 for (start, end) in edges: | 449 for (start, end) in edges: |
444 if start == end + 1: | 450 if start == end + 1: |
445 interline[2 * end + 1] = "/" | 451 interline[2 * end + 1] = "/" |
446 elif start == end - 1: | 452 elif start == end - 1: |
447 interline[2 * start + 1] = "\\" | 453 interline[2 * start + 1] = "\\" |
448 elif start == end: | 454 elif start == end: |
449 interline[2 * start] = "|" | 455 interline[2 * start] = echars[2 * start] |
450 else: | 456 else: |
451 if 2 * end >= len(nodeline): | 457 if 2 * end >= len(nodeline): |
452 continue | 458 continue |
453 nodeline[2 * end] = "+" | 459 nodeline[2 * end] = "+" |
454 if start > end: | 460 if start > end: |
455 (start, end) = (end, start) | 461 (start, end) = (end, start) |
456 for i in range(2 * start + 1, 2 * end): | 462 for i in range(2 * start + 1, 2 * end): |
457 if nodeline[i] != "+": | 463 if nodeline[i] != "+": |
458 nodeline[i] = "-" | 464 nodeline[i] = "-" |
459 | 465 |
460 def _getpaddingline(ni, n_columns, edges): | 466 def _getpaddingline(echars, idx, ncols, edges): |
461 line = [] | 467 # all edges up to the current node |
462 line.extend(["|", " "] * ni) | 468 line = echars[:idx * 2] |
463 if (ni, ni - 1) in edges or (ni, ni) in edges: | 469 # an edge for the current node, if there is one |
464 # (ni, ni - 1) (ni, ni) | 470 if (idx, idx - 1) in edges or (idx, idx) in edges: |
471 # (idx, idx - 1) (idx, idx) | |
465 # | | | | | | | | | 472 # | | | | | | | | |
466 # +---o | | o---+ | 473 # +---o | | o---+ |
467 # | | c | | c | | | 474 # | | X | | X | | |
468 # | |/ / | |/ / | 475 # | |/ / | |/ / |
469 # | | | | | | | 476 # | | | | | | |
470 c = "|" | 477 line.extend(echars[idx * 2:(idx + 1) * 2]) |
471 else: | 478 else: |
472 c = " " | 479 line.extend(' ') |
473 line.extend([c, " "]) | 480 # all edges to the right of the current node |
474 line.extend(["|", " "] * (n_columns - ni - 1)) | 481 remainder = ncols - idx - 1 |
482 if remainder > 0: | |
483 line.extend(echars[-(remainder * 2):]) | |
475 return line | 484 return line |
476 | 485 |
477 def asciistate(): | 486 def asciistate(): |
478 """returns the initial value for the "state" argument to ascii()""" | 487 """returns the initial value for the "state" argument to ascii()""" |
479 return {'seen': [], 'lastcoldiff': 0, 'lastindex': 0} | 488 return { |
489 'seen': [], | |
490 'edges': {}, | |
491 'lastcoldiff': 0, | |
492 'lastindex': 0, | |
493 'styles': EDGES.copy(), | |
494 } | |
480 | 495 |
481 def ascii(ui, state, type, char, text, coldata): | 496 def ascii(ui, state, type, char, text, coldata): |
482 """prints an ASCII graph of the DAG | 497 """prints an ASCII graph of the DAG |
483 | 498 |
484 takes the following arguments (one call per node in the graph): | 499 takes the following arguments (one call per node in the graph): |
496 - The difference between the number of columns (ongoing edges) | 511 - The difference between the number of columns (ongoing edges) |
497 in the next revision and the number of columns (ongoing edges) | 512 in the next revision and the number of columns (ongoing edges) |
498 in the current revision. That is: -1 means one column removed; | 513 in the current revision. That is: -1 means one column removed; |
499 0 means no columns added or removed; 1 means one column added. | 514 0 means no columns added or removed; 1 means one column added. |
500 """ | 515 """ |
501 | |
502 idx, edges, ncols, coldiff = coldata | 516 idx, edges, ncols, coldiff = coldata |
503 assert -2 < coldiff < 2 | 517 assert -2 < coldiff < 2 |
518 | |
519 edgemap, seen = state['edges'], state['seen'] | |
520 # Be tolerant of history issues; make sure we have at least ncols + coldiff | |
521 # elements to work with. See test-glog.t for broken history test cases. | |
522 echars = [c for p in seen for c in (edgemap.get(p, '|'), ' ')] | |
523 echars.extend(('|', ' ') * max(ncols + coldiff - len(seen), 0)) | |
524 | |
504 if coldiff == -1: | 525 if coldiff == -1: |
505 # Transform | 526 # Transform |
506 # | 527 # |
507 # | | | | | | | 528 # | | | | | | |
508 # o | | into o---+ | 529 # o | | into o---+ |
528 # | |/ / | |/ / | 549 # | |/ / | |/ / |
529 # o | | o | | | 550 # o | | o | | |
530 fix_nodeline_tail = len(text) <= 2 and not add_padding_line | 551 fix_nodeline_tail = len(text) <= 2 and not add_padding_line |
531 | 552 |
532 # nodeline is the line containing the node character (typically o) | 553 # nodeline is the line containing the node character (typically o) |
533 nodeline = ["|", " "] * idx | 554 nodeline = echars[:idx * 2] |
534 nodeline.extend([char, " "]) | 555 nodeline.extend([char, " "]) |
535 | 556 |
536 nodeline.extend( | 557 nodeline.extend( |
537 _getnodelineedgestail(idx, state['lastindex'], ncols, coldiff, | 558 _getnodelineedgestail( |
538 state['lastcoldiff'], fix_nodeline_tail)) | 559 echars, idx, state['lastindex'], ncols, coldiff, |
560 state['lastcoldiff'], fix_nodeline_tail)) | |
539 | 561 |
540 # shift_interline is the line containing the non-vertical | 562 # shift_interline is the line containing the non-vertical |
541 # edges between this entry and the next | 563 # edges between this entry and the next |
542 shift_interline = ["|", " "] * idx | 564 shift_interline = echars[:idx * 2] |
565 shift_interline.extend(' ' * (2 + coldiff)) | |
566 count = ncols - idx - 1 | |
543 if coldiff == -1: | 567 if coldiff == -1: |
544 n_spaces = 1 | 568 shift_interline.extend('/ ' * count) |
545 edge_ch = "/" | |
546 elif coldiff == 0: | 569 elif coldiff == 0: |
547 n_spaces = 2 | 570 shift_interline.extend(echars[(idx + 1) * 2:ncols * 2]) |
548 edge_ch = "|" | |
549 else: | 571 else: |
550 n_spaces = 3 | 572 shift_interline.extend(r'\ ' * count) |
551 edge_ch = "\\" | |
552 shift_interline.extend(n_spaces * [" "]) | |
553 shift_interline.extend([edge_ch, " "] * (ncols - idx - 1)) | |
554 | 573 |
555 # draw edges from the current node to its parents | 574 # draw edges from the current node to its parents |
556 _drawedges(edges, nodeline, shift_interline) | 575 _drawedges(echars, edges, nodeline, shift_interline) |
557 | 576 |
558 # lines is the list of all graph lines to print | 577 # lines is the list of all graph lines to print |
559 lines = [nodeline] | 578 lines = [nodeline] |
560 if add_padding_line: | 579 if add_padding_line: |
561 lines.append(_getpaddingline(idx, ncols, edges)) | 580 lines.append(_getpaddingline(echars, idx, ncols, edges)) |
562 lines.append(shift_interline) | 581 lines.append(shift_interline) |
563 | 582 |
564 # make sure that there are as many graph lines as there are | 583 # make sure that there are as many graph lines as there are |
565 # log strings | 584 # log strings |
566 while len(text) < len(lines): | 585 while len(text) < len(lines): |
567 text.append("") | 586 text.append("") |
568 if len(lines) < len(text): | 587 if len(lines) < len(text): |
569 extra_interline = ["|", " "] * (ncols + coldiff) | 588 extra_interline = echars[:(ncols + coldiff) * 2] |
570 while len(lines) < len(text): | 589 while len(lines) < len(text): |
571 lines.append(extra_interline) | 590 lines.append(extra_interline) |
572 | 591 |
573 # print lines | 592 # print lines |
574 indentation_level = max(ncols, ncols + coldiff) | 593 indentation_level = max(ncols, ncols + coldiff) |