mercurial/graphmod.py
changeset 28600 0d6137891114
parent 28376 fa2cd0c9a567
child 28601 cd10171d6c71
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)