This file is indexed.

/usr/lib/python2.7/dist-packages/mercurial/revsetlang.py is in mercurial-common 4.5.3-1ubuntu2.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
# revsetlang.py - parser, tokenizer and utility for revision set language
#
# Copyright 2010 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

from __future__ import absolute_import

import string

from .i18n import _
from . import (
    error,
    node,
    parser,
    pycompat,
    util,
)

elements = {
    # token-type: binding-strength, primary, prefix, infix, suffix
    "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
    "[": (21, None, None, ("subscript", 1, "]"), None),
    "#": (21, None, None, ("relation", 21), None),
    "##": (20, None, None, ("_concat", 20), None),
    "~": (18, None, None, ("ancestor", 18), None),
    "^": (18, None, None, ("parent", 18), "parentpost"),
    "-": (5, None, ("negate", 19), ("minus", 5), None),
    "::": (17, "dagrangeall", ("dagrangepre", 17), ("dagrange", 17),
           "dagrangepost"),
    "..": (17, "dagrangeall", ("dagrangepre", 17), ("dagrange", 17),
           "dagrangepost"),
    ":": (15, "rangeall", ("rangepre", 15), ("range", 15), "rangepost"),
    "not": (10, None, ("not", 10), None, None),
    "!": (10, None, ("not", 10), None, None),
    "and": (5, None, None, ("and", 5), None),
    "&": (5, None, None, ("and", 5), None),
    "%": (5, None, None, ("only", 5), "onlypost"),
    "or": (4, None, None, ("or", 4), None),
    "|": (4, None, None, ("or", 4), None),
    "+": (4, None, None, ("or", 4), None),
    "=": (3, None, None, ("keyvalue", 3), None),
    ",": (2, None, None, ("list", 2), None),
    ")": (0, None, None, None, None),
    "]": (0, None, None, None, None),
    "symbol": (0, "symbol", None, None, None),
    "string": (0, "string", None, None, None),
    "end": (0, None, None, None, None),
}

keywords = {'and', 'or', 'not'}

symbols = {}

_quoteletters = {'"', "'"}
_simpleopletters = set(pycompat.iterbytestr("()[]#:=,-|&+!~^%"))

# default set of valid characters for the initial letter of symbols
_syminitletters = set(pycompat.iterbytestr(
    string.ascii_letters.encode('ascii') +
    string.digits.encode('ascii') +
    '._@')) | set(map(pycompat.bytechr, xrange(128, 256)))

# default set of valid characters for non-initial letters of symbols
_symletters = _syminitletters | set(pycompat.iterbytestr('-/'))

