Show More
@@ -0,0 +1,311 b'' | |||||
|
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) |
General Comments 0
You need to be logged in to leave comments.
Login now