##// END OF EJS Templates
drawdag: provide filenode for its dummy filectx...
Jun Wu -
r32305:91105798 default
parent child Browse files
Show More
@@ -1,313 +1,316 b''
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 from __future__ import absolute_import, print_function
76 from __future__ import absolute_import, print_function
77
77
78 import collections
78 import collections
79 import itertools
79 import itertools
80
80
81 from mercurial.i18n import _
81 from mercurial.i18n import _
82 from mercurial import (
82 from mercurial import (
83 cmdutil,
83 cmdutil,
84 context,
84 context,
85 error,
85 error,
86 node,
86 node,
87 scmutil,
87 scmutil,
88 tags as tagsmod,
88 tags as tagsmod,
89 )
89 )
90
90
91 cmdtable = {}
91 cmdtable = {}
92 command = cmdutil.command(cmdtable)
92 command = cmdutil.command(cmdtable)
93
93
94 _pipechars = '\\/+-|'
94 _pipechars = '\\/+-|'
95 _nonpipechars = ''.join(chr(i) for i in xrange(33, 127)
95 _nonpipechars = ''.join(chr(i) for i in xrange(33, 127)
96 if chr(i) not in _pipechars)
96 if chr(i) not in _pipechars)
97
97
98 def _isname(ch):
98 def _isname(ch):
99 """char -> bool. return True if ch looks like part of a name, False
99 """char -> bool. return True if ch looks like part of a name, False
100 otherwise"""
100 otherwise"""
101 return ch in _nonpipechars
101 return ch in _nonpipechars
102
102
103 def _parseasciigraph(text):
103 def _parseasciigraph(text):
104 """str -> {str : [str]}. convert the ASCII graph to edges"""
104 """str -> {str : [str]}. convert the ASCII graph to edges"""
105 lines = text.splitlines()
105 lines = text.splitlines()
106 edges = collections.defaultdict(list) # {node: []}
106 edges = collections.defaultdict(list) # {node: []}
107
107
108 def get(y, x):
108 def get(y, x):
109 """(int, int) -> char. give a coordinate, return the char. return a
109 """(int, int) -> char. give a coordinate, return the char. return a
110 space for anything out of range"""
110 space for anything out of range"""
111 if x < 0 or y < 0:
111 if x < 0 or y < 0:
112 return ' '
112 return ' '
113 try:
113 try:
114 return lines[y][x]
114 return lines[y][x]
115 except IndexError:
115 except IndexError:
116 return ' '
116 return ' '
117
117
118 def getname(y, x):
118 def getname(y, x):
119 """(int, int) -> str. like get(y, x) but concatenate left and right
119 """(int, int) -> str. like get(y, x) but concatenate left and right
120 parts. if name is an 'o', try to replace it to the right"""
120 parts. if name is an 'o', try to replace it to the right"""
121 result = ''
121 result = ''
122 for i in itertools.count(0):
122 for i in itertools.count(0):
123 ch = get(y, x - i)
123 ch = get(y, x - i)
124 if not _isname(ch):
124 if not _isname(ch):
125 break
125 break
126 result = ch + result
126 result = ch + result
127 for i in itertools.count(1):
127 for i in itertools.count(1):
128 ch = get(y, x + i)
128 ch = get(y, x + i)
129 if not _isname(ch):
129 if not _isname(ch):
130 break
130 break
131 result += ch
131 result += ch
132 if result == 'o':
132 if result == 'o':
133 # special handling, find the name to the right
133 # special handling, find the name to the right
134 result = ''
134 result = ''
135 for i in itertools.count(2):
135 for i in itertools.count(2):
136 ch = get(y, x + i)
136 ch = get(y, x + i)
137 if ch == ' ' or ch in _pipechars:
137 if ch == ' ' or ch in _pipechars:
138 if result or x + i >= len(lines[y]):
138 if result or x + i >= len(lines[y]):
139 break
139 break
140 else:
140 else:
141 result += ch
141 result += ch
142 return result or 'o'
142 return result or 'o'
143 return result
143 return result
144
144
145 def parents(y, x):
145 def parents(y, x):
146 """(int, int) -> [str]. follow the ASCII edges at given position,
146 """(int, int) -> [str]. follow the ASCII edges at given position,
147 return a list of parents"""
147 return a list of parents"""
148 visited = {(y, x)}
148 visited = {(y, x)}
149 visit = []
149 visit = []
150 result = []
150 result = []
151
151
152 def follow(y, x, expected):
152 def follow(y, x, expected):
153 """conditionally append (y, x) to visit array, if it's a char
153 """conditionally append (y, x) to visit array, if it's a char
154 in excepted. 'o' in expected means an '_isname' test.
154 in excepted. 'o' in expected means an '_isname' test.
155 if '-' (or '+') is not in excepted, and get(y, x) is '-' (or '+'),
155 if '-' (or '+') is not in excepted, and get(y, x) is '-' (or '+'),
156 the next line (y + 1, x) will be checked instead."""
156 the next line (y + 1, x) will be checked instead."""
157 ch = get(y, x)
157 ch = get(y, x)
158 if any(ch == c and c not in expected for c in '-+'):
158 if any(ch == c and c not in expected for c in '-+'):
159 y += 1
159 y += 1
160 return follow(y + 1, x, expected)
160 return follow(y + 1, x, expected)
161 if ch in expected or ('o' in expected and _isname(ch)):
161 if ch in expected or ('o' in expected and _isname(ch)):
162 visit.append((y, x))
162 visit.append((y, x))
163
163
164 # -o- # starting point:
164 # -o- # starting point:
165 # /|\ # follow '-' (horizontally), and '/|\' (to the bottom)
165 # /|\ # follow '-' (horizontally), and '/|\' (to the bottom)
166 follow(y + 1, x, '|')
166 follow(y + 1, x, '|')
167 follow(y + 1, x - 1, '/')
167 follow(y + 1, x - 1, '/')
168 follow(y + 1, x + 1, '\\')
168 follow(y + 1, x + 1, '\\')
169 follow(y, x - 1, '-')
169 follow(y, x - 1, '-')
170 follow(y, x + 1, '-')
170 follow(y, x + 1, '-')
171
171
172 while visit:
172 while visit:
173 y, x = visit.pop()
173 y, x = visit.pop()
174 if (y, x) in visited:
174 if (y, x) in visited:
175 continue
175 continue
176 visited.add((y, x))
176 visited.add((y, x))
177 ch = get(y, x)
177 ch = get(y, x)
178 if _isname(ch):
178 if _isname(ch):
179 result.append(getname(y, x))
179 result.append(getname(y, x))
180 continue
180 continue
181 elif ch == '|':
181 elif ch == '|':
182 follow(y + 1, x, '/|o')
182 follow(y + 1, x, '/|o')
183 follow(y + 1, x - 1, '/')
183 follow(y + 1, x - 1, '/')
184 follow(y + 1, x + 1, '\\')
184 follow(y + 1, x + 1, '\\')
185 elif ch == '+':
185 elif ch == '+':
186 follow(y, x - 1, '-')
186 follow(y, x - 1, '-')
187 follow(y, x + 1, '-')
187 follow(y, x + 1, '-')
188 follow(y + 1, x - 1, '/')
188 follow(y + 1, x - 1, '/')
189 follow(y + 1, x + 1, '\\')
189 follow(y + 1, x + 1, '\\')
190 follow(y + 1, x, '|')
190 follow(y + 1, x, '|')
191 elif ch == '\\':
191 elif ch == '\\':
192 follow(y + 1, x + 1, '\\|o')
192 follow(y + 1, x + 1, '\\|o')
193 elif ch == '/':
193 elif ch == '/':
194 follow(y + 1, x - 1, '/|o')
194 follow(y + 1, x - 1, '/|o')
195 elif ch == '-':
195 elif ch == '-':
196 follow(y, x - 1, '-+o')
196 follow(y, x - 1, '-+o')
197 follow(y, x + 1, '-+o')
197 follow(y, x + 1, '-+o')
198 return result
198 return result
199
199
200 for y, line in enumerate(lines):
200 for y, line in enumerate(lines):
201 for x, ch in enumerate(line):
201 for x, ch in enumerate(line):
202 if ch == '#': # comment
202 if ch == '#': # comment
203 break
203 break
204 if _isname(ch):
204 if _isname(ch):
205 edges[getname(y, x)] += parents(y, x)
205 edges[getname(y, x)] += parents(y, x)
206
206
207 return dict(edges)
207 return dict(edges)
208
208
209 class simplefilectx(object):
209 class simplefilectx(object):
210 def __init__(self, path, data):
210 def __init__(self, path, data):
211 self._data = data
211 self._data = data
212 self._path = path
212 self._path = path
213
213
214 def data(self):
214 def data(self):
215 return self._data
215 return self._data
216
216
217 def filenode(self):
218 return None
219
217 def path(self):
220 def path(self):
218 return self._path
221 return self._path
219
222
220 def renamed(self):
223 def renamed(self):
221 return None
224 return None
222
225
223 def flags(self):
226 def flags(self):
224 return ''
227 return ''
225
228
226 class simplecommitctx(context.committablectx):
229 class simplecommitctx(context.committablectx):
227 def __init__(self, repo, name, parentctxs, added=None):
230 def __init__(self, repo, name, parentctxs, added=None):
228 opts = {
231 opts = {
229 'changes': scmutil.status([], added or [], [], [], [], [], []),
232 'changes': scmutil.status([], added or [], [], [], [], [], []),
230 'date': '0 0',
233 'date': '0 0',
231 'extra': {'branch': 'default'},
234 'extra': {'branch': 'default'},
232 }
235 }
233 super(simplecommitctx, self).__init__(self, name, **opts)
236 super(simplecommitctx, self).__init__(self, name, **opts)
234 self._repo = repo
237 self._repo = repo
235 self._name = name
238 self._name = name
236 self._parents = parentctxs
239 self._parents = parentctxs
237 self._parents.sort(key=lambda c: c.node())
240 self._parents.sort(key=lambda c: c.node())
238 while len(self._parents) < 2:
241 while len(self._parents) < 2:
239 self._parents.append(repo[node.nullid])
242 self._parents.append(repo[node.nullid])
240
243
241 def filectx(self, key):
244 def filectx(self, key):
242 return simplefilectx(key, self._name)
245 return simplefilectx(key, self._name)
243
246
244 def commit(self):
247 def commit(self):
245 return self._repo.commitctx(self)
248 return self._repo.commitctx(self)
246
249
247 def _walkgraph(edges):
250 def _walkgraph(edges):
248 """yield node, parents in topologically order"""
251 """yield node, parents in topologically order"""
249 visible = set(edges.keys())
252 visible = set(edges.keys())
250 remaining = {} # {str: [str]}
253 remaining = {} # {str: [str]}
251 for k, vs in edges.iteritems():
254 for k, vs in edges.iteritems():
252 for v in vs:
255 for v in vs:
253 if v not in remaining:
256 if v not in remaining:
254 remaining[v] = []
257 remaining[v] = []
255 remaining[k] = vs[:]
258 remaining[k] = vs[:]
256 while remaining:
259 while remaining:
257 leafs = [k for k, v in remaining.items() if not v]
260 leafs = [k for k, v in remaining.items() if not v]
258 if not leafs:
261 if not leafs:
259 raise error.Abort(_('the graph has cycles'))
262 raise error.Abort(_('the graph has cycles'))
260 for leaf in sorted(leafs):
263 for leaf in sorted(leafs):
261 if leaf in visible:
264 if leaf in visible:
262 yield leaf, edges[leaf]
265 yield leaf, edges[leaf]
263 del remaining[leaf]
266 del remaining[leaf]
264 for k, v in remaining.iteritems():
267 for k, v in remaining.iteritems():
265 if leaf in v:
268 if leaf in v:
266 v.remove(leaf)
269 v.remove(leaf)
267
270
268 @command('debugdrawdag', [])
271 @command('debugdrawdag', [])
269 def debugdrawdag(ui, repo, **opts):
272 def debugdrawdag(ui, repo, **opts):
270 """read an ASCII graph from stdin and create changesets
273 """read an ASCII graph from stdin and create changesets
271
274
272 The ASCII graph is like what :hg:`log -G` outputs, with each `o` replaced
275 The ASCII graph is like what :hg:`log -G` outputs, with each `o` replaced
273 to the name of the node. The command will create dummy changesets and local
276 to the name of the node. The command will create dummy changesets and local
274 tags with those names to make the dummy changesets easier to be referred
277 tags with those names to make the dummy changesets easier to be referred
275 to.
278 to.
276
279
277 If the name of a node is a single character 'o', It will be replaced by the
280 If the name of a node is a single character 'o', It will be replaced by the
278 word to the right. This makes it easier to reuse
281 word to the right. This makes it easier to reuse
279 :hg:`log -G -T '{desc}'` outputs.
282 :hg:`log -G -T '{desc}'` outputs.
280
283
281 For root (no parents) nodes, revset can be used to query existing repo.
284 For root (no parents) nodes, revset can be used to query existing repo.
282 Note that the revset cannot have confusing characters which can be seen as
285 Note that the revset cannot have confusing characters which can be seen as
283 the part of the graph edges, like `|/+-\`.
286 the part of the graph edges, like `|/+-\`.
284 """
287 """
285 text = ui.fin.read()
288 text = ui.fin.read()
286
289
287 # parse the graph and make sure len(parents) <= 2 for each node
290 # parse the graph and make sure len(parents) <= 2 for each node
288 edges = _parseasciigraph(text)
291 edges = _parseasciigraph(text)
289 for k, v in edges.iteritems():
292 for k, v in edges.iteritems():
290 if len(v) > 2:
293 if len(v) > 2:
291 raise error.Abort(_('%s: too many parents: %s')
294 raise error.Abort(_('%s: too many parents: %s')
292 % (k, ' '.join(v)))
295 % (k, ' '.join(v)))
293
296
294 committed = {None: node.nullid} # {name: node}
297 committed = {None: node.nullid} # {name: node}
295
298
296 # for leaf nodes, try to find existing nodes in repo
299 # for leaf nodes, try to find existing nodes in repo
297 for name, parents in edges.iteritems():
300 for name, parents in edges.iteritems():
298 if len(parents) == 0:
301 if len(parents) == 0:
299 try:
302 try:
300 committed[name] = scmutil.revsingle(repo, name)
303 committed[name] = scmutil.revsingle(repo, name)
301 except error.RepoLookupError:
304 except error.RepoLookupError:
302 pass
305 pass
303
306
304 # commit in topological order
307 # commit in topological order
305 for name, parents in _walkgraph(edges):
308 for name, parents in _walkgraph(edges):
306 if name in committed:
309 if name in committed:
307 continue
310 continue
308 pctxs = [repo[committed[n]] for n in parents]
311 pctxs = [repo[committed[n]] for n in parents]
309 ctx = simplecommitctx(repo, name, pctxs, [name])
312 ctx = simplecommitctx(repo, name, pctxs, [name])
310 n = ctx.commit()
313 n = ctx.commit()
311 committed[name] = n
314 committed[name] = n
312 tagsmod.tag(repo, name, n, message=None, user=None, date=None,
315 tagsmod.tag(repo, name, n, message=None, user=None, date=None,
313 local=True)
316 local=True)
General Comments 0
You need to be logged in to leave comments. Login now