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)