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