##// END OF EJS Templates
drawdag: port to python 3
Augie Fackler -
r34204:5a1b4126 default
parent child Browse files
Show More
@@ -1,433 +1,434 b''
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 86 import itertools
87 87 import re
88 88
89 89 from mercurial.i18n import _
90 90 from mercurial import (
91 91 context,
92 92 error,
93 93 node,
94 94 obsolete,
95 pycompat,
95 96 registrar,
96 97 scmutil,
97 98 tags as tagsmod,
98 99 )
99 100
100 101 cmdtable = {}
101 102 command = registrar.command(cmdtable)
102 103
103 _pipechars = '\\/+-|'
104 _nonpipechars = ''.join(chr(i) for i in xrange(33, 127)
105 if chr(i) not in _pipechars)
104 _pipechars = b'\\/+-|'
105 _nonpipechars = b''.join(pycompat.bytechr(i) for i in range(33, 127)
106 if pycompat.bytechr(i) not in _pipechars)
106 107
107 108 def _isname(ch):
108 109 """char -> bool. return True if ch looks like part of a name, False
109 110 otherwise"""
110 111 return ch in _nonpipechars
111 112
112 113 def _parseasciigraph(text):
113 114 r"""str -> {str : [str]}. convert the ASCII graph to edges
114 115
115 116 >>> import pprint
116 >>> pprint.pprint({k: [vv for vv in v]
117 >>> pprint.pprint({pycompat.sysstr(k): [pycompat.sysstr(vv) for vv in v]
117 118 ... for k, v in _parseasciigraph(br'''
118 119 ... G
119 120 ... |
120 121 ... I D C F # split: B -> E, F, G
121 122 ... \ \| | # replace: C -> D -> H
122 123 ... H B E # prune: F, I
123 124 ... \|/
124 125 ... A
125 126 ... ''').items()})
126 127 {'A': [],
127 128 'B': ['A'],
128 129 'C': ['B'],
129 130 'D': ['B'],
130 131 'E': ['A'],
131 132 'F': ['E'],
132 133 'G': ['F'],
133 134 'H': ['A'],
134 135 'I': ['H']}
135 >>> pprint.pprint({k: [vv for vv in v]
136 >>> pprint.pprint({pycompat.sysstr(k): [pycompat.sysstr(vv) for vv in v]
136 137 ... for k, v in _parseasciigraph(br'''
137 138 ... o foo
138 139 ... |\
139 140 ... +---o bar
140 141 ... | | |
141 142 ... | o | baz
142 143 ... | /
143 144 ... +---o d
144 145 ... | |
145 146 ... +---o c
146 147 ... | |
147 148 ... o | b
148 149 ... |/
149 150 ... o a
150 151 ... ''').items()})
151 152 {'a': [],
152 153 'b': ['a'],
153 154 'bar': ['b', 'a'],
154 155 'baz': [],
155 156 'c': ['b'],
156 157 'd': ['b'],
157 158 'foo': ['baz', 'b']}
158 159 """
159 160 lines = text.splitlines()
160 161 edges = collections.defaultdict(list) # {node: []}
161 162
162 163 def get(y, x):
163 164 """(int, int) -> char. give a coordinate, return the char. return a
164 165 space for anything out of range"""
165 166 if x < 0 or y < 0:
166 return ' '
167 return b' '
167 168 try:
168 return lines[y][x]
169 return lines[y][x:x + 1] or b' '
169 170 except IndexError:
170 return ' '
171 return b' '
171 172
172 173 def getname(y, x):
173 174 """(int, int) -> str. like get(y, x) but concatenate left and right
174 175 parts. if name is an 'o', try to replace it to the right"""
175 result = ''
176 result = b''
176 177 for i in itertools.count(0):
177 178 ch = get(y, x - i)
178 179 if not _isname(ch):
179 180 break
180 181 result = ch + result
181 182 for i in itertools.count(1):
182 183 ch = get(y, x + i)
183 184 if not _isname(ch):
184 185 break
185 186 result += ch
186 if result == 'o':
187 if result == b'o':
187 188 # special handling, find the name to the right
188 result = ''
189 result = b''
189 190 for i in itertools.count(2):
190 191 ch = get(y, x + i)
191 if ch == ' ' or ch in _pipechars:
192 if ch == b' ' or ch in _pipechars:
192 193 if result or x + i >= len(lines[y]):
193 194 break
194 195 else:
195 196 result += ch
196 return result or 'o'
197 return result or b'o'
197 198 return result
198 199
199 200 def parents(y, x):
200 201 """(int, int) -> [str]. follow the ASCII edges at given position,
201 202 return a list of parents"""
202 203 visited = {(y, x)}
203 204 visit = []
204 205 result = []
205 206
206 207 def follow(y, x, expected):
207 208 """conditionally append (y, x) to visit array, if it's a char
208 209 in excepted. 'o' in expected means an '_isname' test.
209 210 if '-' (or '+') is not in excepted, and get(y, x) is '-' (or '+'),
210 211 the next line (y + 1, x) will be checked instead."""
211 212 ch = get(y, x)
212 if any(ch == c and c not in expected for c in '-+'):
213 if any(ch == c and c not in expected for c in (b'-', b'+')):
213 214 y += 1
214 215 return follow(y + 1, x, expected)
215 if ch in expected or ('o' in expected and _isname(ch)):
216 if ch in expected or (b'o' in expected and _isname(ch)):
216 217 visit.append((y, x))
217 218
218 219 # -o- # starting point:
219 220 # /|\ # follow '-' (horizontally), and '/|\' (to the bottom)
220 follow(y + 1, x, '|')
221 follow(y + 1, x - 1, '/')
222 follow(y + 1, x + 1, '\\')
223 follow(y, x - 1, '-')
224 follow(y, x + 1, '-')
221 follow(y + 1, x, b'|')
222 follow(y + 1, x - 1, b'/')
223 follow(y + 1, x + 1, b'\\')
224 follow(y, x - 1, b'-')
225 follow(y, x + 1, b'-')
225 226
226 227 while visit:
227 228 y, x = visit.pop()
228 229 if (y, x) in visited:
229 230 continue
230 231 visited.add((y, x))
231 232 ch = get(y, x)
232 233 if _isname(ch):
233 234 result.append(getname(y, x))
234 235 continue
235 elif ch == '|':
236 follow(y + 1, x, '/|o')
237 follow(y + 1, x - 1, '/')
238 follow(y + 1, x + 1, '\\')
239 elif ch == '+':
240 follow(y, x - 1, '-')
241 follow(y, x + 1, '-')
242 follow(y + 1, x - 1, '/')
243 follow(y + 1, x + 1, '\\')
244 follow(y + 1, x, '|')
245 elif ch == '\\':
246 follow(y + 1, x + 1, '\\|o')
247 elif ch == '/':
248 follow(y + 1, x - 1, '/|o')
249 elif ch == '-':
250 follow(y, x - 1, '-+o')
251 follow(y, x + 1, '-+o')
236 elif ch == b'|':
237 follow(y + 1, x, b'/|o')
238 follow(y + 1, x - 1, b'/')
239 follow(y + 1, x + 1, b'\\')
240 elif ch == b'+':
241 follow(y, x - 1, b'-')
242 follow(y, x + 1, b'-')
243 follow(y + 1, x - 1, b'/')
244 follow(y + 1, x + 1, b'\\')
245 follow(y + 1, x, b'|')
246 elif ch == b'\\':
247 follow(y + 1, x + 1, b'\\|o')
248 elif ch == b'/':
249 follow(y + 1, x - 1, b'/|o')
250 elif ch == b'-':
251 follow(y, x - 1, b'-+o')
252 follow(y, x + 1, b'-+o')
252 253 return result
253 254
254 255 for y, line in enumerate(lines):
255 for x, ch in enumerate(line):
256 if ch == '#': # comment
256 for x, ch in enumerate(pycompat.bytestr(line)):
257 if ch == b'#': # comment
257 258 break
258 259 if _isname(ch):
259 260 edges[getname(y, x)] += parents(y, x)
260 261
261 262 return dict(edges)
262 263
263 264 class simplefilectx(object):
264 265 def __init__(self, path, data):
265 266 self._data = data
266 267 self._path = path
267 268
268 269 def data(self):
269 270 return self._data
270 271
271 272 def filenode(self):
272 273 return None
273 274
274 275 def path(self):
275 276 return self._path
276 277
277 278 def renamed(self):
278 279 return None
279 280
280 281 def flags(self):
281 return ''
282 return b''
282 283
283 284 class simplecommitctx(context.committablectx):
284 285 def __init__(self, repo, name, parentctxs, added):
285 286 opts = {
286 287 'changes': scmutil.status([], list(added), [], [], [], [], []),
287 'date': '0 0',
288 'extra': {'branch': 'default'},
288 'date': b'0 0',
289 'extra': {b'branch': b'default'},
289 290 }
290 291 super(simplecommitctx, self).__init__(self, name, **opts)
291 292 self._repo = repo
292 293 self._added = added
293 294 self._parents = parentctxs
294 295 while len(self._parents) < 2:
295 296 self._parents.append(repo[node.nullid])
296 297
297 298 def filectx(self, key):
298 299 return simplefilectx(key, self._added[key])
299 300
300 301 def commit(self):
301 302 return self._repo.commitctx(self)
302 303
303 304 def _walkgraph(edges):
304 305 """yield node, parents in topologically order"""
305 306 visible = set(edges.keys())
306 307 remaining = {} # {str: [str]}
307 for k, vs in edges.iteritems():
308 for k, vs in edges.items():
308 309 for v in vs:
309 310 if v not in remaining:
310 311 remaining[v] = []
311 312 remaining[k] = vs[:]
312 313 while remaining:
313 314 leafs = [k for k, v in remaining.items() if not v]
314 315 if not leafs:
315 316 raise error.Abort(_('the graph has cycles'))
316 317 for leaf in sorted(leafs):
317 318 if leaf in visible:
318 319 yield leaf, edges[leaf]
319 320 del remaining[leaf]
320 for k, v in remaining.iteritems():
321 for k, v in remaining.items():
321 322 if leaf in v:
322 323 v.remove(leaf)
323 324
324 325 def _getcomments(text):
325 326 """
326 >>> [s for s in _getcomments(br'''
327 >>> [pycompat.sysstr(s) for s in _getcomments(br'''
327 328 ... G
328 329 ... |
329 330 ... I D C F # split: B -> E, F, G
330 331 ... \ \| | # replace: C -> D -> H
331 332 ... H B E # prune: F, I
332 333 ... \|/
333 334 ... A
334 335 ... ''')]
335 336 ['split: B -> E, F, G', 'replace: C -> D -> H', 'prune: F, I']
336 337 """
337 338 for line in text.splitlines():
338 if ' # ' not in line:
339 if b' # ' not in line:
339 340 continue
340 yield line.split(' # ', 1)[1].split(' # ')[0].strip()
341 yield line.split(b' # ', 1)[1].split(b' # ')[0].strip()
341 342
342 @command('debugdrawdag', [])
343 @command(b'debugdrawdag', [])
343 344 def debugdrawdag(ui, repo, **opts):
344 345 """read an ASCII graph from stdin and create changesets
345 346
346 347 The ASCII graph is like what :hg:`log -G` outputs, with each `o` replaced
347 348 to the name of the node. The command will create dummy changesets and local
348 349 tags with those names to make the dummy changesets easier to be referred
349 350 to.
350 351
351 352 If the name of a node is a single character 'o', It will be replaced by the
352 353 word to the right. This makes it easier to reuse
353 354 :hg:`log -G -T '{desc}'` outputs.
354 355
355 356 For root (no parents) nodes, revset can be used to query existing repo.
356 357 Note that the revset cannot have confusing characters which can be seen as
357 358 the part of the graph edges, like `|/+-\`.
358 359 """
359 360 text = ui.fin.read()
360 361
361 362 # parse the graph and make sure len(parents) <= 2 for each node
362 363 edges = _parseasciigraph(text)
363 for k, v in edges.iteritems():
364 for k, v in edges.items():
364 365 if len(v) > 2:
365 366 raise error.Abort(_('%s: too many parents: %s')
366 % (k, ' '.join(v)))
367 % (k, b' '.join(v)))
367 368
368 369 # parse comments to get extra file content instructions
369 370 files = collections.defaultdict(dict) # {(name, path): content}
370 371 comments = list(_getcomments(text))
371 filere = re.compile(r'^(\w+)/([\w/]+)\s*=\s*(.*)$', re.M)
372 for name, path, content in filere.findall('\n'.join(comments)):
373 files[name][path] = content.replace(r'\n', '\n')
372 filere = re.compile(br'^(\w+)/([\w/]+)\s*=\s*(.*)$', re.M)
373 for name, path, content in filere.findall(b'\n'.join(comments)):
374 files[name][path] = content.replace(br'\n', b'\n')
374 375
375 376 committed = {None: node.nullid} # {name: node}
376 377
377 378 # for leaf nodes, try to find existing nodes in repo
378 for name, parents in edges.iteritems():
379 for name, parents in edges.items():
379 380 if len(parents) == 0:
380 381 try:
381 382 committed[name] = scmutil.revsingle(repo, name)
382 383 except error.RepoLookupError:
383 384 pass
384 385
385 386 # commit in topological order
386 387 for name, parents in _walkgraph(edges):
387 388 if name in committed:
388 389 continue
389 390 pctxs = [repo[committed[n]] for n in parents]
390 391 pctxs.sort(key=lambda c: c.node())
391 392 added = {}
392 393 if len(parents) > 1:
393 394 # If it's a merge, take the files and contents from the parents
394 395 for f in pctxs[1].manifest():
395 396 if f not in pctxs[0].manifest():
396 397 added[f] = pctxs[1][f].data()
397 398 else:
398 399 # If it's not a merge, add a single file
399 400 added[name] = name
400 401 # add extra file contents in comments
401 402 for path, content in files.get(name, {}).items():
402 403 added[path] = content
403 404 ctx = simplecommitctx(repo, name, pctxs, added)
404 405 n = ctx.commit()
405 406 committed[name] = n
406 407 tagsmod.tag(repo, [name], n, message=None, user=None, date=None,
407 408 local=True)
408 409
409 410 # handle special comments
410 with repo.wlock(), repo.lock(), repo.transaction('drawdag'):
411 with repo.wlock(), repo.lock(), repo.transaction(b'drawdag'):
411 412 getctx = lambda x: repo.unfiltered()[committed[x.strip()]]
412 413 for comment in comments:
413 414 rels = [] # obsolete relationships
414 args = comment.split(':', 1)
415 args = comment.split(b':', 1)
415 416 if len(args) <= 1:
416 417 continue
417 418
418 419 cmd = args[0].strip()
419 420 arg = args[1].strip()
420 421
421 if cmd in ('replace', 'rebase', 'amend'):
422 nodes = [getctx(m) for m in arg.split('->')]
422 if cmd in (b'replace', b'rebase', b'amend'):
423 nodes = [getctx(m) for m in arg.split(b'->')]
423 424 for i in range(len(nodes) - 1):
424 425 rels.append((nodes[i], (nodes[i + 1],)))
425 elif cmd in ('split',):
426 pre, succs = arg.split('->')
427 succs = succs.split(',')
426 elif cmd in (b'split',):
427 pre, succs = arg.split(b'->')
428 succs = succs.split(b',')
428 429 rels.append((getctx(pre), (getctx(s) for s in succs)))
429 elif cmd in ('prune',):
430 for n in arg.split(','):
430 elif cmd in (b'prune',):
431 for n in arg.split(b','):
431 432 rels.append((getctx(n), ()))
432 433 if rels:
433 434 obsolete.createmarkers(repo, rels, date=(0, 0), operation=cmd)
General Comments 0
You need to be logged in to leave comments. Login now