def tokenize(program, lookup=None, syminitletters=None, symletters=None):
    '''
    Parse a revset statement into a stream of tokens

    ``syminitletters`` is the set of valid characters for the initial
    letter of symbols.

    By default, character ``c`` is recognized as valid for initial
    letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.

    ``symletters`` is the set of valid characters for non-initial
    letters of symbols.

    By default, character ``c`` is recognized as valid for non-initial
    letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.

    Check that @ is a valid unquoted token character (issue3686):
    >>> list(tokenize(b"@::"))
    [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]

    '''
    program = pycompat.bytestr(program)
    if syminitletters is None:
        syminitletters = _syminitletters
    if symletters is None:
        symletters = _symletters

    if program and lookup:
        # attempt to parse old-style ranges first to deal with
        # things like old-tag which contain query metacharacters
        parts = program.split(':', 1)
        if all(lookup(sym) for sym in parts if sym):
            if parts[0]:
                yield ('symbol', parts[0], 0)
            if len(parts) > 1:
                s = len(parts[0])
                yield (':', None, s)
                if parts[1]:
                    yield ('symbol', parts[1], s + 1)
            yield ('end', None, len(program))
            return

    pos, l = 0, len(program)
    while pos < l:
        c = program[pos]
        if c.isspace(): # skip inter-token whitespace
            pass
        elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
            yield ('::', None, pos)
            pos += 1 # skip ahead
        elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
            yield ('..', None, pos)
            pos += 1 # skip ahead
        elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
            yield ('##', None, pos)
            pos += 1 # skip ahead
        elif c in _simpleopletters: # handle simple operators
            yield (c, None, pos)
        elif (c in _quoteletters or c == 'r' and
              program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
            if c == 'r':
                pos += 1
                c = program[pos]
                decode = lambda x: x
            else:
                decode = parser.unescapestr
            pos += 1
            s = pos
            while pos < l: # find closing quote
                d = program[pos]
                if d == '\\': # skip over escaped characters
                    pos += 2
                    continue
                if d == c:
                    yield ('string', decode(program[s:pos]), s)
                    break
                pos += 1
            else:
                raise error.ParseError(_("unterminated string"), s)
        # gather up a symbol/keyword
        elif c in syminitletters:
            s = pos
            pos += 1
            while pos < l: # find end of symbol
                d = program[pos]
                if d not in symletters:
                    break
                if d == '.' and program[pos - 1] == '.': # special case for ..
                    pos -= 1
                    break
                pos += 1
            sym = program[s:pos]
            if sym in keywords: # operator keywords
                yield (sym, None, s)
            elif '-' in sym:
                # some jerk gave us foo-bar-baz, try to check if it's a symbol
                if lookup and lookup(sym):
                    # looks like a real symbol
                    yield ('symbol', sym, s)
                else:
                    # looks like an expression
                    parts = sym.split('-')
                    for p in parts[:-1]:
                        if p: # possible consecutive -
                            yield ('symbol', p, s)
                        s += len(p)
                        yield ('-', None, pos)
                        s += 1
                    if parts[-1]: # possible trailing -
                        yield ('symbol', parts[-1], s)
            else:
                yield ('symbol', sym, s)
            pos -= 1
        else:
            raise error.ParseError(_("syntax error in revset '%s'") %
                                   program, pos)
        pos += 1
    yield ('end', None, pos)

# helpers

_notset = object()

def getsymbol(x):
    if x and x[0] == 'symbol':
        return x[1]
    raise error.ParseError(_('not a symbol'))

def getstring(x, err):
    if x and (x[0] == 'string' or x[0] == 'symbol'):
        return x[1]
    raise error.ParseError(err)

def getinteger(x, err, default=_notset):
    if not x and default is not _notset:
        return default
    try:
        return int(getstring(x, err))
    except ValueError:
        raise error.ParseError(err)

def getboolean(x, err):
    value = util.parsebool(getsymbol(x))
    if value is not None:
        return value
    raise error.ParseError(err)

def getlist(x):
    if not x:
        return []
    if x[0] == 'list':
        return list(x[1:])
    return [x]

def getrange(x, err):
    if not x:
        raise error.ParseError(err)
    op = x[0]
    if op == 'range':
        return x[1], x[2]
    elif op == 'rangepre':
        return None, x[1]
    elif op == 'rangepost':
        return x[1], None
    elif op == 'rangeall':
        return None, None
    raise error.ParseError(err)

def getargs(x, min, max, err):
    l = getlist(x)
    if len(l) < min or (max >= 0 and len(l) > max):
        raise error.ParseError(err)
    return l

def getargsdict(x, funcname, keys):
    return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
                                keyvaluenode='keyvalue', keynode='symbol')

# cache of {spec: raw parsed tree} built internally
_treecache = {}

def _cachedtree(spec):
    # thread safe because parse() is reentrant and dict.__setitem__() is atomic
    tree = _treecache.get(spec)
    if tree is None:
        _treecache[spec] = tree = parse(spec)
    return tree

def _build(tmplspec, *repls):
    """Create raw parsed tree from a template revset statement

    >>> _build(b'f(_) and _', (b'string', b'1'), (b'symbol', b'2'))
    ('and', ('func', ('symbol', 'f'), ('string', '1')), ('symbol', '2'))
    """
    template = _cachedtree(tmplspec)
    return parser.buildtree(template, ('symbol', '_'), *repls)

