tests/drawdag.py
changeset 30449 a31634336471
child 31671 d761ef24d6e1
equal deleted inserted replaced
30448:8836f13e3c5b 30449:a31634336471
       
     1 # drawdag.py - convert ASCII revision DAG to actual changesets
       
     2 #
       
     3 # Copyright 2016 Facebook, Inc.
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 """
       
     8 create changesets from an ASCII graph for testing purpose.
       
     9 
       
    10 For example, given the following input::
       
    11 
       
    12     c d
       
    13     |/
       
    14     b
       
    15     |
       
    16     a
       
    17 
       
    18 4 changesets and 4 local tags will be created.
       
    19 `hg log -G -T "{rev} {desc} (tag: {tags})"` will output::
       
    20 
       
    21     o  3 d (tag: d tip)
       
    22     |
       
    23     | o  2 c (tag: c)
       
    24     |/
       
    25     o  1 b (tag: b)
       
    26     |
       
    27     o  0 a (tag: a)
       
    28 
       
    29 For root nodes (nodes without parents) in the graph, they can be revsets
       
    30 pointing to existing nodes.  The ASCII graph could also have disconnected
       
    31 components with same names referring to the same changeset.
       
    32 
       
    33 Therefore, given the repo having the 4 changesets (and tags) above, with the
       
    34 following ASCII graph as input::
       
    35 
       
    36     foo    bar       bar  foo
       
    37      |     /          |    |
       
    38     ancestor(c,d)     a   baz
       
    39 
       
    40 The result (`hg log -G -T "{desc}"`) will look like::
       
    41 
       
    42     o    foo
       
    43     |\
       
    44     +---o  bar
       
    45     | | |
       
    46     | o |  baz
       
    47     |  /
       
    48     +---o  d
       
    49     | |
       
    50     +---o  c
       
    51     | |
       
    52     o |  b
       
    53     |/
       
    54     o  a
       
    55 
       
    56 Note that if you take the above `hg log` output directly as input. It will work
       
    57 as expected - the result would be an isomorphic graph::
       
    58 
       
    59     o    foo
       
    60     |\
       
    61     | | o  d
       
    62     | |/
       
    63     | | o  c
       
    64     | |/
       
    65     | | o  bar
       
    66     | |/|
       
    67     | o |  b
       
    68     | |/
       
    69     o /  baz
       
    70      /
       
    71     o  a
       
    72 
       
    73 This is because 'o' is specially handled in the input: instead of using 'o' as
       
    74 the node name, the word to the right will be used.
       
    75 """
       
    76 from __future__ import absolute_import, print_function
       
    77 
       
    78 import collections
       
    79 import itertools
       
    80 
       
    81 from mercurial.i18n import _
       
    82 from mercurial import (
       
    83     cmdutil,
       
    84     context,
       
    85     error,
       
    86     node,
       
    87     scmutil,
       
    88 )
       
    89 
       
    90 cmdtable = {}
       
    91 command = cmdutil.command(cmdtable)
       
    92 
       
    93 _pipechars = '\\/+-|'
       
    94 _nonpipechars = ''.join(chr(i) for i in xrange(33, 127)
       
    95                         if chr(i) not in _pipechars)
       
    96 
       
    97 def _isname(ch):
       
    98     """char -> bool. return True if ch looks like part of a name, False
       
    99     otherwise"""
       
   100     return ch in _nonpipechars
       
   101 
       
   102 def _parseasciigraph(text):
       
   103     """str -> {str : [str]}. convert the ASCII graph to edges"""
       
   104     lines = text.splitlines()
       
   105     edges = collections.defaultdict(list)  # {node: []}
       
   106 
       
   107     def get(y, x):
       
   108         """(int, int) -> char. give a coordinate, return the char. return a
       
   109         space for anything out of range"""
       
   110         if x < 0 or y < 0:
       
   111             return ' '
       
   112         try:
       
   113             return lines[y][x]
       
   114         except IndexError:
       
   115             return ' '
       
   116 
       
   117     def getname(y, x):
       
   118         """(int, int) -> str. like get(y, x) but concatenate left and right
       
   119         parts. if name is an 'o', try to replace it to the right"""
       
   120         result = ''
       
   121         for i in itertools.count(0):
       
   122             ch = get(y, x - i)
       
   123             if not _isname(ch):
       
   124                 break
       
   125             result = ch + result
       
   126         for i in itertools.count(1):
       
   127             ch = get(y, x + i)
       
   128             if not _isname(ch):
       
   129                 break
       
   130             result += ch
       
   131         if result == 'o':
       
   132             # special handling, find the name to the right
       
   133             result = ''
       
   134             for i in itertools.count(2):
       
   135                 ch = get(y, x + i)
       
   136                 if ch == ' ' or ch in _pipechars:
       
   137                     if result or x + i >= len(lines[y]):
       
   138                         break
       
   139                 else:
       
   140                     result += ch
       
   141             return result or 'o'
       
   142         return result
       
   143 
       
   144     def parents(y, x):
       
   145         """(int, int) -> [str]. follow the ASCII edges at given position,
       
   146         return a list of parents"""
       
   147         visited = set([(y, x)])
       
   148         visit = []
       
   149         result = []
       
   150 
       
   151         def follow(y, x, expected):
       
   152             """conditionally append (y, x) to visit array, if it's a char
       
   153             in excepted. 'o' in expected means an '_isname' test.
       
   154             if '-' (or '+') is not in excepted, and get(y, x) is '-' (or '+'),
       
   155             the next line (y + 1, x) will be checked instead."""
       
   156             ch = get(y, x)
       
   157             if any(ch == c and c not in expected for c in '-+'):
       
   158                 y += 1
       
   159                 return follow(y + 1, x, expected)
       
   160             if ch in expected or ('o' in expected and _isname(ch)):
       
   161                 visit.append((y, x))
       
   162 
       
   163         #  -o-  # starting point:
       
   164         #  /|\ # follow '-' (horizontally), and '/|\' (to the bottom)
       
   165         follow(y + 1, x, '|')
       
   166         follow(y + 1, x - 1, '/')
       
   167         follow(y + 1, x + 1, '\\')
       
   168         follow(y, x - 1, '-')
       
   169         follow(y, x + 1, '-')
       
   170 
       
   171         while visit:
       
   172             y, x = visit.pop()
       
   173             if (y, x) in visited:
       
   174                 continue
       
   175             visited.add((y, x))
       
   176             ch = get(y, x)
       
   177             if _isname(ch):
       
   178                 result.append(getname(y, x))
       
   179                 continue
       
   180             elif ch == '|':
       
   181                 follow(y + 1, x, '/|o')
       
   182                 follow(y + 1, x - 1, '/')
       
   183                 follow(y + 1, x + 1, '\\')
       
   184             elif ch == '+':
       
   185                 follow(y, x - 1, '-')
       
   186                 follow(y, x + 1, '-')
       
   187                 follow(y + 1, x - 1, '/')
       
   188                 follow(y + 1, x + 1, '\\')
       
   189                 follow(y + 1, x, '|')
       
   190             elif ch == '\\':
       
   191                 follow(y + 1, x + 1, '\\|o')
       
   192             elif ch == '/':
       
   193                 follow(y + 1, x - 1, '/|o')
       
   194             elif ch == '-':
       
   195                 follow(y, x - 1, '-+o')
       
   196                 follow(y, x + 1, '-+o')
       
   197         return result
       
   198 
       
   199     for y, line in enumerate(lines):
       
   200         for x, ch in enumerate(line):
       
   201             if ch == '#':  # comment
       
   202                 break
       
   203             if _isname(ch):
       
   204                 edges[getname(y, x)] += parents(y, x)
       
   205 
       
   206     return dict(edges)
       
   207 
       
   208 class simplefilectx(object):
       
   209     def __init__(self, path, data):
       
   210         self._data = data
       
   211         self._path = path
       
   212 
       
   213     def data(self):
       
   214         return self._data
       
   215 
       
   216     def path(self):
       
   217         return self._path
       
   218 
       
   219     def renamed(self):
       
   220         return None
       
   221 
       
   222     def flags(self):
       
   223         return ''
       
   224 
       
   225 class simplecommitctx(context.committablectx):
       
   226     def __init__(self, repo, name, parentctxs, added=None):
       
   227         opts = {
       
   228             'changes': scmutil.status([], added or [], [], [], [], [], []),
       
   229             'date': '0 0',
       
   230             'extra': {'branch': 'default'},
       
   231         }
       
   232         super(simplecommitctx, self).__init__(self, name, **opts)
       
   233         self._repo = repo
       
   234         self._name = name
       
   235         self._parents = parentctxs
       
   236         self._parents.sort(key=lambda c: c.node())
       
   237         while len(self._parents) < 2:
       
   238             self._parents.append(repo[node.nullid])
       
   239 
       
   240     def filectx(self, key):
       
   241         return simplefilectx(key, self._name)
       
   242 
       
   243     def commit(self):
       
   244         return self._repo.commitctx(self)
       
   245 
       
   246 def _walkgraph(edges):
       
   247     """yield node, parents in topologically order"""
       
   248     visible = set(edges.keys())
       
   249     remaining = {}  # {str: [str]}
       
   250     for k, vs in edges.iteritems():
       
   251         for v in vs:
       
   252             if v not in remaining:
       
   253                 remaining[v] = []
       
   254         remaining[k] = vs[:]
       
   255     while remaining:
       
   256         leafs = [k for k, v in remaining.items() if not v]
       
   257         if not leafs:
       
   258             raise error.Abort(_('the graph has cycles'))
       
   259         for leaf in sorted(leafs):
       
   260             if leaf in visible:
       
   261                 yield leaf, edges[leaf]
       
   262             del remaining[leaf]
       
   263             for k, v in remaining.iteritems():
       
   264                 if leaf in v:
       
   265                     v.remove(leaf)
       
   266 
       
   267 @command('debugdrawdag', [])
       
   268 def debugdrawdag(ui, repo, **opts):
       
   269     """read an ASCII graph from stdin and create changesets
       
   270 
       
   271     The ASCII graph is like what :hg:`log -G` outputs, with each `o` replaced
       
   272     to the name of the node. The command will create dummy changesets and local
       
   273     tags with those names to make the dummy changesets easier to be referred
       
   274     to.
       
   275 
       
   276     If the name of a node is a single character 'o', It will be replaced by the
       
   277     word to the right. This makes it easier to reuse
       
   278     :hg:`log -G -T '{desc}'` outputs.
       
   279 
       
   280     For root (no parents) nodes, revset can be used to query existing repo.
       
   281     Note that the revset cannot have confusing characters which can be seen as
       
   282     the part of the graph edges, like `|/+-\`.
       
   283     """
       
   284     text = ui.fin.read()
       
   285 
       
   286     # parse the graph and make sure len(parents) <= 2 for each node
       
   287     edges = _parseasciigraph(text)
       
   288     for k, v in edges.iteritems():
       
   289         if len(v) > 2:
       
   290             raise error.Abort(_('%s: too many parents: %s')
       
   291                               % (k, ' '.join(v)))
       
   292 
       
   293     committed = {None: node.nullid}  # {name: node}
       
   294 
       
   295     # for leaf nodes, try to find existing nodes in repo
       
   296     for name, parents in edges.iteritems():
       
   297         if len(parents) == 0:
       
   298             try:
       
   299                 committed[name] = scmutil.revsingle(repo, name)
       
   300             except error.RepoLookupError:
       
   301                 pass
       
   302 
       
   303     # commit in topological order
       
   304     for name, parents in _walkgraph(edges):
       
   305         if name in committed:
       
   306             continue
       
   307         pctxs = [repo[committed[n]] for n in parents]
       
   308         ctx = simplecommitctx(repo, name, pctxs, [name])
       
   309         n = ctx.commit()
       
   310         committed[name] = n
       
   311         repo.tag(name, n, message=None, user=None, date=None, local=True)