##// END OF EJS Templates
revsetlang: fix _quote on int on python3...
Augie Fackler -
r31636:f3b15127 default
parent child Browse files
Show More
@@ -1,702 +1,702 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 "##": (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 202 def getlist(x):
203 203 if not x:
204 204 return []
205 205 if x[0] == 'list':
206 206 return list(x[1:])
207 207 return [x]
208 208
209 209 def getrange(x, err):
210 210 if not x:
211 211 raise error.ParseError(err)
212 212 op = x[0]
213 213 if op == 'range':
214 214 return x[1], x[2]
215 215 elif op == 'rangepre':
216 216 return None, x[1]
217 217 elif op == 'rangepost':
218 218 return x[1], None
219 219 elif op == 'rangeall':
220 220 return None, None
221 221 raise error.ParseError(err)
222 222
223 223 def getargs(x, min, max, err):
224 224 l = getlist(x)
225 225 if len(l) < min or (max >= 0 and len(l) > max):
226 226 raise error.ParseError(err)
227 227 return l
228 228
229 229 def getargsdict(x, funcname, keys):
230 230 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
231 231 keyvaluenode='keyvalue', keynode='symbol')
232 232
233 233 # Constants for ordering requirement, used in _analyze():
234 234 #
235 235 # If 'define', any nested functions and operations can change the ordering of
236 236 # the entries in the set. If 'follow', any nested functions and operations
237 237 # should take the ordering specified by the first operand to the '&' operator.
238 238 #
239 239 # For instance,
240 240 #
241 241 # X & (Y | Z)
242 242 # ^ ^^^^^^^
243 243 # | follow
244 244 # define
245 245 #
246 246 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
247 247 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
248 248 #
249 249 # 'any' means the order doesn't matter. For instance,
250 250 #
251 251 # X & !Y
252 252 # ^
253 253 # any
254 254 #
255 255 # 'y()' can either enforce its ordering requirement or take the ordering
256 256 # specified by 'x()' because 'not()' doesn't care the order.
257 257 #
258 258 # Transition of ordering requirement:
259 259 #
260 260 # 1. starts with 'define'
261 261 # 2. shifts to 'follow' by 'x & y'
262 262 # 3. changes back to 'define' on function call 'f(x)' or function-like
263 263 # operation 'x (f) y' because 'f' may have its own ordering requirement
264 264 # for 'x' and 'y' (e.g. 'first(x)')
265 265 #
266 266 anyorder = 'any' # don't care the order
267 267 defineorder = 'define' # should define the order
268 268 followorder = 'follow' # must follow the current order
269 269
270 270 # transition table for 'x & y', from the current expression 'x' to 'y'
271 271 _tofolloworder = {
272 272 anyorder: anyorder,
273 273 defineorder: followorder,
274 274 followorder: followorder,
275 275 }
276 276
277 277 def _matchonly(revs, bases):
278 278 """
279 279 >>> f = lambda *args: _matchonly(*map(parse, args))
280 280 >>> f('ancestors(A)', 'not ancestors(B)')
281 281 ('list', ('symbol', 'A'), ('symbol', 'B'))
282 282 """
283 283 if (revs is not None
284 284 and revs[0] == 'func'
285 285 and getsymbol(revs[1]) == 'ancestors'
286 286 and bases is not None
287 287 and bases[0] == 'not'
288 288 and bases[1][0] == 'func'
289 289 and getsymbol(bases[1][1]) == 'ancestors'):
290 290 return ('list', revs[2], bases[1][2])
291 291
292 292 def _fixops(x):
293 293 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
294 294 handled well by our simple top-down parser"""
295 295 if not isinstance(x, tuple):
296 296 return x
297 297
298 298 op = x[0]
299 299 if op == 'parent':
300 300 # x^:y means (x^) : y, not x ^ (:y)
301 301 # x^: means (x^) :, not x ^ (:)
302 302 post = ('parentpost', x[1])
303 303 if x[2][0] == 'dagrangepre':
304 304 return _fixops(('dagrange', post, x[2][1]))
305 305 elif x[2][0] == 'rangepre':
306 306 return _fixops(('range', post, x[2][1]))
307 307 elif x[2][0] == 'rangeall':
308 308 return _fixops(('rangepost', post))
309 309 elif op == 'or':
310 310 # make number of arguments deterministic:
311 311 # x + y + z -> (or x y z) -> (or (list x y z))
312 312 return (op, _fixops(('list',) + x[1:]))
313 313
314 314 return (op,) + tuple(_fixops(y) for y in x[1:])
315 315
316 316 def _analyze(x, order):
317 317 if x is None:
318 318 return x
319 319
320 320 op = x[0]
321 321 if op == 'minus':
322 322 return _analyze(('and', x[1], ('not', x[2])), order)
323 323 elif op == 'only':
324 324 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
325 325 return _analyze(t, order)
326 326 elif op == 'onlypost':
327 327 return _analyze(('func', ('symbol', 'only'), x[1]), order)
328 328 elif op == 'dagrangepre':
329 329 return _analyze(('func', ('symbol', 'ancestors'), x[1]), order)
330 330 elif op == 'dagrangepost':
331 331 return _analyze(('func', ('symbol', 'descendants'), x[1]), order)
332 332 elif op == 'negate':
333 333 s = getstring(x[1], _("can't negate that"))
334 334 return _analyze(('string', '-' + s), order)
335 335 elif op in ('string', 'symbol'):
336 336 return x
337 337 elif op == 'and':
338 338 ta = _analyze(x[1], order)
339 339 tb = _analyze(x[2], _tofolloworder[order])
340 340 return (op, ta, tb, order)
341 341 elif op == 'or':
342 342 return (op, _analyze(x[1], order), order)
343 343 elif op == 'not':
344 344 return (op, _analyze(x[1], anyorder), order)
345 345 elif op == 'rangeall':
346 346 return (op, None, order)
347 347 elif op in ('rangepre', 'rangepost', 'parentpost'):
348 348 return (op, _analyze(x[1], defineorder), order)
349 349 elif op == 'group':
350 350 return _analyze(x[1], order)
351 351 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
352 352 ta = _analyze(x[1], defineorder)
353 353 tb = _analyze(x[2], defineorder)
354 354 return (op, ta, tb, order)
355 355 elif op == 'list':
356 356 return (op,) + tuple(_analyze(y, order) for y in x[1:])
357 357 elif op == 'keyvalue':
358 358 return (op, x[1], _analyze(x[2], order))
359 359 elif op == 'func':
360 360 f = getsymbol(x[1])
361 361 d = defineorder
362 362 if f == 'present':
363 363 # 'present(set)' is known to return the argument set with no
364 364 # modification, so forward the current order to its argument
365 365 d = order
366 366 return (op, x[1], _analyze(x[2], d), order)
367 367 raise ValueError('invalid operator %r' % op)
368 368
369 369 def analyze(x, order=defineorder):
370 370 """Transform raw parsed tree to evaluatable tree which can be fed to
371 371 optimize() or getset()
372 372
373 373 All pseudo operations should be mapped to real operations or functions
374 374 defined in methods or symbols table respectively.
375 375
376 376 'order' specifies how the current expression 'x' is ordered (see the
377 377 constants defined above.)
378 378 """
379 379 return _analyze(x, order)
380 380
381 381 def _optimize(x, small):
382 382 if x is None:
383 383 return 0, x
384 384
385 385 smallbonus = 1
386 386 if small:
387 387 smallbonus = .5
388 388
389 389 op = x[0]
390 390 if op in ('string', 'symbol'):
391 391 return smallbonus, x # single revisions are small
392 392 elif op == 'and':
393 393 wa, ta = _optimize(x[1], True)
394 394 wb, tb = _optimize(x[2], True)
395 395 order = x[3]
396 396 w = min(wa, wb)
397 397
398 398 # (::x and not ::y)/(not ::y and ::x) have a fast path
399 399 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
400 400 if tm:
401 401 return w, ('func', ('symbol', 'only'), tm, order)
402 402
403 403 if tb is not None and tb[0] == 'not':
404 404 return wa, ('difference', ta, tb[1], order)
405 405
406 406 if wa > wb:
407 407 return w, (op, tb, ta, order)
408 408 return w, (op, ta, tb, order)
409 409 elif op == 'or':
410 410 # fast path for machine-generated expression, that is likely to have
411 411 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
412 412 order = x[2]
413 413 ws, ts, ss = [], [], []
414 414 def flushss():
415 415 if not ss:
416 416 return
417 417 if len(ss) == 1:
418 418 w, t = ss[0]
419 419 else:
420 420 s = '\0'.join(t[1] for w, t in ss)
421 421 y = ('func', ('symbol', '_list'), ('string', s), order)
422 422 w, t = _optimize(y, False)
423 423 ws.append(w)
424 424 ts.append(t)
425 425 del ss[:]
426 426 for y in getlist(x[1]):
427 427 w, t = _optimize(y, False)
428 428 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
429 429 ss.append((w, t))
430 430 continue
431 431 flushss()
432 432 ws.append(w)
433 433 ts.append(t)
434 434 flushss()
435 435 if len(ts) == 1:
436 436 return ws[0], ts[0] # 'or' operation is fully optimized out
437 437 # we can't reorder trees by weight because it would change the order.
438 438 # ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a")
439 439 # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0]))
440 440 return max(ws), (op, ('list',) + tuple(ts), order)
441 441 elif op == 'not':
442 442 # Optimize not public() to _notpublic() because we have a fast version
443 443 if x[1][:3] == ('func', ('symbol', 'public'), None):
444 444 order = x[1][3]
445 445 newsym = ('func', ('symbol', '_notpublic'), None, order)
446 446 o = _optimize(newsym, not small)
447 447 return o[0], o[1]
448 448 else:
449 449 o = _optimize(x[1], not small)
450 450 order = x[2]
451 451 return o[0], (op, o[1], order)
452 452 elif op == 'rangeall':
453 453 return smallbonus, x
454 454 elif op in ('rangepre', 'rangepost', 'parentpost'):
455 455 o = _optimize(x[1], small)
456 456 order = x[2]
457 457 return o[0], (op, o[1], order)
458 458 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
459 459 wa, ta = _optimize(x[1], small)
460 460 wb, tb = _optimize(x[2], small)
461 461 order = x[3]
462 462 return wa + wb, (op, ta, tb, order)
463 463 elif op == 'list':
464 464 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
465 465 return sum(ws), (op,) + ts
466 466 elif op == 'keyvalue':
467 467 w, t = _optimize(x[2], small)
468 468 return w, (op, x[1], t)
469 469 elif op == 'func':
470 470 f = getsymbol(x[1])
471 471 wa, ta = _optimize(x[2], small)
472 472 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
473 473 'keyword', 'outgoing', 'user', 'destination'):
474 474 w = 10 # slow
475 475 elif f in ('modifies', 'adds', 'removes'):
476 476 w = 30 # slower
477 477 elif f == "contains":
478 478 w = 100 # very slow
479 479 elif f == "ancestor":
480 480 w = 1 * smallbonus
481 481 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
482 482 w = 0
483 483 elif f == "sort":
484 484 w = 10 # assume most sorts look at changelog
485 485 else:
486 486 w = 1
487 487 order = x[3]
488 488 return w + wa, (op, x[1], ta, order)
489 489 raise ValueError('invalid operator %r' % op)
490 490
491 491 def optimize(tree):
492 492 """Optimize evaluatable tree
493 493
494 494 All pseudo operations should be transformed beforehand.
495 495 """
496 496 _weight, newtree = _optimize(tree, small=True)
497 497 return newtree
498 498
499 499 # the set of valid characters for the initial letter of symbols in
500 500 # alias declarations and definitions
501 501 _aliassyminitletters = _syminitletters | set(pycompat.sysstr('$'))
502 502
503 503 def _parsewith(spec, lookup=None, syminitletters=None):
504 504 """Generate a parse tree of given spec with given tokenizing options
505 505
506 506 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
507 507 ('func', ('symbol', 'foo'), ('symbol', '$1'))
508 508 >>> _parsewith('$1')
509 509 Traceback (most recent call last):
510 510 ...
511 511 ParseError: ("syntax error in revset '$1'", 0)
512 512 >>> _parsewith('foo bar')
513 513 Traceback (most recent call last):
514 514 ...
515 515 ParseError: ('invalid token', 4)
516 516 """
517 517 p = parser.parser(elements)
518 518 tree, pos = p.parse(tokenize(spec, lookup=lookup,
519 519 syminitletters=syminitletters))
520 520 if pos != len(spec):
521 521 raise error.ParseError(_('invalid token'), pos)
522 522 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
523 523
524 524 class _aliasrules(parser.basealiasrules):
525 525 """Parsing and expansion rule set of revset aliases"""
526 526 _section = _('revset alias')
527 527
528 528 @staticmethod
529 529 def _parse(spec):
530 530 """Parse alias declaration/definition ``spec``
531 531
532 532 This allows symbol names to use also ``$`` as an initial letter
533 533 (for backward compatibility), and callers of this function should
534 534 examine whether ``$`` is used also for unexpected symbols or not.
535 535 """
536 536 return _parsewith(spec, syminitletters=_aliassyminitletters)
537 537
538 538 @staticmethod
539 539 def _trygetfunc(tree):
540 540 if tree[0] == 'func' and tree[1][0] == 'symbol':
541 541 return tree[1][1], getlist(tree[2])
542 542
543 543 def expandaliases(ui, tree):
544 544 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
545 545 tree = _aliasrules.expand(aliases, tree)
546 546 # warn about problematic (but not referred) aliases
547 547 for name, alias in sorted(aliases.iteritems()):
548 548 if alias.error and not alias.warned:
549 549 ui.warn(_('warning: %s\n') % (alias.error))
550 550 alias.warned = True
551 551 return tree
552 552
553 553 def foldconcat(tree):
554 554 """Fold elements to be concatenated by `##`
555 555 """
556 556 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
557 557 return tree
558 558 if tree[0] == '_concat':
559 559 pending = [tree]
560 560 l = []
561 561 while pending:
562 562 e = pending.pop()
563 563 if e[0] == '_concat':
564 564 pending.extend(reversed(e[1:]))
565 565 elif e[0] in ('string', 'symbol'):
566 566 l.append(e[1])
567 567 else:
568 568 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
569 569 raise error.ParseError(msg)
570 570 return ('string', ''.join(l))
571 571 else:
572 572 return tuple(foldconcat(t) for t in tree)
573 573
574 574 def parse(spec, lookup=None):
575 575 return _parsewith(spec, lookup=lookup)
576 576
577 577 def _quote(s):
578 578 r"""Quote a value in order to make it safe for the revset engine.
579 579
580 580 >>> _quote('asdf')
581 581 "'asdf'"
582 582 >>> _quote("asdf'\"")
583 583 '\'asdf\\\'"\''
584 584 >>> _quote('asdf\'')
585 585 "'asdf\\''"
586 586 >>> _quote(1)
587 587 "'1'"
588 588 """
589 return "'%s'" % util.escapestr('%s' % s)
589 return "'%s'" % util.escapestr(pycompat.bytestr(s))
590 590
591 591 def formatspec(expr, *args):
592 592 '''
593 593 This is a convenience function for using revsets internally, and
594 594 escapes arguments appropriately. Aliases are intentionally ignored
595 595 so that intended expression behavior isn't accidentally subverted.
596 596
597 597 Supported arguments:
598 598
599 599 %r = revset expression, parenthesized
600 600 %d = int(arg), no quoting
601 601 %s = string(arg), escaped and single-quoted
602 602 %b = arg.branch(), escaped and single-quoted
603 603 %n = hex(arg), single-quoted
604 604 %% = a literal '%'
605 605
606 606 Prefixing the type with 'l' specifies a parenthesized list of that type.
607 607
608 608 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
609 609 '(10 or 11):: and ((this()) or (that()))'
610 610 >>> formatspec('%d:: and not %d::', 10, 20)
611 611 '10:: and not 20::'
612 612 >>> formatspec('%ld or %ld', [], [1])
613 613 "_list('') or 1"
614 614 >>> formatspec('keyword(%s)', 'foo\\xe9')
615 615 "keyword('foo\\\\xe9')"
616 616 >>> b = lambda: 'default'
617 617 >>> b.branch = b
618 618 >>> formatspec('branch(%b)', b)
619 619 "branch('default')"
620 620 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
621 621 "root(_list('a\\x00b\\x00c\\x00d'))"
622 622 '''
623 623
624 624 def argtype(c, arg):
625 625 if c == 'd':
626 626 return '%d' % int(arg)
627 627 elif c == 's':
628 628 return _quote(arg)
629 629 elif c == 'r':
630 630 parse(arg) # make sure syntax errors are confined
631 631 return '(%s)' % arg
632 632 elif c == 'n':
633 633 return _quote(node.hex(arg))
634 634 elif c == 'b':
635 635 return _quote(arg.branch())
636 636
637 637 def listexp(s, t):
638 638 l = len(s)
639 639 if l == 0:
640 640 return "_list('')"
641 641 elif l == 1:
642 642 return argtype(t, s[0])
643 643 elif t == 'd':
644 644 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
645 645 elif t == 's':
646 646 return "_list('%s')" % "\0".join(s)
647 647 elif t == 'n':
648 648 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
649 649 elif t == 'b':
650 650 return "_list('%s')" % "\0".join(a.branch() for a in s)
651 651
652 652 m = l // 2
653 653 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
654 654
655 655 expr = pycompat.bytestr(expr)
656 656 ret = ''
657 657 pos = 0
658 658 arg = 0
659 659 while pos < len(expr):
660 660 c = expr[pos]
661 661 if c == '%':
662 662 pos += 1
663 663 d = expr[pos]
664 664 if d == '%':
665 665 ret += d
666 666 elif d in 'dsnbr':
667 667 ret += argtype(d, args[arg])
668 668 arg += 1
669 669 elif d == 'l':
670 670 # a list of some type
671 671 pos += 1
672 672 d = expr[pos]
673 673 ret += listexp(list(args[arg]), d)
674 674 arg += 1
675 675 else:
676 676 raise error.Abort(_('unexpected revspec format character %s')
677 677 % d)
678 678 else:
679 679 ret += c
680 680 pos += 1
681 681
682 682 return ret
683 683
684 684 def prettyformat(tree):
685 685 return parser.prettyformat(tree, ('string', 'symbol'))
686 686
687 687 def depth(tree):
688 688 if isinstance(tree, tuple):
689 689 return max(map(depth, tree)) + 1
690 690 else:
691 691 return 0
692 692
693 693 def funcsused(tree):
694 694 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
695 695 return set()
696 696 else:
697 697 funcs = set()
698 698 for s in tree[1:]:
699 699 funcs |= funcsused(s)
700 700 if tree[0] == 'func':
701 701 funcs.add(tree[1][1])
702 702 return funcs
General Comments 0
You need to be logged in to leave comments. Login now