def _match(patspec, tree):
    """Test if a tree matches the given pattern statement; return the matches

    >>> _match(b'f(_)', parse(b'f()'))
    >>> _match(b'f(_)', parse(b'f(1)'))
    [('func', ('symbol', 'f'), ('symbol', '1')), ('symbol', '1')]
    >>> _match(b'f(_)', parse(b'f(1, 2)'))
    """
    pattern = _cachedtree(patspec)
    return parser.matchtree(pattern, tree, ('symbol', '_'),
                            {'keyvalue', 'list'})

def _matchonly(revs, bases):
    return _match('ancestors(_) and not ancestors(_)', ('and', revs, bases))

def _fixops(x):
    """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
    handled well by our simple top-down parser"""
    if not isinstance(x, tuple):
        return x

    op = x[0]
    if op == 'parent':
        # x^:y means (x^) : y, not x ^ (:y)
        # x^:  means (x^) :,   not x ^ (:)
        post = ('parentpost', x[1])
        if x[2][0] == 'dagrangepre':
            return _fixops(('dagrange', post, x[2][1]))
        elif x[2][0] == 'dagrangeall':
            return _fixops(('dagrangepost', post))
        elif x[2][0] == 'rangepre':
            return _fixops(('range', post, x[2][1]))
        elif x[2][0] == 'rangeall':
            return _fixops(('rangepost', post))
    elif op == 'or':
        # make number of arguments deterministic:
        # x + y + z -> (or x y z) -> (or (list x y z))
        return (op, _fixops(('list',) + x[1:]))
    elif op == 'subscript' and x[1][0] == 'relation':
        # x#y[z] ternary
        return _fixops(('relsubscript', x[1][1], x[1][2], x[2]))

    return (op,) + tuple(_fixops(y) for y in x[1:])

def _analyze(x):
    if x is None:
        return x

    op = x[0]
    if op == 'minus':
        return _analyze(_build('_ and not _', *x[1:]))
    elif op == 'only':
        return _analyze(_build('only(_, _)', *x[1:]))
    elif op == 'onlypost':
        return _analyze(_build('only(_)', x[1]))
    elif op == 'dagrangeall':
        raise error.ParseError(_("can't use '::' in this context"))
    elif op == 'dagrangepre':
        return _analyze(_build('ancestors(_)', x[1]))
    elif op == 'dagrangepost':
        return _analyze(_build('descendants(_)', x[1]))
    elif op == 'negate':
        s = getstring(x[1], _("can't negate that"))
        return _analyze(('string', '-' + s))
    elif op in ('string', 'symbol'):
        return x
    elif op == 'rangeall':
        return (op, None)
    elif op in {'or', 'not', 'rangepre', 'rangepost', 'parentpost'}:
        return (op, _analyze(x[1]))
    elif op == 'group':
        return _analyze(x[1])
    elif op in {'and', 'dagrange', 'range', 'parent', 'ancestor', 'relation',
                'subscript'}:
        ta = _analyze(x[1])
        tb = _analyze(x[2])
        return (op, ta, tb)
    elif op == 'relsubscript':
        ta = _analyze(x[1])
        tb = _analyze(x[2])
        tc = _analyze(x[3])
        return (op, ta, tb, tc)
    elif op == 'list':
        return (op,) + tuple(_analyze(y) for y in x[1:])
    elif op == 'keyvalue':
        return (op, x[1], _analyze(x[2]))
    elif op == 'func':
        return (op, x[1], _analyze(x[2]))
    raise ValueError('invalid operator %r' % op)

def analyze(x):
    """Transform raw parsed tree to evaluatable tree which can be fed to
    optimize() or getset()

    All pseudo operations should be mapped to real operations or functions
    defined in methods or symbols table respectively.
    """
    return _analyze(x)

