##// END OF EJS Templates
revset: remove "small" argument from "_optimize"...
Jun Wu -
r34273:53fb09c7 default
parent child Browse files
Show More
@@ -1,679 +1,675 b''
1 1 # revsetlang.py - parser, tokenizer and utility for revision set language
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import string
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 node,
16 16 parser,
17 17 pycompat,
18 18 util,
19 19 )
20 20
21 21 elements = {
22 22 # token-type: binding-strength, primary, prefix, infix, suffix
23 23 "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
24 24 "[": (21, None, None, ("subscript", 1, "]"), None),
25 25 "#": (21, None, None, ("relation", 21), None),
26 26 "##": (20, None, None, ("_concat", 20), None),
27 27 "~": (18, None, None, ("ancestor", 18), None),
28 28 "^": (18, None, None, ("parent", 18), "parentpost"),
29 29 "-": (5, None, ("negate", 19), ("minus", 5), None),
30 30 "::": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
31 31 "..": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
32 32 ":": (15, "rangeall", ("rangepre", 15), ("range", 15), "rangepost"),
33 33 "not": (10, None, ("not", 10), None, None),
34 34 "!": (10, None, ("not", 10), None, None),
35 35 "and": (5, None, None, ("and", 5), None),
36 36 "&": (5, None, None, ("and", 5), None),
37 37 "%": (5, None, None, ("only", 5), "onlypost"),
38 38 "or": (4, None, None, ("or", 4), None),
39 39 "|": (4, None, None, ("or", 4), None),
40 40 "+": (4, None, None, ("or", 4), None),
41 41 "=": (3, None, None, ("keyvalue", 3), None),
42 42 ",": (2, None, None, ("list", 2), None),
43 43 ")": (0, None, None, None, None),
44 44 "]": (0, None, None, None, None),
45 45 "symbol": (0, "symbol", None, None, None),
46 46 "string": (0, "string", None, None, None),
47 47 "end": (0, None, None, None, None),
48 48 }
49 49
50 50 keywords = {'and', 'or', 'not'}
51 51
52 52 _quoteletters = {'"', "'"}
53 53 _simpleopletters = set(pycompat.iterbytestr("()[]#:=,-|&+!~^%"))
54 54
55 55 # default set of valid characters for the initial letter of symbols
56 56 _syminitletters = set(pycompat.iterbytestr(
57 57 string.ascii_letters.encode('ascii') +
58 58 string.digits.encode('ascii') +
59 59 '._@')) | set(map(pycompat.bytechr, xrange(128, 256)))
60 60
61 61 # default set of valid characters for non-initial letters of symbols
62 62 _symletters = _syminitletters | set(pycompat.iterbytestr('-/'))
63 63
64 64 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
65 65 '''
66 66 Parse a revset statement into a stream of tokens
67 67
68 68 ``syminitletters`` is the set of valid characters for the initial
69 69 letter of symbols.
70 70
71 71 By default, character ``c`` is recognized as valid for initial
72 72 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
73 73
74 74 ``symletters`` is the set of valid characters for non-initial
75 75 letters of symbols.
76 76
77 77 By default, character ``c`` is recognized as valid for non-initial
78 78 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
79 79
80 80 Check that @ is a valid unquoted token character (issue3686):
81 81 >>> list(tokenize(b"@::"))
82 82 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
83 83
84 84 '''
85 85 program = pycompat.bytestr(program)
86 86 if syminitletters is None:
87 87 syminitletters = _syminitletters
88 88 if symletters is None:
89 89 symletters = _symletters
90 90
91 91 if program and lookup:
92 92 # attempt to parse old-style ranges first to deal with
93 93 # things like old-tag which contain query metacharacters
94 94 parts = program.split(':', 1)
95 95 if all(lookup(sym) for sym in parts if sym):
96 96 if parts[0]:
97 97 yield ('symbol', parts[0], 0)
98 98 if len(parts) > 1:
99 99 s = len(parts[0])
100 100 yield (':', None, s)
101 101 if parts[1]:
102 102 yield ('symbol', parts[1], s + 1)
103 103 yield ('end', None, len(program))
104 104 return
105 105
106 106 pos, l = 0, len(program)
107 107 while pos < l:
108 108 c = program[pos]
109 109 if c.isspace(): # skip inter-token whitespace
110 110 pass
111 111 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
112 112 yield ('::', None, pos)
113 113 pos += 1 # skip ahead
114 114 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
115 115 yield ('..', None, pos)
116 116 pos += 1 # skip ahead
117 117 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
118 118 yield ('##', None, pos)
119 119 pos += 1 # skip ahead
120 120 elif c in _simpleopletters: # handle simple operators
121 121 yield (c, None, pos)
122 122 elif (c in _quoteletters or c == 'r' and
123 123 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
124 124 if c == 'r':
125 125 pos += 1
126 126 c = program[pos]
127 127 decode = lambda x: x
128 128 else:
129 129 decode = parser.unescapestr
130 130 pos += 1
131 131 s = pos
132 132 while pos < l: # find closing quote
133 133 d = program[pos]
134 134 if d == '\\': # skip over escaped characters
135 135 pos += 2
136 136 continue
137 137 if d == c:
138 138 yield ('string', decode(program[s:pos]), s)
139 139 break
140 140 pos += 1
141 141 else:
142 142 raise error.ParseError(_("unterminated string"), s)
143 143 # gather up a symbol/keyword
144 144 elif c in syminitletters:
145 145 s = pos
146 146 pos += 1
147 147 while pos < l: # find end of symbol
148 148 d = program[pos]
149 149 if d not in symletters:
150 150 break
151 151 if d == '.' and program[pos - 1] == '.': # special case for ..
152 152 pos -= 1
153 153 break
154 154 pos += 1
155 155 sym = program[s:pos]
156 156 if sym in keywords: # operator keywords
157 157 yield (sym, None, s)
158 158 elif '-' in sym:
159 159 # some jerk gave us foo-bar-baz, try to check if it's a symbol
160 160 if lookup and lookup(sym):
161 161 # looks like a real symbol
162 162 yield ('symbol', sym, s)
163 163 else:
164 164 # looks like an expression
165 165 parts = sym.split('-')
166 166 for p in parts[:-1]:
167 167 if p: # possible consecutive -
168 168 yield ('symbol', p, s)
169 169 s += len(p)
170 170 yield ('-', None, pos)
171 171 s += 1
172 172 if parts[-1]: # possible trailing -
173 173 yield ('symbol', parts[-1], s)
174 174 else:
175 175 yield ('symbol', sym, s)
176 176 pos -= 1
177 177 else:
178 178 raise error.ParseError(_("syntax error in revset '%s'") %
179 179 program, pos)
180 180 pos += 1
181 181 yield ('end', None, pos)
182 182
183 183 # helpers
184 184
185 185 _notset = object()
186 186
187 187 def getsymbol(x):
188 188 if x and x[0] == 'symbol':
189 189 return x[1]
190 190 raise error.ParseError(_('not a symbol'))
191 191
192 192 def getstring(x, err):
193 193 if x and (x[0] == 'string' or x[0] == 'symbol'):
194 194 return x[1]
195 195 raise error.ParseError(err)
196 196
197 197 def getinteger(x, err, default=_notset):
198 198 if not x and default is not _notset:
199 199 return default
200 200 try:
201 201 return int(getstring(x, err))
202 202 except ValueError:
203 203 raise error.ParseError(err)
204 204
205 205 def getboolean(x, err):
206 206 value = util.parsebool(getsymbol(x))
207 207 if value is not None:
208 208 return value
209 209 raise error.ParseError(err)
210 210
211 211 def getlist(x):
212 212 if not x:
213 213 return []
214 214 if x[0] == 'list':
215 215 return list(x[1:])
216 216 return [x]
217 217
218 218 def getrange(x, err):
219 219 if not x:
220 220 raise error.ParseError(err)
221 221 op = x[0]
222 222 if op == 'range':
223 223 return x[1], x[2]
224 224 elif op == 'rangepre':
225 225 return None, x[1]
226 226 elif op == 'rangepost':
227 227 return x[1], None
228 228 elif op == 'rangeall':
229 229 return None, None
230 230 raise error.ParseError(err)
231 231
232 232 def getargs(x, min, max, err):
233 233 l = getlist(x)
234 234 if len(l) < min or (max >= 0 and len(l) > max):
235 235 raise error.ParseError(err)
236 236 return l
237 237
238 238 def getargsdict(x, funcname, keys):
239 239 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
240 240 keyvaluenode='keyvalue', keynode='symbol')
241 241
242 242 # cache of {spec: raw parsed tree} built internally
243 243 _treecache = {}
244 244
245 245 def _cachedtree(spec):
246 246 # thread safe because parse() is reentrant and dict.__setitem__() is atomic
247 247 tree = _treecache.get(spec)
248 248 if tree is None:
249 249 _treecache[spec] = tree = parse(spec)
250 250 return tree
251 251
252 252 def _build(tmplspec, *repls):
253 253 """Create raw parsed tree from a template revset statement
254 254
255 255 >>> _build(b'f(_) and _', (b'string', b'1'), (b'symbol', b'2'))
256 256 ('and', ('func', ('symbol', 'f'), ('string', '1')), ('symbol', '2'))
257 257 """
258 258 template = _cachedtree(tmplspec)
259 259 return parser.buildtree(template, ('symbol', '_'), *repls)
260 260
261 261 def _match(patspec, tree):
262 262 """Test if a tree matches the given pattern statement; return the matches
263 263
264 264 >>> _match(b'f(_)', parse(b'f()'))
265 265 >>> _match(b'f(_)', parse(b'f(1)'))
266 266 [('func', ('symbol', 'f'), ('symbol', '1')), ('symbol', '1')]
267 267 >>> _match(b'f(_)', parse(b'f(1, 2)'))
268 268 """
269 269 pattern = _cachedtree(patspec)
270 270 return parser.matchtree(pattern, tree, ('symbol', '_'),
271 271 {'keyvalue', 'list'})
272 272
273 273 def _matchonly(revs, bases):
274 274 return _match('ancestors(_) and not ancestors(_)', ('and', revs, bases))
275 275
276 276 def _fixops(x):
277 277 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
278 278 handled well by our simple top-down parser"""
279 279 if not isinstance(x, tuple):
280 280 return x
281 281
282 282 op = x[0]
283 283 if op == 'parent':
284 284 # x^:y means (x^) : y, not x ^ (:y)
285 285 # x^: means (x^) :, not x ^ (:)
286 286 post = ('parentpost', x[1])
287 287 if x[2][0] == 'dagrangepre':
288 288 return _fixops(('dagrange', post, x[2][1]))
289 289 elif x[2][0] == 'rangepre':
290 290 return _fixops(('range', post, x[2][1]))
291 291 elif x[2][0] == 'rangeall':
292 292 return _fixops(('rangepost', post))
293 293 elif op == 'or':
294 294 # make number of arguments deterministic:
295 295 # x + y + z -> (or x y z) -> (or (list x y z))
296 296 return (op, _fixops(('list',) + x[1:]))
297 297 elif op == 'subscript' and x[1][0] == 'relation':
298 298 # x#y[z] ternary
299 299 return _fixops(('relsubscript', x[1][1], x[1][2], x[2]))
300 300
301 301 return (op,) + tuple(_fixops(y) for y in x[1:])
302 302
303 303 def _analyze(x):
304 304 if x is None:
305 305 return x
306 306
307 307 op = x[0]
308 308 if op == 'minus':
309 309 return _analyze(_build('_ and not _', *x[1:]))
310 310 elif op == 'only':
311 311 return _analyze(_build('only(_, _)', *x[1:]))
312 312 elif op == 'onlypost':
313 313 return _analyze(_build('only(_)', x[1]))
314 314 elif op == 'dagrangepre':
315 315 return _analyze(_build('ancestors(_)', x[1]))
316 316 elif op == 'dagrangepost':
317 317 return _analyze(_build('descendants(_)', x[1]))
318 318 elif op == 'negate':
319 319 s = getstring(x[1], _("can't negate that"))
320 320 return _analyze(('string', '-' + s))
321 321 elif op in ('string', 'symbol'):
322 322 return x
323 323 elif op == 'rangeall':
324 324 return (op, None)
325 325 elif op in {'or', 'not', 'rangepre', 'rangepost', 'parentpost'}:
326 326 return (op, _analyze(x[1]))
327 327 elif op == 'group':
328 328 return _analyze(x[1])
329 329 elif op in {'and', 'dagrange', 'range', 'parent', 'ancestor', 'relation',
330 330 'subscript'}:
331 331 ta = _analyze(x[1])
332 332 tb = _analyze(x[2])
333 333 return (op, ta, tb)
334 334 elif op == 'relsubscript':
335 335 ta = _analyze(x[1])
336 336 tb = _analyze(x[2])
337 337 tc = _analyze(x[3])
338 338 return (op, ta, tb, tc)
339 339 elif op == 'list':
340 340 return (op,) + tuple(_analyze(y) for y in x[1:])
341 341 elif op == 'keyvalue':
342 342 return (op, x[1], _analyze(x[2]))
343 343 elif op == 'func':
344 344 return (op, x[1], _analyze(x[2]))
345 345 raise ValueError('invalid operator %r' % op)
346 346
347 347 def analyze(x):
348 348 """Transform raw parsed tree to evaluatable tree which can be fed to
349 349 optimize() or getset()
350 350
351 351 All pseudo operations should be mapped to real operations or functions
352 352 defined in methods or symbols table respectively.
353 353 """
354 354 return _analyze(x)
355 355
356 def _optimize(x, small):
356 def _optimize(x):
357 357 if x is None:
358 358 return 0, x
359 359
360 smallbonus = 1
361 if small:
362 smallbonus = .5
363
364 360 op = x[0]
365 361 if op in ('string', 'symbol'):
366 return smallbonus, x # single revisions are small
362 return 0.5, x # single revisions are small
367 363 elif op == 'and':
368 wa, ta = _optimize(x[1], True)
369 wb, tb = _optimize(x[2], True)
364 wa, ta = _optimize(x[1])
365 wb, tb = _optimize(x[2])
370 366 w = min(wa, wb)
371 367
372 368 # (draft/secret/_notpublic() & ::x) have a fast path
373 369 m = _match('_() & ancestors(_)', ('and', ta, tb))
374 370 if m and getsymbol(m[1]) in {'draft', 'secret', '_notpublic'}:
375 371 return w, _build('_phaseandancestors(_, _)', m[1], m[2])
376 372
377 373 # (::x and not ::y)/(not ::y and ::x) have a fast path
378 374 m = _matchonly(ta, tb) or _matchonly(tb, ta)
379 375 if m:
380 376 return w, _build('only(_, _)', *m[1:])
381 377
382 378 m = _match('not _', tb)
383 379 if m:
384 380 return wa, ('difference', ta, m[1])
385 381 if wa > wb:
386 382 op = 'andsmally'
387 383 return w, (op, ta, tb)
388 384 elif op == 'or':
389 385 # fast path for machine-generated expression, that is likely to have
390 386 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
391 387 ws, ts, ss = [], [], []
392 388 def flushss():
393 389 if not ss:
394 390 return
395 391 if len(ss) == 1:
396 392 w, t = ss[0]
397 393 else:
398 394 s = '\0'.join(t[1] for w, t in ss)
399 395 y = _build('_list(_)', ('string', s))
400 w, t = _optimize(y, False)
396 w, t = _optimize(y)
401 397 ws.append(w)
402 398 ts.append(t)
403 399 del ss[:]
404 400 for y in getlist(x[1]):
405 w, t = _optimize(y, False)
401 w, t = _optimize(y)
406 402 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
407 403 ss.append((w, t))
408 404 continue
409 405 flushss()
410 406 ws.append(w)
411 407 ts.append(t)
412 408 flushss()
413 409 if len(ts) == 1:
414 410 return ws[0], ts[0] # 'or' operation is fully optimized out
415 411 return max(ws), (op, ('list',) + tuple(ts))
416 412 elif op == 'not':
417 413 # Optimize not public() to _notpublic() because we have a fast version
418 414 if _match('public()', x[1]):
419 o = _optimize(_build('_notpublic()'), not small)
415 o = _optimize(_build('_notpublic()'))
420 416 return o[0], o[1]
421 417 else:
422 o = _optimize(x[1], not small)
418 o = _optimize(x[1])
423 419 return o[0], (op, o[1])
424 420 elif op == 'rangeall':
425 return smallbonus, x
421 return 1, x
426 422 elif op in ('rangepre', 'rangepost', 'parentpost'):
427 o = _optimize(x[1], small)
423 o = _optimize(x[1])
428 424 return o[0], (op, o[1])
429 425 elif op in ('dagrange', 'range'):
430 wa, ta = _optimize(x[1], small)
431 wb, tb = _optimize(x[2], small)
426 wa, ta = _optimize(x[1])
427 wb, tb = _optimize(x[2])
432 428 return wa + wb, (op, ta, tb)
433 429 elif op in ('parent', 'ancestor', 'relation', 'subscript'):
434 w, t = _optimize(x[1], small)
430 w, t = _optimize(x[1])
435 431 return w, (op, t, x[2])
436 432 elif op == 'relsubscript':
437 w, t = _optimize(x[1], small)
433 w, t = _optimize(x[1])
438 434 return w, (op, t, x[2], x[3])
439 435 elif op == 'list':
440 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
436 ws, ts = zip(*(_optimize(y) for y in x[1:]))
441 437 return sum(ws), (op,) + ts
442 438 elif op == 'keyvalue':
443 w, t = _optimize(x[2], small)
439 w, t = _optimize(x[2])
444 440 return w, (op, x[1], t)
445 441 elif op == 'func':
446 442 f = getsymbol(x[1])
447 wa, ta = _optimize(x[2], small)
443 wa, ta = _optimize(x[2])
448 444 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
449 445 'keyword', 'outgoing', 'user', 'destination'):
450 446 w = 10 # slow
451 447 elif f in ('modifies', 'adds', 'removes'):
452 448 w = 30 # slower
453 449 elif f == "contains":
454 450 w = 100 # very slow
455 451 elif f == "ancestor":
456 w = 1 * smallbonus
452 w = 0.5
457 453 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
458 454 w = 0
459 455 elif f == "sort":
460 456 w = 10 # assume most sorts look at changelog
461 457 else:
462 458 w = 1
463 459 return w + wa, (op, x[1], ta)
464 460 raise ValueError('invalid operator %r' % op)
465 461
466 462 def optimize(tree):
467 463 """Optimize evaluatable tree
468 464
469 465 All pseudo operations should be transformed beforehand.
470 466 """
471 _weight, newtree = _optimize(tree, small=True)
467 _weight, newtree = _optimize(tree)
472 468 return newtree
473 469
474 470 # the set of valid characters for the initial letter of symbols in
475 471 # alias declarations and definitions
476 472 _aliassyminitletters = _syminitletters | {'$'}
477 473
478 474 def _parsewith(spec, lookup=None, syminitletters=None):
479 475 """Generate a parse tree of given spec with given tokenizing options
480 476
481 477 >>> _parsewith(b'foo($1)', syminitletters=_aliassyminitletters)
482 478 ('func', ('symbol', 'foo'), ('symbol', '$1'))
483 479 >>> _parsewith(b'$1')
484 480 Traceback (most recent call last):
485 481 ...
486 482 ParseError: ("syntax error in revset '$1'", 0)
487 483 >>> _parsewith(b'foo bar')
488 484 Traceback (most recent call last):
489 485 ...
490 486 ParseError: ('invalid token', 4)
491 487 """
492 488 p = parser.parser(elements)
493 489 tree, pos = p.parse(tokenize(spec, lookup=lookup,
494 490 syminitletters=syminitletters))
495 491 if pos != len(spec):
496 492 raise error.ParseError(_('invalid token'), pos)
497 493 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
498 494
499 495 class _aliasrules(parser.basealiasrules):
500 496 """Parsing and expansion rule set of revset aliases"""
501 497 _section = _('revset alias')
502 498
503 499 @staticmethod
504 500 def _parse(spec):
505 501 """Parse alias declaration/definition ``spec``
506 502
507 503 This allows symbol names to use also ``$`` as an initial letter
508 504 (for backward compatibility), and callers of this function should
509 505 examine whether ``$`` is used also for unexpected symbols or not.
510 506 """
511 507 return _parsewith(spec, syminitletters=_aliassyminitletters)
512 508
513 509 @staticmethod
514 510 def _trygetfunc(tree):
515 511 if tree[0] == 'func' and tree[1][0] == 'symbol':
516 512 return tree[1][1], getlist(tree[2])
517 513
518 514 def expandaliases(tree, aliases, warn=None):
519 515 """Expand aliases in a tree, aliases is a list of (name, value) tuples"""
520 516 aliases = _aliasrules.buildmap(aliases)
521 517 tree = _aliasrules.expand(aliases, tree)
522 518 # warn about problematic (but not referred) aliases
523 519 if warn is not None:
524 520 for name, alias in sorted(aliases.iteritems()):
525 521 if alias.error and not alias.warned:
526 522 warn(_('warning: %s\n') % (alias.error))
527 523 alias.warned = True
528 524 return tree
529 525
530 526 def foldconcat(tree):
531 527 """Fold elements to be concatenated by `##`
532 528 """
533 529 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
534 530 return tree
535 531 if tree[0] == '_concat':
536 532 pending = [tree]
537 533 l = []
538 534 while pending:
539 535 e = pending.pop()
540 536 if e[0] == '_concat':
541 537 pending.extend(reversed(e[1:]))
542 538 elif e[0] in ('string', 'symbol'):
543 539 l.append(e[1])
544 540 else:
545 541 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
546 542 raise error.ParseError(msg)
547 543 return ('string', ''.join(l))
548 544 else:
549 545 return tuple(foldconcat(t) for t in tree)
550 546
551 547 def parse(spec, lookup=None):
552 548 return _parsewith(spec, lookup=lookup)
553 549
554 550 def _quote(s):
555 551 r"""Quote a value in order to make it safe for the revset engine.
556 552
557 553 >>> _quote(b'asdf')
558 554 "'asdf'"
559 555 >>> _quote(b"asdf'\"")
560 556 '\'asdf\\\'"\''
561 557 >>> _quote(b'asdf\'')
562 558 "'asdf\\''"
563 559 >>> _quote(1)
564 560 "'1'"
565 561 """
566 562 return "'%s'" % util.escapestr(pycompat.bytestr(s))
567 563
568 564 def formatspec(expr, *args):
569 565 '''
570 566 This is a convenience function for using revsets internally, and
571 567 escapes arguments appropriately. Aliases are intentionally ignored
572 568 so that intended expression behavior isn't accidentally subverted.
573 569
574 570 Supported arguments:
575 571
576 572 %r = revset expression, parenthesized
577 573 %d = int(arg), no quoting
578 574 %s = string(arg), escaped and single-quoted
579 575 %b = arg.branch(), escaped and single-quoted
580 576 %n = hex(arg), single-quoted
581 577 %% = a literal '%'
582 578
583 579 Prefixing the type with 'l' specifies a parenthesized list of that type.
584 580
585 581 >>> formatspec(b'%r:: and %lr', b'10 or 11', (b"this()", b"that()"))
586 582 '(10 or 11):: and ((this()) or (that()))'
587 583 >>> formatspec(b'%d:: and not %d::', 10, 20)
588 584 '10:: and not 20::'
589 585 >>> formatspec(b'%ld or %ld', [], [1])
590 586 "_list('') or 1"
591 587 >>> formatspec(b'keyword(%s)', b'foo\\xe9')
592 588 "keyword('foo\\\\xe9')"
593 589 >>> b = lambda: b'default'
594 590 >>> b.branch = b
595 591 >>> formatspec(b'branch(%b)', b)
596 592 "branch('default')"
597 593 >>> formatspec(b'root(%ls)', [b'a', b'b', b'c', b'd'])
598 594 "root(_list('a\\x00b\\x00c\\x00d'))"
599 595 '''
600 596
601 597 def argtype(c, arg):
602 598 if c == 'd':
603 599 return '%d' % int(arg)
604 600 elif c == 's':
605 601 return _quote(arg)
606 602 elif c == 'r':
607 603 parse(arg) # make sure syntax errors are confined
608 604 return '(%s)' % arg
609 605 elif c == 'n':
610 606 return _quote(node.hex(arg))
611 607 elif c == 'b':
612 608 return _quote(arg.branch())
613 609
614 610 def listexp(s, t):
615 611 l = len(s)
616 612 if l == 0:
617 613 return "_list('')"
618 614 elif l == 1:
619 615 return argtype(t, s[0])
620 616 elif t == 'd':
621 617 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
622 618 elif t == 's':
623 619 return "_list('%s')" % "\0".join(s)
624 620 elif t == 'n':
625 621 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
626 622 elif t == 'b':
627 623 return "_list('%s')" % "\0".join(a.branch() for a in s)
628 624
629 625 m = l // 2
630 626 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
631 627
632 628 expr = pycompat.bytestr(expr)
633 629 ret = ''
634 630 pos = 0
635 631 arg = 0
636 632 while pos < len(expr):
637 633 c = expr[pos]
638 634 if c == '%':
639 635 pos += 1
640 636 d = expr[pos]
641 637 if d == '%':
642 638 ret += d
643 639 elif d in 'dsnbr':
644 640 ret += argtype(d, args[arg])
645 641 arg += 1
646 642 elif d == 'l':
647 643 # a list of some type
648 644 pos += 1
649 645 d = expr[pos]
650 646 ret += listexp(list(args[arg]), d)
651 647 arg += 1
652 648 else:
653 649 raise error.Abort(_('unexpected revspec format character %s')
654 650 % d)
655 651 else:
656 652 ret += c
657 653 pos += 1
658 654
659 655 return ret
660 656
661 657 def prettyformat(tree):
662 658 return parser.prettyformat(tree, ('string', 'symbol'))
663 659
664 660 def depth(tree):
665 661 if isinstance(tree, tuple):
666 662 return max(map(depth, tree)) + 1
667 663 else:
668 664 return 0
669 665
670 666 def funcsused(tree):
671 667 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
672 668 return set()
673 669 else:
674 670 funcs = set()
675 671 for s in tree[1:]:
676 672 funcs |= funcsused(s)
677 673 if tree[0] == 'func':
678 674 funcs.add(tree[1][1])
679 675 return funcs
General Comments 0
You need to be logged in to leave comments. Login now