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