def _optimize(x):
    if x is None:
        return 0, x

    op = x[0]
    if op in ('string', 'symbol'):
        return 0.5, x # single revisions are small
    elif op == 'and':
        wa, ta = _optimize(x[1])
        wb, tb = _optimize(x[2])
        w = min(wa, wb)

        # (draft/secret/_notpublic() & ::x) have a fast path
        m = _match('_() & ancestors(_)', ('and', ta, tb))
        if m and getsymbol(m[1]) in {'draft', 'secret', '_notpublic'}:
            return w, _build('_phaseandancestors(_, _)', m[1], m[2])

        # (::x and not ::y)/(not ::y and ::x) have a fast path
        m = _matchonly(ta, tb) or _matchonly(tb, ta)
        if m:
            return w, _build('only(_, _)', *m[1:])

        m = _match('not _', tb)
        if m:
            return wa, ('difference', ta, m[1])
        if wa > wb:
            op = 'andsmally'
        return w, (op, ta, tb)
    elif op == 'or':
        # fast path for machine-generated expression, that is likely to have
        # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
        ws, ts, ss = [], [], []
        def flushss():
            if not ss:
                return
            if len(ss) == 1:
                w, t = ss[0]
            else:
                s = '\0'.join(t[1] for w, t in ss)
                y = _build('_list(_)', ('string', s))
                w, t = _optimize(y)
            ws.append(w)
            ts.append(t)
            del ss[:]
        for y in getlist(x[1]):
            w, t = _optimize(y)
            if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
                ss.append((w, t))
                continue
            flushss()
            ws.append(w)
            ts.append(t)
        flushss()
        if len(ts) == 1:
            return ws[0], ts[0] # 'or' operation is fully optimized out
        return max(ws), (op, ('list',) + tuple(ts))
    elif op == 'not':
        # Optimize not public() to _notpublic() because we have a fast version
        if _match('public()', x[1]):
            o = _optimize(_build('_notpublic()'))
            return o[0], o[1]
        else:
            o = _optimize(x[1])
            return o[0], (op, o[1])
    elif op == 'rangeall':
        return 1, x
    elif op in ('rangepre', 'rangepost', 'parentpost'):
        o = _optimize(x[1])
        return o[0], (op, o[1])
    elif op in ('dagrange', 'range'):
        wa, ta = _optimize(x[1])
        wb, tb = _optimize(x[2])
        return wa + wb, (op, ta, tb)
    elif op in ('parent', 'ancestor', 'relation', 'subscript'):
        w, t = _optimize(x[1])
        return w, (op, t, x[2])
    elif op == 'relsubscript':
        w, t = _optimize(x[1])
        return w, (op, t, x[2], x[3])
    elif op == 'list':
        ws, ts = zip(*(_optimize(y) for y in x[1:]))
        return sum(ws), (op,) + ts
    elif op == 'keyvalue':
        w, t = _optimize(x[2])
        return w, (op, x[1], t)
    elif op == 'func':
        f = getsymbol(x[1])
        wa, ta = _optimize(x[2])
        w = getattr(symbols.get(f), '_weight', 1)
        return w + wa, (op, x[1], ta)
    raise ValueError('invalid operator %r' % op)

def optimize(tree):
    """Optimize evaluatable tree

    All pseudo operations should be transformed beforehand.
    """
    _weight, newtree = _optimize(tree)
    return newtree

# the set of valid characters for the initial letter of symbols in
# alias declarations and definitions
_aliassyminitletters = _syminitletters | {'$'}

def _parsewith(spec, lookup=None, syminitletters=None):
    """Generate a parse tree of given spec with given tokenizing options

    >>> _parsewith(b'foo($1)', syminitletters=_aliassyminitletters)
    ('func', ('symbol', 'foo'), ('symbol', '$1'))
    >>> _parsewith(b'$1')
    Traceback (most recent call last):
      ...
    ParseError: ("syntax error in revset '$1'", 0)
    >>> _parsewith(b'foo bar')
    Traceback (most recent call last):
      ...
    ParseError: ('invalid token', 4)
    """
    p = parser.parser(elements)
    tree, pos = p.parse(tokenize(spec, lookup=lookup,
                                 syminitletters=syminitletters))
    if pos != len(spec):
        raise error.ParseError(_('invalid token'), pos)
    return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))

