mercurial/graphmod.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43776 faa8a59f4a06
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    25     pycompat,
    25     pycompat,
    26     smartset,
    26     smartset,
    27     util,
    27     util,
    28 )
    28 )
    29 
    29 
    30 CHANGESET = 'C'
    30 CHANGESET = b'C'
    31 PARENT = 'P'
    31 PARENT = b'P'
    32 GRANDPARENT = 'G'
    32 GRANDPARENT = b'G'
    33 MISSINGPARENT = 'M'
    33 MISSINGPARENT = b'M'
    34 # Style of line to draw. None signals a line that ends and is removed at this
    34 # Style of line to draw. None signals a line that ends and is removed at this
    35 # point. A number prefix means only the last N characters of the current block
    35 # point. A number prefix means only the last N characters of the current block
    36 # will use that style, the rest will use the PARENT style. Add a - sign
    36 # will use that style, the rest will use the PARENT style. Add a - sign
    37 # (so making N negative) and all but the first N characters use that style.
    37 # (so making N negative) and all but the first N characters use that style.
    38 EDGES = {PARENT: '|', GRANDPARENT: ':', MISSINGPARENT: None}
    38 EDGES = {PARENT: b'|', GRANDPARENT: b':', MISSINGPARENT: None}
    39 
    39 
    40 
    40 
    41 def dagwalker(repo, revs):
    41 def dagwalker(repo, revs):
    42     """cset DAG generator yielding (id, CHANGESET, ctx, [parentinfo]) tuples
    42     """cset DAG generator yielding (id, CHANGESET, ctx, [parentinfo]) tuples
    43 
    43 
   116     seen = []
   116     seen = []
   117     colors = {}
   117     colors = {}
   118     newcolor = 1
   118     newcolor = 1
   119     config = {}
   119     config = {}
   120 
   120 
   121     for key, val in repo.ui.configitems('graph'):
   121     for key, val in repo.ui.configitems(b'graph'):
   122         if '.' in key:
   122         if b'.' in key:
   123             branch, setting = key.rsplit('.', 1)
   123             branch, setting = key.rsplit(b'.', 1)
   124             # Validation
   124             # Validation
   125             if setting == "width" and val.isdigit():
   125             if setting == b"width" and val.isdigit():
   126                 config.setdefault(branch, {})[setting] = int(val)
   126                 config.setdefault(branch, {})[setting] = int(val)
   127             elif setting == "color" and val.isalnum():
   127             elif setting == b"color" and val.isalnum():
   128                 config.setdefault(branch, {})[setting] = val
   128                 config.setdefault(branch, {})[setting] = val
   129 
   129 
   130     if config:
   130     if config:
   131         getconf = util.lrucachefunc(
   131         getconf = util.lrucachefunc(
   132             lambda rev: config.get(repo[rev].branch(), {})
   132             lambda rev: config.get(repo[rev].branch(), {})
   166                 edges.append(
   166                 edges.append(
   167                     (
   167                     (
   168                         ecol,
   168                         ecol,
   169                         next.index(eid),
   169                         next.index(eid),
   170                         colors[eid],
   170                         colors[eid],
   171                         bconf.get('width', -1),
   171                         bconf.get(b'width', -1),
   172                         bconf.get('color', ''),
   172                         bconf.get(b'color', b''),
   173                     )
   173                     )
   174                 )
   174                 )
   175             elif eid == cur:
   175             elif eid == cur:
   176                 for ptype, p in parents:
   176                 for ptype, p in parents:
   177                     bconf = getconf(p)
   177                     bconf = getconf(p)
   178                     edges.append(
   178                     edges.append(
   179                         (
   179                         (
   180                             ecol,
   180                             ecol,
   181                             next.index(p),
   181                             next.index(p),
   182                             color,
   182                             color,
   183                             bconf.get('width', -1),
   183                             bconf.get(b'width', -1),
   184                             bconf.get('color', ''),
   184                             bconf.get(b'color', b''),
   185                         )
   185                         )
   186                     )
   186                     )
   187 
   187 
   188         # Yield and move on
   188         # Yield and move on
   189         yield (cur, type, data, (col, color), edges)
   189         yield (cur, type, data, (col, color), edges)
   190         seen = next
   190         seen = next
   191 
   191 
   192 
   192 
   193 def asciiedges(type, char, state, rev, parents):
   193 def asciiedges(type, char, state, rev, parents):
   194     """adds edge info to changelog DAG walk suitable for ascii()"""
   194     """adds edge info to changelog DAG walk suitable for ascii()"""
   195     seen = state['seen']
   195     seen = state[b'seen']
   196     if rev not in seen:
   196     if rev not in seen:
   197         seen.append(rev)
   197         seen.append(rev)
   198     nodeidx = seen.index(rev)
   198     nodeidx = seen.index(rev)
   199 
   199 
   200     knownparents = []
   200     knownparents = []
   205             continue
   205             continue
   206         if parent in seen:
   206         if parent in seen:
   207             knownparents.append(parent)
   207             knownparents.append(parent)
   208         else:
   208         else:
   209             newparents.append(parent)
   209             newparents.append(parent)
   210             state['edges'][parent] = state['styles'].get(ptype, '|')
   210             state[b'edges'][parent] = state[b'styles'].get(ptype, b'|')
   211 
   211 
   212     ncols = len(seen)
   212     ncols = len(seen)
   213     width = 1 + ncols * 2
   213     width = 1 + ncols * 2
   214     nextseen = seen[:]
   214     nextseen = seen[:]
   215     nextseen[nodeidx : nodeidx + 1] = newparents
   215     nextseen[nodeidx : nodeidx + 1] = newparents
   224         edges.append((nodeidx, nodeidx))
   224         edges.append((nodeidx, nodeidx))
   225         edges.append((nodeidx, nodeidx + 1))
   225         edges.append((nodeidx, nodeidx + 1))
   226         nmorecols = 1
   226         nmorecols = 1
   227         width += 2
   227         width += 2
   228         yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
   228         yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
   229         char = '\\'
   229         char = b'\\'
   230         nodeidx += 1
   230         nodeidx += 1
   231         ncols += 1
   231         ncols += 1
   232         edges = []
   232         edges = []
   233         del newparents[0]
   233         del newparents[0]
   234 
   234 
   238         edges.append((nodeidx, nodeidx + 1))
   238         edges.append((nodeidx, nodeidx + 1))
   239     nmorecols = len(nextseen) - ncols
   239     nmorecols = len(nextseen) - ncols
   240     if nmorecols > 0:
   240     if nmorecols > 0:
   241         width += 2
   241         width += 2
   242     # remove current node from edge characters, no longer needed
   242     # remove current node from edge characters, no longer needed
   243     state['edges'].pop(rev, None)
   243     state[b'edges'].pop(rev, None)
   244     yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
   244     yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
   245 
   245 
   246 
   246 
   247 def _fixlongrightedges(edges):
   247 def _fixlongrightedges(edges):
   248     for (i, (start, end)) in enumerate(edges):
   248     for (i, (start, end)) in enumerate(edges):
   254     if fix_tail and coldiff == pdiff and coldiff != 0:
   254     if fix_tail and coldiff == pdiff and coldiff != 0:
   255         # Still going in the same non-vertical direction.
   255         # Still going in the same non-vertical direction.
   256         if coldiff == -1:
   256         if coldiff == -1:
   257             start = max(idx + 1, pidx)
   257             start = max(idx + 1, pidx)
   258             tail = echars[idx * 2 : (start - 1) * 2]
   258             tail = echars[idx * 2 : (start - 1) * 2]
   259             tail.extend(["/", " "] * (ncols - start))
   259             tail.extend([b"/", b" "] * (ncols - start))
   260             return tail
   260             return tail
   261         else:
   261         else:
   262             return ["\\", " "] * (ncols - idx - 1)
   262             return [b"\\", b" "] * (ncols - idx - 1)
   263     else:
   263     else:
   264         remainder = ncols - idx - 1
   264         remainder = ncols - idx - 1
   265         return echars[-(remainder * 2) :] if remainder > 0 else []
   265         return echars[-(remainder * 2) :] if remainder > 0 else []
   266 
   266 
   267 
   267 
   268 def _drawedges(echars, edges, nodeline, interline):
   268 def _drawedges(echars, edges, nodeline, interline):
   269     for (start, end) in edges:
   269     for (start, end) in edges:
   270         if start == end + 1:
   270         if start == end + 1:
   271             interline[2 * end + 1] = "/"
   271             interline[2 * end + 1] = b"/"
   272         elif start == end - 1:
   272         elif start == end - 1:
   273             interline[2 * start + 1] = "\\"
   273             interline[2 * start + 1] = b"\\"
   274         elif start == end:
   274         elif start == end:
   275             interline[2 * start] = echars[2 * start]
   275             interline[2 * start] = echars[2 * start]
   276         else:
   276         else:
   277             if 2 * end >= len(nodeline):
   277             if 2 * end >= len(nodeline):
   278                 continue
   278                 continue
   279             nodeline[2 * end] = "+"
   279             nodeline[2 * end] = b"+"
   280             if start > end:
   280             if start > end:
   281                 (start, end) = (end, start)
   281                 (start, end) = (end, start)
   282             for i in range(2 * start + 1, 2 * end):
   282             for i in range(2 * start + 1, 2 * end):
   283                 if nodeline[i] != "+":
   283                 if nodeline[i] != b"+":
   284                     nodeline[i] = "-"
   284                     nodeline[i] = b"-"
   285 
   285 
   286 
   286 
   287 def _getpaddingline(echars, idx, ncols, edges):
   287 def _getpaddingline(echars, idx, ncols, edges):
   288     # all edges up to the current node
   288     # all edges up to the current node
   289     line = echars[: idx * 2]
   289     line = echars[: idx * 2]
   295         # | | X |           | X | |
   295         # | | X |           | X | |
   296         # | |/ /            | |/ /
   296         # | |/ /            | |/ /
   297         # | | |             | | |
   297         # | | |             | | |
   298         line.extend(echars[idx * 2 : (idx + 1) * 2])
   298         line.extend(echars[idx * 2 : (idx + 1) * 2])
   299     else:
   299     else:
   300         line.extend([' ', ' '])
   300         line.extend([b' ', b' '])
   301     # all edges to the right of the current node
   301     # all edges to the right of the current node
   302     remainder = ncols - idx - 1
   302     remainder = ncols - idx - 1
   303     if remainder > 0:
   303     if remainder > 0:
   304         line.extend(echars[-(remainder * 2) :])
   304         line.extend(echars[-(remainder * 2) :])
   305     return line
   305     return line
   320     # We need enough space to draw adjustment lines for these.
   320     # We need enough space to draw adjustment lines for these.
   321     edgechars = extra[::2]
   321     edgechars = extra[::2]
   322     while edgechars and edgechars[-1] is None:
   322     while edgechars and edgechars[-1] is None:
   323         edgechars.pop()
   323         edgechars.pop()
   324     shift_size = max((edgechars.count(None) * 2) - 1, 0)
   324     shift_size = max((edgechars.count(None) * 2) - 1, 0)
   325     minlines = 3 if not state['graphshorten'] else 2
   325     minlines = 3 if not state[b'graphshorten'] else 2
   326     while len(lines) < minlines + shift_size:
   326     while len(lines) < minlines + shift_size:
   327         lines.append(extra[:])
   327         lines.append(extra[:])
   328 
   328 
   329     if shift_size:
   329     if shift_size:
   330         empties = []
   330         empties = []
   336             else:
   336             else:
   337                 toshift.append(i * 2)
   337                 toshift.append(i * 2)
   338         targets = list(range(first_empty, first_empty + len(toshift) * 2, 2))
   338         targets = list(range(first_empty, first_empty + len(toshift) * 2, 2))
   339         positions = toshift[:]
   339         positions = toshift[:]
   340         for line in lines[-shift_size:]:
   340         for line in lines[-shift_size:]:
   341             line[first_empty:] = [' '] * (len(line) - first_empty)
   341             line[first_empty:] = [b' '] * (len(line) - first_empty)
   342             for i in range(len(positions)):
   342             for i in range(len(positions)):
   343                 pos = positions[i] - 1
   343                 pos = positions[i] - 1
   344                 positions[i] = max(pos, targets[i])
   344                 positions[i] = max(pos, targets[i])
   345                 line[pos] = '/' if pos > targets[i] else extra[toshift[i]]
   345                 line[pos] = b'/' if pos > targets[i] else extra[toshift[i]]
   346 
   346 
   347     map = {1: '|', 2: '~'} if not state['graphshorten'] else {1: '~'}
   347     map = {1: b'|', 2: b'~'} if not state[b'graphshorten'] else {1: b'~'}
   348     for i, line in enumerate(lines):
   348     for i, line in enumerate(lines):
   349         if None not in line:
   349         if None not in line:
   350             continue
   350             continue
   351         line[:] = [c or map.get(i, ' ') for c in line]
   351         line[:] = [c or map.get(i, b' ') for c in line]
   352 
   352 
   353     # remove edges that ended
   353     # remove edges that ended
   354     remove = [p for p, c in edgemap.items() if c is None]
   354     remove = [p for p, c in edgemap.items() if c is None]
   355     for parent in remove:
   355     for parent in remove:
   356         del edgemap[parent]
   356         del edgemap[parent]
   358 
   358 
   359 
   359 
   360 def asciistate():
   360 def asciistate():
   361     """returns the initial value for the "state" argument to ascii()"""
   361     """returns the initial value for the "state" argument to ascii()"""
   362     return {
   362     return {
   363         'seen': [],
   363         b'seen': [],
   364         'edges': {},
   364         b'edges': {},
   365         'lastcoldiff': 0,
   365         b'lastcoldiff': 0,
   366         'lastindex': 0,
   366         b'lastindex': 0,
   367         'styles': EDGES.copy(),
   367         b'styles': EDGES.copy(),
   368         'graphshorten': False,
   368         b'graphshorten': False,
   369     }
   369     }
   370 
   370 
   371 
   371 
   372 def outputgraph(ui, graph):
   372 def outputgraph(ui, graph):
   373     """outputs an ASCII graph of a DAG
   373     """outputs an ASCII graph of a DAG
   381 
   381 
   382     this function can be monkey-patched by extensions to alter graph display
   382     this function can be monkey-patched by extensions to alter graph display
   383     without needing to mimic all of the edge-fixup logic in ascii()
   383     without needing to mimic all of the edge-fixup logic in ascii()
   384     """
   384     """
   385     for (ln, logstr) in graph:
   385     for (ln, logstr) in graph:
   386         ui.write((ln + logstr).rstrip() + "\n")
   386         ui.write((ln + logstr).rstrip() + b"\n")
   387 
   387 
   388 
   388 
   389 def ascii(ui, state, type, char, text, coldata):
   389 def ascii(ui, state, type, char, text, coldata):
   390     """prints an ASCII graph of the DAG
   390     """prints an ASCII graph of the DAG
   391 
   391 
   407         0 means no columns added or removed; 1 means one column added.
   407         0 means no columns added or removed; 1 means one column added.
   408     """
   408     """
   409     idx, edges, ncols, coldiff = coldata
   409     idx, edges, ncols, coldiff = coldata
   410     assert -2 < coldiff < 2
   410     assert -2 < coldiff < 2
   411 
   411 
   412     edgemap, seen = state['edges'], state['seen']
   412     edgemap, seen = state[b'edges'], state[b'seen']
   413     # Be tolerant of history issues; make sure we have at least ncols + coldiff
   413     # Be tolerant of history issues; make sure we have at least ncols + coldiff
   414     # elements to work with. See test-glog.t for broken history test cases.
   414     # elements to work with. See test-glog.t for broken history test cases.
   415     echars = [c for p in seen for c in (edgemap.get(p, '|'), ' ')]
   415     echars = [c for p in seen for c in (edgemap.get(p, b'|'), b' ')]
   416     echars.extend(('|', ' ') * max(ncols + coldiff - len(seen), 0))
   416     echars.extend((b'|', b' ') * max(ncols + coldiff - len(seen), 0))
   417 
   417 
   418     if coldiff == -1:
   418     if coldiff == -1:
   419         # Transform
   419         # Transform
   420         #
   420         #
   421         #     | | |        | | |
   421         #     | | |        | | |
   444     #     o | |            o | |
   444     #     o | |            o | |
   445     fix_nodeline_tail = len(text) <= 2 and not add_padding_line
   445     fix_nodeline_tail = len(text) <= 2 and not add_padding_line
   446 
   446 
   447     # nodeline is the line containing the node character (typically o)
   447     # nodeline is the line containing the node character (typically o)
   448     nodeline = echars[: idx * 2]
   448     nodeline = echars[: idx * 2]
   449     nodeline.extend([char, " "])
   449     nodeline.extend([char, b" "])
   450 
   450 
   451     nodeline.extend(
   451     nodeline.extend(
   452         _getnodelineedgestail(
   452         _getnodelineedgestail(
   453             echars,
   453             echars,
   454             idx,
   454             idx,
   455             state['lastindex'],
   455             state[b'lastindex'],
   456             ncols,
   456             ncols,
   457             coldiff,
   457             coldiff,
   458             state['lastcoldiff'],
   458             state[b'lastcoldiff'],
   459             fix_nodeline_tail,
   459             fix_nodeline_tail,
   460         )
   460         )
   461     )
   461     )
   462 
   462 
   463     # shift_interline is the line containing the non-vertical
   463     # shift_interline is the line containing the non-vertical
   464     # edges between this entry and the next
   464     # edges between this entry and the next
   465     shift_interline = echars[: idx * 2]
   465     shift_interline = echars[: idx * 2]
   466     for i in pycompat.xrange(2 + coldiff):
   466     for i in pycompat.xrange(2 + coldiff):
   467         shift_interline.append(' ')
   467         shift_interline.append(b' ')
   468     count = ncols - idx - 1
   468     count = ncols - idx - 1
   469     if coldiff == -1:
   469     if coldiff == -1:
   470         for i in pycompat.xrange(count):
   470         for i in pycompat.xrange(count):
   471             shift_interline.extend(['/', ' '])
   471             shift_interline.extend([b'/', b' '])
   472     elif coldiff == 0:
   472     elif coldiff == 0:
   473         shift_interline.extend(echars[(idx + 1) * 2 : ncols * 2])
   473         shift_interline.extend(echars[(idx + 1) * 2 : ncols * 2])
   474     else:
   474     else:
   475         for i in pycompat.xrange(count):
   475         for i in pycompat.xrange(count):
   476             shift_interline.extend(['\\', ' '])
   476             shift_interline.extend([b'\\', b' '])
   477 
   477 
   478     # draw edges from the current node to its parents
   478     # draw edges from the current node to its parents
   479     _drawedges(echars, edges, nodeline, shift_interline)
   479     _drawedges(echars, edges, nodeline, shift_interline)
   480 
   480 
   481     # lines is the list of all graph lines to print
   481     # lines is the list of all graph lines to print
   483     if add_padding_line:
   483     if add_padding_line:
   484         lines.append(_getpaddingline(echars, idx, ncols, edges))
   484         lines.append(_getpaddingline(echars, idx, ncols, edges))
   485 
   485 
   486     # If 'graphshorten' config, only draw shift_interline
   486     # If 'graphshorten' config, only draw shift_interline
   487     # when there is any non vertical flow in graph.
   487     # when there is any non vertical flow in graph.
   488     if state['graphshorten']:
   488     if state[b'graphshorten']:
   489         if any(c in br'\/' for c in shift_interline if c):
   489         if any(c in br'\/' for c in shift_interline if c):
   490             lines.append(shift_interline)
   490             lines.append(shift_interline)
   491     # Else, no 'graphshorten' config so draw shift_interline.
   491     # Else, no 'graphshorten' config so draw shift_interline.
   492     else:
   492     else:
   493         lines.append(shift_interline)
   493         lines.append(shift_interline)
   500             lines.append(extra_interline[:])
   500             lines.append(extra_interline[:])
   501 
   501 
   502     _drawendinglines(lines, extra_interline, edgemap, seen, state)
   502     _drawendinglines(lines, extra_interline, edgemap, seen, state)
   503 
   503 
   504     while len(text) < len(lines):
   504     while len(text) < len(lines):
   505         text.append("")
   505         text.append(b"")
   506 
   506 
   507     # print lines
   507     # print lines
   508     indentation_level = max(ncols, ncols + coldiff)
   508     indentation_level = max(ncols, ncols + coldiff)
   509     lines = ["%-*s " % (2 * indentation_level, "".join(line)) for line in lines]
   509     lines = [
       
   510         b"%-*s " % (2 * indentation_level, b"".join(line)) for line in lines
       
   511     ]
   510     outputgraph(ui, zip(lines, text))
   512     outputgraph(ui, zip(lines, text))
   511 
   513 
   512     # ... and start over
   514     # ... and start over
   513     state['lastcoldiff'] = coldiff
   515     state[b'lastcoldiff'] = coldiff
   514     state['lastindex'] = idx
   516     state[b'lastindex'] = idx