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