class _aliasrules(parser.basealiasrules):
    """Parsing and expansion rule set of revset aliases"""
    _section = _('revset alias')

    @staticmethod
    def _parse(spec):
        """Parse alias declaration/definition ``spec``

        This allows symbol names to use also ``$`` as an initial letter
        (for backward compatibility), and callers of this function should
        examine whether ``$`` is used also for unexpected symbols or not.
        """
        return _parsewith(spec, syminitletters=_aliassyminitletters)

    @staticmethod
    def _trygetfunc(tree):
        if tree[0] == 'func' and tree[1][0] == 'symbol':
            return tree[1][1], getlist(tree[2])

def expandaliases(tree, aliases, warn=None):
    """Expand aliases in a tree, aliases is a list of (name, value) tuples"""
    aliases = _aliasrules.buildmap(aliases)
    tree = _aliasrules.expand(aliases, tree)
    # warn about problematic (but not referred) aliases
    if warn is not None:
        for name, alias in sorted(aliases.iteritems()):
            if alias.error and not alias.warned:
                warn(_('warning: %s\n') % (alias.error))
                alias.warned = True
    return tree

def foldconcat(tree):
    """Fold elements to be concatenated by `##`
    """
    if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
        return tree
    if tree[0] == '_concat':
        pending = [tree]
        l = []
        while pending:
            e = pending.pop()
            if e[0] == '_concat':
                pending.extend(reversed(e[1:]))
            elif e[0] in ('string', 'symbol'):
                l.append(e[1])
            else:
                msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
                raise error.ParseError(msg)
        return ('string', ''.join(l))
    else:
        return tuple(foldconcat(t) for t in tree)

def parse(spec, lookup=None):
    return _parsewith(spec, lookup=lookup)

def _quote(s):
    r"""Quote a value in order to make it safe for the revset engine.

    >>> _quote(b'asdf')
    "'asdf'"
    >>> _quote(b"asdf'\"")
    '\'asdf\\\'"\''
    >>> _quote(b'asdf\'')
    "'asdf\\''"
    >>> _quote(1)
    "'1'"
    """
    return "'%s'" % util.escapestr(pycompat.bytestr(s))

def _formatargtype(c, arg):
    if c == 'd':
        return '%d' % int(arg)
    elif c == 's':
        return _quote(arg)
    elif c == 'r':
        parse(arg) # make sure syntax errors are confined
        return '(%s)' % arg
    elif c == 'n':
        return _quote(node.hex(arg))
    elif c == 'b':
        try:
            return _quote(arg.branch())
        except AttributeError:
            raise TypeError
    raise error.ParseError(_('unexpected revspec format character %s') % c)

def _formatlistexp(s, t):
    l = len(s)
    if l == 0:
        return "_list('')"
    elif l == 1:
        return _formatargtype(t, s[0])
    elif t == 'd':
        return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
    elif t == 's':
        return "_list(%s)" % _quote("\0".join(s))
    elif t == 'n':
        return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
    elif t == 'b':
        try:
            return "_list('%s')" % "\0".join(a.branch() for a in s)
        except AttributeError:
            raise TypeError

    m = l // 2
    return '(%s or %s)' % (_formatlistexp(s[:m], t), _formatlistexp(s[m:], t))

def _formatparamexp(args, t):
    return ', '.join(_formatargtype(t, a) for a in args)

_formatlistfuncs = {
    'l': _formatlistexp,
    'p': _formatparamexp,
}

