##// END OF EJS Templates
drawdag: allow override file contents via comments...
Jun Wu -
r33785:0531ffd5 default
parent child Browse files
Show More
@@ -1,363 +1,376
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 import re
87 88
88 89 from mercurial.i18n import _
89 90 from mercurial import (
90 91 context,
91 92 error,
92 93 node,
93 94 obsolete,
94 95 registrar,
95 96 scmutil,
96 97 tags as tagsmod,
97 98 )
98 99
99 100 cmdtable = {}
100 101 command = registrar.command(cmdtable)
101 102
102 103 _pipechars = '\\/+-|'
103 104 _nonpipechars = ''.join(chr(i) for i in xrange(33, 127)
104 105 if chr(i) not in _pipechars)
105 106
106 107 def _isname(ch):
107 108 """char -> bool. return True if ch looks like part of a name, False
108 109 otherwise"""
109 110 return ch in _nonpipechars
110 111
111 112 def _parseasciigraph(text):
112 113 """str -> {str : [str]}. convert the ASCII graph to edges"""
113 114 lines = text.splitlines()
114 115 edges = collections.defaultdict(list) # {node: []}
115 116
116 117 def get(y, x):
117 118 """(int, int) -> char. give a coordinate, return the char. return a
118 119 space for anything out of range"""
119 120 if x < 0 or y < 0:
120 121 return ' '
121 122 try:
122 123 return lines[y][x]
123 124 except IndexError:
124 125 return ' '
125 126
126 127 def getname(y, x):
127 128 """(int, int) -> str. like get(y, x) but concatenate left and right
128 129 parts. if name is an 'o', try to replace it to the right"""
129 130 result = ''
130 131 for i in itertools.count(0):
131 132 ch = get(y, x - i)
132 133 if not _isname(ch):
133 134 break
134 135 result = ch + result
135 136 for i in itertools.count(1):
136 137 ch = get(y, x + i)
137 138 if not _isname(ch):
138 139 break
139 140 result += ch
140 141 if result == 'o':
141 142 # special handling, find the name to the right
142 143 result = ''
143 144 for i in itertools.count(2):
144 145 ch = get(y, x + i)
145 146 if ch == ' ' or ch in _pipechars:
146 147 if result or x + i >= len(lines[y]):
147 148 break
148 149 else:
149 150 result += ch
150 151 return result or 'o'
151 152 return result
152 153
153 154 def parents(y, x):
154 155 """(int, int) -> [str]. follow the ASCII edges at given position,
155 156 return a list of parents"""
156 157 visited = {(y, x)}
157 158 visit = []
158 159 result = []
159 160
160 161 def follow(y, x, expected):
161 162 """conditionally append (y, x) to visit array, if it's a char
162 163 in excepted. 'o' in expected means an '_isname' test.
163 164 if '-' (or '+') is not in excepted, and get(y, x) is '-' (or '+'),
164 165 the next line (y + 1, x) will be checked instead."""
165 166 ch = get(y, x)
166 167 if any(ch == c and c not in expected for c in '-+'):
167 168 y += 1
168 169 return follow(y + 1, x, expected)
169 170 if ch in expected or ('o' in expected and _isname(ch)):
170 171 visit.append((y, x))
171 172
172 173 # -o- # starting point:
173 174 # /|\ # follow '-' (horizontally), and '/|\' (to the bottom)
174 175 follow(y + 1, x, '|')
175 176 follow(y + 1, x - 1, '/')
176 177 follow(y + 1, x + 1, '\\')
177 178 follow(y, x - 1, '-')
178 179 follow(y, x + 1, '-')
179 180
180 181 while visit:
181 182 y, x = visit.pop()
182 183 if (y, x) in visited:
183 184 continue
184 185 visited.add((y, x))
185 186 ch = get(y, x)
186 187 if _isname(ch):
187 188 result.append(getname(y, x))
188 189 continue
189 190 elif ch == '|':
190 191 follow(y + 1, x, '/|o')
191 192 follow(y + 1, x - 1, '/')
192 193 follow(y + 1, x + 1, '\\')
193 194 elif ch == '+':
194 195 follow(y, x - 1, '-')
195 196 follow(y, x + 1, '-')
196 197 follow(y + 1, x - 1, '/')
197 198 follow(y + 1, x + 1, '\\')
198 199 follow(y + 1, x, '|')
199 200 elif ch == '\\':
200 201 follow(y + 1, x + 1, '\\|o')
201 202 elif ch == '/':
202 203 follow(y + 1, x - 1, '/|o')
203 204 elif ch == '-':
204 205 follow(y, x - 1, '-+o')
205 206 follow(y, x + 1, '-+o')
206 207 return result
207 208
208 209 for y, line in enumerate(lines):
209 210 for x, ch in enumerate(line):
210 211 if ch == '#': # comment
211 212 break
212 213 if _isname(ch):
213 214 edges[getname(y, x)] += parents(y, x)
214 215
215 216 return dict(edges)
216 217
217 218 class simplefilectx(object):
218 219 def __init__(self, path, data):
219 220 self._data = data
220 221 self._path = path
221 222
222 223 def data(self):
223 224 return self._data
224 225
225 226 def filenode(self):
226 227 return None
227 228
228 229 def path(self):
229 230 return self._path
230 231
231 232 def renamed(self):
232 233 return None
233 234
234 235 def flags(self):
235 236 return ''
236 237
237 238 class simplecommitctx(context.committablectx):
238 239 def __init__(self, repo, name, parentctxs, added):
239 240 opts = {
240 241 'changes': scmutil.status([], list(added), [], [], [], [], []),
241 242 'date': '0 0',
242 243 'extra': {'branch': 'default'},
243 244 }
244 245 super(simplecommitctx, self).__init__(self, name, **opts)
245 246 self._repo = repo
246 247 self._added = added
247 248 self._parents = parentctxs
248 249 while len(self._parents) < 2:
249 250 self._parents.append(repo[node.nullid])
250 251
251 252 def filectx(self, key):
252 253 return simplefilectx(key, self._added[key])
253 254
254 255 def commit(self):
255 256 return self._repo.commitctx(self)
256 257
257 258 def _walkgraph(edges):
258 259 """yield node, parents in topologically order"""
259 260 visible = set(edges.keys())
260 261 remaining = {} # {str: [str]}
261 262 for k, vs in edges.iteritems():
262 263 for v in vs:
263 264 if v not in remaining:
264 265 remaining[v] = []
265 266 remaining[k] = vs[:]
266 267 while remaining:
267 268 leafs = [k for k, v in remaining.items() if not v]
268 269 if not leafs:
269 270 raise error.Abort(_('the graph has cycles'))
270 271 for leaf in sorted(leafs):
271 272 if leaf in visible:
272 273 yield leaf, edges[leaf]
273 274 del remaining[leaf]
274 275 for k, v in remaining.iteritems():
275 276 if leaf in v:
276 277 v.remove(leaf)
277 278
279 def _getcomments(text):
280 for line in text.splitlines():
281 if ' # ' not in line:
282 continue
283 yield line.split(' # ', 1)[1].split(' # ')[0].strip()
284
278 285 @command('debugdrawdag', [])
279 286 def debugdrawdag(ui, repo, **opts):
280 287 """read an ASCII graph from stdin and create changesets
281 288
282 289 The ASCII graph is like what :hg:`log -G` outputs, with each `o` replaced
283 290 to the name of the node. The command will create dummy changesets and local
284 291 tags with those names to make the dummy changesets easier to be referred
285 292 to.
286 293
287 294 If the name of a node is a single character 'o', It will be replaced by the
288 295 word to the right. This makes it easier to reuse
289 296 :hg:`log -G -T '{desc}'` outputs.
290 297
291 298 For root (no parents) nodes, revset can be used to query existing repo.
292 299 Note that the revset cannot have confusing characters which can be seen as
293 300 the part of the graph edges, like `|/+-\`.
294 301 """
295 302 text = ui.fin.read()
296 303
297 304 # parse the graph and make sure len(parents) <= 2 for each node
298 305 edges = _parseasciigraph(text)
299 306 for k, v in edges.iteritems():
300 307 if len(v) > 2:
301 308 raise error.Abort(_('%s: too many parents: %s')
302 309 % (k, ' '.join(v)))
303 310
311 # parse comments to get extra file content instructions
312 files = collections.defaultdict(dict) # {(name, path): content}
313 comments = list(_getcomments(text))
314 filere = re.compile(r'^(\w+)/([\w/]+)\s*=\s*(.*)$', re.M)
315 for name, path, content in filere.findall('\n'.join(comments)):
316 files[name][path] = content.replace(r'\n', '\n')
317
304 318 committed = {None: node.nullid} # {name: node}
305 319
306 320 # for leaf nodes, try to find existing nodes in repo
307 321 for name, parents in edges.iteritems():
308 322 if len(parents) == 0:
309 323 try:
310 324 committed[name] = scmutil.revsingle(repo, name)
311 325 except error.RepoLookupError:
312 326 pass
313 327
314 328 # commit in topological order
315 329 for name, parents in _walkgraph(edges):
316 330 if name in committed:
317 331 continue
318 332 pctxs = [repo[committed[n]] for n in parents]
319 333 pctxs.sort(key=lambda c: c.node())
320 334 added = {}
321 335 if len(parents) > 1:
322 336 # If it's a merge, take the files and contents from the parents
323 337 for f in pctxs[1].manifest():
324 338 if f not in pctxs[0].manifest():
325 339 added[f] = pctxs[1][f].data()
326 340 else:
327 341 # If it's not a merge, add a single file
328 342 added[name] = name
343 # add extra file contents in comments
344 for path, content in files.get(name, {}).items():
345 added[path] = content
329 346 ctx = simplecommitctx(repo, name, pctxs, added)
330 347 n = ctx.commit()
331 348 committed[name] = n
332 349 tagsmod.tag(repo, name, n, message=None, user=None, date=None,
333 350 local=True)
334 351
335 352 # handle special comments
336 353 with repo.wlock(), repo.lock(), repo.transaction('drawdag'):
337 354 getctx = lambda x: repo.unfiltered()[committed[x.strip()]]
338 for line in text.splitlines():
339 if ' # ' not in line:
340 continue
341
355 for comment in comments:
342 356 rels = [] # obsolete relationships
343 comment = line.split(' # ', 1)[1].split(' # ')[0].strip()
344 357 args = comment.split(':', 1)
345 358 if len(args) <= 1:
346 359 continue
347 360
348 361 cmd = args[0].strip()
349 362 arg = args[1].strip()
350 363
351 364 if cmd in ('replace', 'rebase', 'amend'):
352 365 nodes = [getctx(m) for m in arg.split('->')]
353 366 for i in range(len(nodes) - 1):
354 367 rels.append((nodes[i], (nodes[i + 1],)))
355 368 elif cmd in ('split',):
356 369 pre, succs = arg.split('->')
357 370 succs = succs.split(',')
358 371 rels.append((getctx(pre), (getctx(s) for s in succs)))
359 372 elif cmd in ('prune',):
360 373 for n in arg.split(','):
361 374 rels.append((getctx(n), ()))
362 375 if rels:
363 376 obsolete.createmarkers(repo, rels, date=(0, 0), operation=cmd)
@@ -1,234 +1,272
1 1 $ cat >> $HGRCPATH<<EOF
2 2 > [extensions]
3 3 > drawdag=$TESTDIR/drawdag.py
4 4 > [experimental]
5 5 > stabilization=all
6 6 > EOF
7 7
8 8 $ reinit () {
9 9 > rm -rf .hg && hg init
10 10 > }
11 11
12 12 $ hg init
13 13
14 14 Test what said in drawdag.py docstring
15 15
16 16 $ hg debugdrawdag <<'EOS'
17 17 > c d
18 18 > |/
19 19 > b
20 20 > |
21 21 > a
22 22 > EOS
23 23
24 24 $ hg log -G -T '{rev} {desc} ({tags})'
25 25 o 3 d (d tip)
26 26 |
27 27 | o 2 c (c)
28 28 |/
29 29 o 1 b (b)
30 30 |
31 31 o 0 a (a)
32 32
33 33 $ hg debugdrawdag <<'EOS'
34 34 > foo bar bar foo
35 35 > | / | |
36 36 > ancestor(c,d) a baz
37 37 > EOS
38 38
39 39 $ hg log -G -T '{desc}'
40 40 o foo
41 41 |\
42 42 +---o bar
43 43 | | |
44 44 | o | baz
45 45 | /
46 46 +---o d
47 47 | |
48 48 +---o c
49 49 | |
50 50 o | b
51 51 |/
52 52 o a
53 53
54 54 $ reinit
55 55
56 56 $ hg debugdrawdag <<'EOS'
57 57 > o foo
58 58 > |\
59 59 > +---o bar
60 60 > | | |
61 61 > | o | baz
62 62 > | /
63 63 > +---o d
64 64 > | |
65 65 > +---o c
66 66 > | |
67 67 > o | b
68 68 > |/
69 69 > o a
70 70 > EOS
71 71
72 72 $ hg log -G -T '{desc}'
73 73 o foo
74 74 |\
75 75 | | o d
76 76 | |/
77 77 | | o c
78 78 | |/
79 79 | | o bar
80 80 | |/|
81 81 | o | b
82 82 | |/
83 83 o / baz
84 84 /
85 85 o a
86 86
87 87 $ reinit
88 88
89 89 $ hg debugdrawdag <<'EOS'
90 90 > o foo
91 91 > |\
92 92 > | | o d
93 93 > | |/
94 94 > | | o c
95 95 > | |/
96 96 > | | o bar
97 97 > | |/|
98 98 > | o | b
99 99 > | |/
100 100 > o / baz
101 101 > /
102 102 > o a
103 103 > EOS
104 104
105 105 $ hg log -G -T '{desc}'
106 106 o foo
107 107 |\
108 108 | | o d
109 109 | |/
110 110 | | o c
111 111 | |/
112 112 | | o bar
113 113 | |/|
114 114 | o | b
115 115 | |/
116 116 o / baz
117 117 /
118 118 o a
119 119
120 120 $ hg manifest -r a
121 121 a
122 122 $ hg manifest -r b
123 123 a
124 124 b
125 125 $ hg manifest -r bar
126 126 a
127 127 b
128 128 $ hg manifest -r foo
129 129 a
130 130 b
131 131 baz
132 132
133 133 Edges existed in repo are no-ops
134 134
135 135 $ reinit
136 136 $ hg debugdrawdag <<'EOS'
137 137 > B C C
138 138 > | | |
139 139 > A A B
140 140 > EOS
141 141
142 142 $ hg log -G -T '{desc}'
143 143 o C
144 144 |\
145 145 | o B
146 146 |/
147 147 o A
148 148
149 149
150 150 $ hg debugdrawdag <<'EOS'
151 151 > C D C
152 152 > | | |
153 153 > B B A
154 154 > EOS
155 155
156 156 $ hg log -G -T '{desc}'
157 157 o D
158 158 |
159 159 | o C
160 160 |/|
161 161 o | B
162 162 |/
163 163 o A
164 164
165 165
166 166 Node with more than 2 parents are disallowed
167 167
168 168 $ hg debugdrawdag <<'EOS'
169 169 > A
170 170 > /|\
171 171 > D B C
172 172 > EOS
173 173 abort: A: too many parents: C D B
174 174 [255]
175 175
176 176 Cycles are disallowed
177 177
178 178 $ hg debugdrawdag <<'EOS'
179 179 > A
180 180 > |
181 181 > A
182 182 > EOS
183 183 abort: the graph has cycles
184 184 [255]
185 185
186 186 $ hg debugdrawdag <<'EOS'
187 187 > A
188 188 > |
189 189 > B
190 190 > |
191 191 > A
192 192 > EOS
193 193 abort: the graph has cycles
194 194 [255]
195 195
196 196 Create obsmarkers via comments
197 197
198 198 $ reinit
199 199
200 200 $ hg debugdrawdag <<'EOS'
201 201 > G
202 202 > |
203 203 > I D C F # split: B -> E, F, G
204 204 > \ \| | # replace: C -> D -> H
205 205 > H B E # prune: F, I
206 206 > \|/
207 207 > A
208 208 > EOS
209 209
210 210 $ hg log -r 'sort(all(), topo)' -G --hidden -T '{desc} {node}'
211 211 o G 711f53bbef0bebd12eb6f0511d5e2e998b984846
212 212 |
213 213 x F 64a8289d249234b9886244d379f15e6b650b28e3
214 214 |
215 215 o E 7fb047a69f220c21711122dfd94305a9efb60cba
216 216 |
217 217 | x D be0ef73c17ade3fc89dc41701eb9fc3a91b58282
218 218 | |
219 219 | | x C 26805aba1e600a82e93661149f2313866a221a7b
220 220 | |/
221 221 | x B 112478962961147124edd43549aedd1a335e44bf
222 222 |/
223 223 | x I 58e6b987bf7045fcd9c54f496396ca1d1fc81047
224 224 | |
225 225 | o H 575c4b5ec114d64b681d33f8792853568bfb2b2c
226 226 |/
227 227 o A 426bada5c67598ca65036d57d9e4b64b0c1ce7a0
228 228
229 229 $ hg debugobsolete
230 230 112478962961147124edd43549aedd1a335e44bf 7fb047a69f220c21711122dfd94305a9efb60cba 64a8289d249234b9886244d379f15e6b650b28e3 711f53bbef0bebd12eb6f0511d5e2e998b984846 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
231 231 26805aba1e600a82e93661149f2313866a221a7b be0ef73c17ade3fc89dc41701eb9fc3a91b58282 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
232 232 be0ef73c17ade3fc89dc41701eb9fc3a91b58282 575c4b5ec114d64b681d33f8792853568bfb2b2c 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
233 233 64a8289d249234b9886244d379f15e6b650b28e3 0 {7fb047a69f220c21711122dfd94305a9efb60cba} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
234 234 58e6b987bf7045fcd9c54f496396ca1d1fc81047 0 {575c4b5ec114d64b681d33f8792853568bfb2b2c} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
235
236 Change file contents via comments
237
238 $ reinit
239 $ hg debugdrawdag <<'EOS'
240 > C # A/dir1/a = 1\n2
241 > |\ # B/dir2/b = 34
242 > A B # C/dir1/c = 5
243 > # C/dir2/c = 6
244 > # C/A = a
245 > # C/B = b
246 > EOS
247
248 $ hg log -G -T '{desc} {files}'
249 o C A B dir1/c dir2/c
250 |\
251 | o B B dir2/b
252 |
253 o A A dir1/a
254
255 $ for f in `hg files -r C`; do
256 > echo FILE "$f"
257 > hg cat -r C "$f"
258 > echo
259 > done
260 FILE A
261 a
262 FILE B
263 b
264 FILE dir1/a
265 1
266 2
267 FILE dir1/c
268 5
269 FILE dir2/b
270 34
271 FILE dir2/c
272 6
General Comments 0
You need to be logged in to leave comments. Login now