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