def formatspec(expr, *args):
    '''
    This is a convenience function for using revsets internally, and
    escapes arguments appropriately. Aliases are intentionally ignored
    so that intended expression behavior isn't accidentally subverted.

    Supported arguments:

    %r = revset expression, parenthesized
    %d = int(arg), no quoting
    %s = string(arg), escaped and single-quoted
    %b = arg.branch(), escaped and single-quoted
    %n = hex(arg), single-quoted
    %% = a literal '%'

    Prefixing the type with 'l' specifies a parenthesized list of that type,
    and 'p' specifies a list of function parameters of that type.

    >>> formatspec(b'%r:: and %lr', b'10 or 11', (b"this()", b"that()"))
    '(10 or 11):: and ((this()) or (that()))'
    >>> formatspec(b'%d:: and not %d::', 10, 20)
    '10:: and not 20::'
    >>> formatspec(b'%ld or %ld', [], [1])
    "_list('') or 1"
    >>> formatspec(b'keyword(%s)', b'foo\\xe9')
    "keyword('foo\\\\xe9')"
    >>> b = lambda: b'default'
    >>> b.branch = b
    >>> formatspec(b'branch(%b)', b)
    "branch('default')"
    >>> formatspec(b'root(%ls)', [b'a', b'b', b'c', b'd'])
    "root(_list('a\\\\x00b\\\\x00c\\\\x00d'))"
    >>> formatspec(b'sort(%r, %ps)', b':', [b'desc', b'user'])
    "sort((:), 'desc', 'user')"
    >>> formatspec('%ls', ['a', "'"])
    "_list('a\\\\x00\\\\'')"
    '''
    expr = pycompat.bytestr(expr)
    argiter = iter(args)
    ret = []
    pos = 0
    while pos < len(expr):
        q = expr.find('%', pos)
        if q < 0:
            ret.append(expr[pos:])
            break
        ret.append(expr[pos:q])
        pos = q + 1
        try:
            d = expr[pos]
        except IndexError:
            raise error.ParseError(_('incomplete revspec format character'))
        if d == '%':
            ret.append(d)
            pos += 1
            continue

        try:
            arg = next(argiter)
        except StopIteration:
            raise error.ParseError(_('missing argument for revspec'))
        f = _formatlistfuncs.get(d)
        if f:
            # a list of some type
            pos += 1
            try:
                d = expr[pos]
            except IndexError:
                raise error.ParseError(_('incomplete revspec format character'))
            try:
                ret.append(f(list(arg), d))
            except (TypeError, ValueError):
                raise error.ParseError(_('invalid argument for revspec'))
        else:
            try:
                ret.append(_formatargtype(d, arg))
            except (TypeError, ValueError):
                raise error.ParseError(_('invalid argument for revspec'))
        pos += 1

    try:
        next(argiter)
        raise error.ParseError(_('too many revspec arguments specified'))
    except StopIteration:
        pass
    return ''.join(ret)

def prettyformat(tree):
    return parser.prettyformat(tree, ('string', 'symbol'))

def depth(tree):
    if isinstance(tree, tuple):
        return max(map(depth, tree)) + 1
    else:
        return 0

def funcsused(tree):
    if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
        return set()
    else:
        funcs = set()
        for s in tree[1:]:
            funcs |= funcsused(s)
        if tree[0] == 'func':
            funcs.add(tree[1][1])
        return funcs

_hashre = util.re.compile('[0-9a-fA-F]{1,40}$')

def _ishashlikesymbol(symbol):
    """returns true if the symbol looks like a hash"""
    return _hashre.match(symbol)

def gethashlikesymbols(tree):
    """returns the list of symbols of the tree that look like hashes

    >>> gethashlikesymbols(('dagrange', ('symbol', '3'), ('symbol', 'abe3ff')))
    ['3', 'abe3ff']
    >>> gethashlikesymbols(('func', ('symbol', 'precursors'), ('symbol', '.')))
    []
    >>> gethashlikesymbols(('func', ('symbol', 'precursors'), ('symbol', '34')))
    ['34']
    >>> gethashlikesymbols(('symbol', 'abe3ffZ'))
    []
    """
    if not tree:
        return []

    if tree[0] == "symbol":
        if _ishashlikesymbol(tree[1]):
            return [tree[1]]
    elif len(tree) >= 3:
        results = []
        for subtree in tree[1:]:
            results += gethashlikesymbols(subtree)
        return results
    return []