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