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