--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/drawdag.py Wed Nov 09 16:01:34 2016 +0000
@@ -0,0 +1,311 @@
+# drawdag.py - convert ASCII revision DAG to actual changesets
+#
+# Copyright 2016 Facebook, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+"""
+create changesets from an ASCII graph for testing purpose.
+
+For example, given the following input::
+
+ c d
+ |/
+ b
+ |
+ a
+
+4 changesets and 4 local tags will be created.
+`hg log -G -T "{rev} {desc} (tag: {tags})"` will output::
+
+ o 3 d (tag: d tip)
+ |
+ | o 2 c (tag: c)
+ |/
+ o 1 b (tag: b)
+ |
+ o 0 a (tag: a)
+
+For root nodes (nodes without parents) in the graph, they can be revsets
+pointing to existing nodes. The ASCII graph could also have disconnected
+components with same names referring to the same changeset.
+
+Therefore, given the repo having the 4 changesets (and tags) above, with the
+following ASCII graph as input::
+
+ foo bar bar foo
+ | / | |
+ ancestor(c,d) a baz
+
+The result (`hg log -G -T "{desc}"`) will look like::
+
+ o foo
+ |\
+ +---o bar
+ | | |
+ | o | baz
+ | /
+ +---o d
+ | |
+ +---o c
+ | |
+ o | b
+ |/
+ o a
+
+Note that if you take the above `hg log` output directly as input. It will work
+as expected - the result would be an isomorphic graph::
+
+ o foo
+ |\
+ | | o d
+ | |/
+ | | o c
+ | |/
+ | | o bar
+ | |/|
+ | o | b
+ | |/
+ o / baz
+ /
+ o a
+
+This is because 'o' is specially handled in the input: instead of using 'o' as
+the node name, the word to the right will be used.
+"""
+from __future__ import absolute_import, print_function
+
+import collections
+import itertools
+
+from mercurial.i18n import _
+from mercurial import (
+ cmdutil,
+ context,
+ error,
+ node,
+ scmutil,
+)
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+_pipechars = '\\/+-|'
+_nonpipechars = ''.join(chr(i) for i in xrange(33, 127)
+ if chr(i) not in _pipechars)
+
+def _isname(ch):
+ """char -> bool. return True if ch looks like part of a name, False
+ otherwise"""
+ return ch in _nonpipechars
+
+def _parseasciigraph(text):
+ """str -> {str : [str]}. convert the ASCII graph to edges"""
+ lines = text.splitlines()
+ edges = collections.defaultdict(list) # {node: []}
+
+ def get(y, x):
+ """(int, int) -> char. give a coordinate, return the char. return a
+ space for anything out of range"""
+ if x < 0 or y < 0:
+ return ' '
+ try:
+ return lines[y][x]
+ except IndexError:
+ return ' '
+
+ def getname(y, x):
+ """(int, int) -> str. like get(y, x) but concatenate left and right
+ parts. if name is an 'o', try to replace it to the right"""
+ result = ''
+ for i in itertools.count(0):
+ ch = get(y, x - i)
+ if not _isname(ch):
+ break
+ result = ch + result
+ for i in itertools.count(1):
+ ch = get(y, x + i)
+ if not _isname(ch):
+ break
+ result += ch
+ if result == 'o':
+ # special handling, find the name to the right
+ result = ''
+ for i in itertools.count(2):
+ ch = get(y, x + i)
+ if ch == ' ' or ch in _pipechars:
+ if result or x + i >= len(lines[y]):
+ break
+ else:
+ result += ch
+ return result or 'o'
+ return result
+
+ def parents(y, x):
+ """(int, int) -> [str]. follow the ASCII edges at given position,
+ return a list of parents"""
+ visited = set([(y, x)])
+ visit = []
+ result = []
+
+ def follow(y, x, expected):
+ """conditionally append (y, x) to visit array, if it's a char
+ in excepted. 'o' in expected means an '_isname' test.
+ if '-' (or '+') is not in excepted, and get(y, x) is '-' (or '+'),
+ the next line (y + 1, x) will be checked instead."""
+ ch = get(y, x)
+ if any(ch == c and c not in expected for c in '-+'):
+ y += 1
+ return follow(y + 1, x, expected)
+ if ch in expected or ('o' in expected and _isname(ch)):
+ visit.append((y, x))
+
+ # -o- # starting point:
+ # /|\ # follow '-' (horizontally), and '/|\' (to the bottom)
+ follow(y + 1, x, '|')
+ follow(y + 1, x - 1, '/')
+ follow(y + 1, x + 1, '\\')
+ follow(y, x - 1, '-')
+ follow(y, x + 1, '-')
+
+ while visit:
+ y, x = visit.pop()
+ if (y, x) in visited:
+ continue
+ visited.add((y, x))
+ ch = get(y, x)
+ if _isname(ch):
+ result.append(getname(y, x))
+ continue
+ elif ch == '|':
+ follow(y + 1, x, '/|o')
+ follow(y + 1, x - 1, '/')
+ follow(y + 1, x + 1, '\\')
+ elif ch == '+':
+ follow(y, x - 1, '-')
+ follow(y, x + 1, '-')
+ follow(y + 1, x - 1, '/')
+ follow(y + 1, x + 1, '\\')
+ follow(y + 1, x, '|')
+ elif ch == '\\':
+ follow(y + 1, x + 1, '\\|o')
+ elif ch == '/':
+ follow(y + 1, x - 1, '/|o')
+ elif ch == '-':
+ follow(y, x - 1, '-+o')
+ follow(y, x + 1, '-+o')
+ return result
+
+ for y, line in enumerate(lines):
+ for x, ch in enumerate(line):
+ if ch == '#': # comment
+ break
+ if _isname(ch):
+ edges[getname(y, x)] += parents(y, x)
+
+ return dict(edges)
+
+class simplefilectx(object):
+ def __init__(self, path, data):
+ self._data = data
+ self._path = path
+
+ def data(self):
+ return self._data
+
+ def path(self):
+ return self._path
+
+ def renamed(self):
+ return None
+
+ def flags(self):
+ return ''
+
+class simplecommitctx(context.committablectx):
+ def __init__(self, repo, name, parentctxs, added=None):
+ opts = {
+ 'changes': scmutil.status([], added or [], [], [], [], [], []),
+ 'date': '0 0',
+ 'extra': {'branch': 'default'},
+ }
+ super(simplecommitctx, self).__init__(self, name, **opts)
+ self._repo = repo
+ self._name = name
+ self._parents = parentctxs
+ self._parents.sort(key=lambda c: c.node())
+ while len(self._parents) < 2:
+ self._parents.append(repo[node.nullid])
+
+ def filectx(self, key):
+ return simplefilectx(key, self._name)
+
+ def commit(self):
+ return self._repo.commitctx(self)
+
+def _walkgraph(edges):
+ """yield node, parents in topologically order"""
+ visible = set(edges.keys())
+ remaining = {} # {str: [str]}
+ for k, vs in edges.iteritems():
+ for v in vs:
+ if v not in remaining:
+ remaining[v] = []
+ remaining[k] = vs[:]
+ while remaining:
+ leafs = [k for k, v in remaining.items() if not v]
+ if not leafs:
+ raise error.Abort(_('the graph has cycles'))
+ for leaf in sorted(leafs):
+ if leaf in visible:
+ yield leaf, edges[leaf]
+ del remaining[leaf]
+ for k, v in remaining.iteritems():
+ if leaf in v:
+ v.remove(leaf)
+
+@command('debugdrawdag', [])
+def debugdrawdag(ui, repo, **opts):
+ """read an ASCII graph from stdin and create changesets
+
+ The ASCII graph is like what :hg:`log -G` outputs, with each `o` replaced
+ to the name of the node. The command will create dummy changesets and local
+ tags with those names to make the dummy changesets easier to be referred
+ to.
+
+ If the name of a node is a single character 'o', It will be replaced by the
+ word to the right. This makes it easier to reuse
+ :hg:`log -G -T '{desc}'` outputs.
+
+ For root (no parents) nodes, revset can be used to query existing repo.
+ Note that the revset cannot have confusing characters which can be seen as
+ the part of the graph edges, like `|/+-\`.
+ """
+ text = ui.fin.read()
+
+ # parse the graph and make sure len(parents) <= 2 for each node
+ edges = _parseasciigraph(text)
+ for k, v in edges.iteritems():
+ if len(v) > 2:
+ raise error.Abort(_('%s: too many parents: %s')
+ % (k, ' '.join(v)))
+
+ committed = {None: node.nullid} # {name: node}
+
+ # for leaf nodes, try to find existing nodes in repo
+ for name, parents in edges.iteritems():
+ if len(parents) == 0:
+ try:
+ committed[name] = scmutil.revsingle(repo, name)
+ except error.RepoLookupError:
+ pass
+
+ # commit in topological order
+ for name, parents in _walkgraph(edges):
+ if name in committed:
+ continue
+ pctxs = [repo[committed[n]] for n in parents]
+ ctx = simplecommitctx(repo, name, pctxs, [name])
+ n = ctx.commit()
+ committed[name] = n
+ repo.tag(name, n, message=None, user=None, date=None, local=True)