1# revset.py - revision set queries for mercurial
2#
3# Copyright 2010 Olivia Mackall <olivia@selenic.com>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8from __future__ import absolute_import
9
10import re
11
12from .i18n import _
13from .pycompat import getattr
14from .node import (
15    bin,
16    nullrev,
17    wdirrev,
18)
19from . import (
20    dagop,
21    destutil,
22    diffutil,
23    encoding,
24    error,
25    grep as grepmod,
26    hbisect,
27    match as matchmod,
28    obsolete as obsmod,
29    obsutil,
30    pathutil,
31    phases,
32    pycompat,
33    registrar,
34    repoview,
35    revsetlang,
36    scmutil,
37    smartset,
38    stack as stackmod,
39    util,
40)
41from .utils import (
42    dateutil,
43    stringutil,
44    urlutil,
45)
46
47# helpers for processing parsed tree
48getsymbol = revsetlang.getsymbol
49getstring = revsetlang.getstring
50getinteger = revsetlang.getinteger
51getboolean = revsetlang.getboolean
52getlist = revsetlang.getlist
53getintrange = revsetlang.getintrange
54getargs = revsetlang.getargs
55getargsdict = revsetlang.getargsdict
56
57baseset = smartset.baseset
58generatorset = smartset.generatorset
59spanset = smartset.spanset
60fullreposet = smartset.fullreposet
61
62# revisions not included in all(), but populated if specified
63_virtualrevs = (nullrev, wdirrev)
64
65# Constants for ordering requirement, used in getset():
66#
67# If 'define', any nested functions and operations MAY change the ordering of
68# the entries in the set (but if changes the ordering, it MUST ALWAYS change
69# it). If 'follow', any nested functions and operations MUST take the ordering
70# specified by the first operand to the '&' operator.
71#
72# For instance,
73#
74#   X & (Y | Z)
75#   ^   ^^^^^^^
76#   |   follow
77#   define
78#
79# will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
80# of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
81#
82# 'any' means the order doesn't matter. For instance,
83#
84#   (X & !Y) | ancestors(Z)
85#         ^              ^
86#         any            any
87#
88# For 'X & !Y', 'X' decides the order and 'Y' is subtracted from 'X', so the
89# order of 'Y' does not matter. For 'ancestors(Z)', Z's order does not matter
90# since 'ancestors' does not care about the order of its argument.
91#
92# Currently, most revsets do not care about the order, so 'define' is
93# equivalent to 'follow' for them, and the resulting order is based on the
94# 'subset' parameter passed down to them:
95#
96#   m = revset.match(...)
97#   m(repo, subset, order=defineorder)
98#           ^^^^^^
99#      For most revsets, 'define' means using the order this subset provides
100#
101# There are a few revsets that always redefine the order if 'define' is
102# specified: 'sort(X)', 'reverse(X)', 'x:y'.
103anyorder = b'any'  # don't care the order, could be even random-shuffled
104defineorder = b'define'  # ALWAYS redefine, or ALWAYS follow the current order
105followorder = b'follow'  # MUST follow the current order
106
107# helpers
108
109
110def getset(repo, subset, x, order=defineorder):
111    if not x:
112        raise error.ParseError(_(b"missing argument"))
113    return methods[x[0]](repo, subset, *x[1:], order=order)
114
115
116def _getrevsource(repo, r):
117    extra = repo[r].extra()
118    for label in (b'source', b'transplant_source', b'rebase_source'):
119        if label in extra:
120            try:
121                return repo[extra[label]].rev()
122            except error.RepoLookupError:
123                pass
124    return None
125
126
127def _sortedb(xs):
128    return sorted(pycompat.rapply(pycompat.maybebytestr, xs))
129
130
131# operator methods
132
133
134def stringset(repo, subset, x, order):
135    if not x:
136        raise error.ParseError(_(b"empty string is not a valid revision"))
137    x = scmutil.intrev(scmutil.revsymbol(repo, x))
138    if x in subset or x in _virtualrevs and isinstance(subset, fullreposet):
139        return baseset([x])
140    return baseset()
141
142
143def rawsmartset(repo, subset, x, order):
144    """argument is already a smartset, use that directly"""
145    if order == followorder:
146        return subset & x
147    else:
148        return x & subset
149
150
151def rangeset(repo, subset, x, y, order):
152    m = getset(repo, fullreposet(repo), x)
153    n = getset(repo, fullreposet(repo), y)
154
155    if not m or not n:
156        return baseset()
157    return _makerangeset(repo, subset, m.first(), n.last(), order)
158
159
160def rangeall(repo, subset, x, order):
161    assert x is None
162    return _makerangeset(repo, subset, 0, repo.changelog.tiprev(), order)
163
164
165def rangepre(repo, subset, y, order):
166    # ':y' can't be rewritten to '0:y' since '0' may be hidden
167    n = getset(repo, fullreposet(repo), y)
168    if not n:
169        return baseset()
170    return _makerangeset(repo, subset, 0, n.last(), order)
171
172
173def rangepost(repo, subset, x, order):
174    m = getset(repo, fullreposet(repo), x)
175    if not m:
176        return baseset()
177    return _makerangeset(
178        repo, subset, m.first(), repo.changelog.tiprev(), order
179    )
180
181
182def _makerangeset(repo, subset, m, n, order):
183    if m == n:
184        r = baseset([m])
185    elif n == wdirrev:
186        r = spanset(repo, m, len(repo)) + baseset([n])
187    elif m == wdirrev:
188        r = baseset([m]) + spanset(repo, repo.changelog.tiprev(), n - 1)
189    elif m < n:
190        r = spanset(repo, m, n + 1)
191    else:
192        r = spanset(repo, m, n - 1)
193
194    if order == defineorder:
195        return r & subset
196    else:
197        # carrying the sorting over when possible would be more efficient
198        return subset & r
199
200
201def dagrange(repo, subset, x, y, order):
202    r = fullreposet(repo)
203    xs = dagop.reachableroots(
204        repo, getset(repo, r, x), getset(repo, r, y), includepath=True
205    )
206    return subset & xs
207
208
209def andset(repo, subset, x, y, order):
210    if order == anyorder:
211        yorder = anyorder
212    else:
213        yorder = followorder
214    return getset(repo, getset(repo, subset, x, order), y, yorder)
215
216
217def andsmallyset(repo, subset, x, y, order):
218    # 'andsmally(x, y)' is equivalent to 'and(x, y)', but faster when y is small
219    if order == anyorder:
220        yorder = anyorder
221    else:
222        yorder = followorder
223    return getset(repo, getset(repo, subset, y, yorder), x, order)
224
225
226def differenceset(repo, subset, x, y, order):
227    return getset(repo, subset, x, order) - getset(repo, subset, y, anyorder)
228
229
230def _orsetlist(repo, subset, xs, order):
231    assert xs
232    if len(xs) == 1:
233        return getset(repo, subset, xs[0], order)
234    p = len(xs) // 2
235    a = _orsetlist(repo, subset, xs[:p], order)
236    b = _orsetlist(repo, subset, xs[p:], order)
237    return a + b
238
239
240def orset(repo, subset, x, order):
241    xs = getlist(x)
242    if not xs:
243        return baseset()
244    if order == followorder:
245        # slow path to take the subset order
246        return subset & _orsetlist(repo, fullreposet(repo), xs, anyorder)
247    else:
248        return _orsetlist(repo, subset, xs, order)
249
250
251def notset(repo, subset, x, order):
252    return subset - getset(repo, subset, x, anyorder)
253
254
255def relationset(repo, subset, x, y, order):
256    # this is pretty basic implementation of 'x#y' operator, still
257    # experimental so undocumented. see the wiki for further ideas.
258    # https://www.mercurial-scm.org/wiki/RevsetOperatorPlan
259    rel = getsymbol(y)
260    if rel in relations:
261        return relations[rel](repo, subset, x, rel, order)
262
263    relnames = [r for r in relations.keys() if len(r) > 1]
264    raise error.UnknownIdentifier(rel, relnames)
265
266
267def _splitrange(a, b):
268    """Split range with bounds a and b into two ranges at 0 and return two
269    tuples of numbers for use as startdepth and stopdepth arguments of
270    revancestors and revdescendants.
271
272    >>> _splitrange(-10, -5)     # [-10:-5]
273    ((5, 11), (None, None))
274    >>> _splitrange(5, 10)       # [5:10]
275    ((None, None), (5, 11))
276    >>> _splitrange(-10, 10)     # [-10:10]
277    ((0, 11), (0, 11))
278    >>> _splitrange(-10, 0)      # [-10:0]
279    ((0, 11), (None, None))
280    >>> _splitrange(0, 10)       # [0:10]
281    ((None, None), (0, 11))
282    >>> _splitrange(0, 0)        # [0:0]
283    ((0, 1), (None, None))
284    >>> _splitrange(1, -1)       # [1:-1]
285    ((None, None), (None, None))
286    """
287    ancdepths = (None, None)
288    descdepths = (None, None)
289    if a == b == 0:
290        ancdepths = (0, 1)
291    if a < 0:
292        ancdepths = (-min(b, 0), -a + 1)
293    if b > 0:
294        descdepths = (max(a, 0), b + 1)
295    return ancdepths, descdepths
296
297
298def generationsrel(repo, subset, x, rel, order):
299    z = (b'rangeall', None)
300    return generationssubrel(repo, subset, x, rel, z, order)
301
302
303def generationssubrel(repo, subset, x, rel, z, order):
304    # TODO: rewrite tests, and drop startdepth argument from ancestors() and
305    # descendants() predicates
306    a, b = getintrange(
307        z,
308        _(b'relation subscript must be an integer or a range'),
309        _(b'relation subscript bounds must be integers'),
310        deffirst=-(dagop.maxlogdepth - 1),
311        deflast=+(dagop.maxlogdepth - 1),
312    )
313    (ancstart, ancstop), (descstart, descstop) = _splitrange(a, b)
314
315    if ancstart is None and descstart is None:
316        return baseset()
317
318    revs = getset(repo, fullreposet(repo), x)
319    if not revs:
320        return baseset()
321
322    if ancstart is not None and descstart is not None:
323        s = dagop.revancestors(repo, revs, False, ancstart, ancstop)
324        s += dagop.revdescendants(repo, revs, False, descstart, descstop)
325    elif ancstart is not None:
326        s = dagop.revancestors(repo, revs, False, ancstart, ancstop)
327    elif descstart is not None:
328        s = dagop.revdescendants(repo, revs, False, descstart, descstop)
329
330    return subset & s
331
332
333def relsubscriptset(repo, subset, x, y, z, order):
334    # this is pretty basic implementation of 'x#y[z]' operator, still
335    # experimental so undocumented. see the wiki for further ideas.
336    # https://www.mercurial-scm.org/wiki/RevsetOperatorPlan
337    rel = getsymbol(y)
338    if rel in subscriptrelations:
339        return subscriptrelations[rel](repo, subset, x, rel, z, order)
340
341    relnames = [r for r in subscriptrelations.keys() if len(r) > 1]
342    raise error.UnknownIdentifier(rel, relnames)
343
344
345def subscriptset(repo, subset, x, y, order):
346    raise error.ParseError(_(b"can't use a subscript in this context"))
347
348
349def listset(repo, subset, *xs, **opts):
350    raise error.ParseError(
351        _(b"can't use a list in this context"),
352        hint=_(b'see \'hg help "revsets.x or y"\''),
353    )
354
355
356def keyvaluepair(repo, subset, k, v, order):
357    raise error.ParseError(_(b"can't use a key-value pair in this context"))
358
359
360def func(repo, subset, a, b, order):
361    f = getsymbol(a)
362    if f in symbols:
363        func = symbols[f]
364        if getattr(func, '_takeorder', False):
365            return func(repo, subset, b, order)
366        return func(repo, subset, b)
367
368    keep = lambda fn: getattr(fn, '__doc__', None) is not None
369
370    syms = [s for (s, fn) in symbols.items() if keep(fn)]
371    raise error.UnknownIdentifier(f, syms)
372
373
374# functions
375
376# symbols are callables like:
377#   fn(repo, subset, x)
378# with:
379#   repo - current repository instance
380#   subset - of revisions to be examined
381#   x - argument in tree form
382symbols = revsetlang.symbols
383
384# symbols which can't be used for a DoS attack for any given input
385# (e.g. those which accept regexes as plain strings shouldn't be included)
386# functions that just return a lot of changesets (like all) don't count here
387safesymbols = set()
388
389predicate = registrar.revsetpredicate()
390
391
392@predicate(b'_destupdate')
393def _destupdate(repo, subset, x):
394    # experimental revset for update destination
395    args = getargsdict(x, b'limit', b'clean')
396    return subset & baseset(
397        [destutil.destupdate(repo, **pycompat.strkwargs(args))[0]]
398    )
399
400
401@predicate(b'_destmerge')
402def _destmerge(repo, subset, x):
403    # experimental revset for merge destination
404    sourceset = None
405    if x is not None:
406        sourceset = getset(repo, fullreposet(repo), x)
407    return subset & baseset([destutil.destmerge(repo, sourceset=sourceset)])
408
409
410@predicate(b'adds(pattern)', safe=True, weight=30)
411def adds(repo, subset, x):
412    """Changesets that add a file matching pattern.
413
414    The pattern without explicit kind like ``glob:`` is expected to be
415    relative to the current directory and match against a file or a
416    directory.
417    """
418    # i18n: "adds" is a keyword
419    pat = getstring(x, _(b"adds requires a pattern"))
420    return checkstatus(repo, subset, pat, 'added')
421
422
423@predicate(b'ancestor(*changeset)', safe=True, weight=0.5)
424def ancestor(repo, subset, x):
425    """A greatest common ancestor of the changesets.
426
427    Accepts 0 or more changesets.
428    Will return empty list when passed no args.
429    Greatest common ancestor of a single changeset is that changeset.
430    """
431    reviter = iter(orset(repo, fullreposet(repo), x, order=anyorder))
432    try:
433        anc = repo[next(reviter)]
434    except StopIteration:
435        return baseset()
436    for r in reviter:
437        anc = anc.ancestor(repo[r])
438
439    r = scmutil.intrev(anc)
440    if r in subset:
441        return baseset([r])
442    return baseset()
443
444
445def _ancestors(
446    repo, subset, x, followfirst=False, startdepth=None, stopdepth=None
447):
448    heads = getset(repo, fullreposet(repo), x)
449    if not heads:
450        return baseset()
451    s = dagop.revancestors(repo, heads, followfirst, startdepth, stopdepth)
452    return subset & s
453
454
455@predicate(b'ancestors(set[, depth])', safe=True)
456def ancestors(repo, subset, x):
457    """Changesets that are ancestors of changesets in set, including the
458    given changesets themselves.
459
460    If depth is specified, the result only includes changesets up to
461    the specified generation.
462    """
463    # startdepth is for internal use only until we can decide the UI
464    args = getargsdict(x, b'ancestors', b'set depth startdepth')
465    if b'set' not in args:
466        # i18n: "ancestors" is a keyword
467        raise error.ParseError(_(b'ancestors takes at least 1 argument'))
468    startdepth = stopdepth = None
469    if b'startdepth' in args:
470        n = getinteger(
471            args[b'startdepth'], b"ancestors expects an integer startdepth"
472        )
473        if n < 0:
474            raise error.ParseError(b"negative startdepth")
475        startdepth = n
476    if b'depth' in args:
477        # i18n: "ancestors" is a keyword
478        n = getinteger(args[b'depth'], _(b"ancestors expects an integer depth"))
479        if n < 0:
480            raise error.ParseError(_(b"negative depth"))
481        stopdepth = n + 1
482    return _ancestors(
483        repo, subset, args[b'set'], startdepth=startdepth, stopdepth=stopdepth
484    )
485
486
487@predicate(b'_firstancestors', safe=True)
488def _firstancestors(repo, subset, x):
489    # ``_firstancestors(set)``
490    # Like ``ancestors(set)`` but follows only the first parents.
491    return _ancestors(repo, subset, x, followfirst=True)
492
493
494def _childrenspec(repo, subset, x, n, order):
495    """Changesets that are the Nth child of a changeset
496    in set.
497    """
498    cs = set()
499    for r in getset(repo, fullreposet(repo), x):
500        for i in range(n):
501            c = repo[r].children()
502            if len(c) == 0:
503                break
504            if len(c) > 1:
505                raise error.RepoLookupError(
506                    _(b"revision in set has more than one child")
507                )
508            r = c[0].rev()
509        else:
510            cs.add(r)
511    return subset & cs
512
513
514def ancestorspec(repo, subset, x, n, order):
515    """``set~n``
516    Changesets that are the Nth ancestor (first parents only) of a changeset
517    in set.
518    """
519    n = getinteger(n, _(b"~ expects a number"))
520    if n < 0:
521        # children lookup
522        return _childrenspec(repo, subset, x, -n, order)
523    ps = set()
524    cl = repo.changelog
525    for r in getset(repo, fullreposet(repo), x):
526        for i in range(n):
527            try:
528                r = cl.parentrevs(r)[0]
529            except error.WdirUnsupported:
530                r = repo[r].p1().rev()
531        ps.add(r)
532    return subset & ps
533
534
535@predicate(b'author(string)', safe=True, weight=10)
536def author(repo, subset, x):
537    """Alias for ``user(string)``."""
538    # i18n: "author" is a keyword
539    n = getstring(x, _(b"author requires a string"))
540    kind, pattern, matcher = _substringmatcher(n, casesensitive=False)
541    return subset.filter(
542        lambda x: matcher(repo[x].user()), condrepr=(b'<user %r>', n)
543    )
544
545
546@predicate(b'bisect(string)', safe=True)
547def bisect(repo, subset, x):
548    """Changesets marked in the specified bisect status:
549
550    - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
551    - ``goods``, ``bads``      : csets topologically good/bad
552    - ``range``              : csets taking part in the bisection
553    - ``pruned``             : csets that are goods, bads or skipped
554    - ``untested``           : csets whose fate is yet unknown
555    - ``ignored``            : csets ignored due to DAG topology
556    - ``current``            : the cset currently being bisected
557    """
558    # i18n: "bisect" is a keyword
559    status = getstring(x, _(b"bisect requires a string")).lower()
560    state = set(hbisect.get(repo, status))
561    return subset & state
562
563
564# Backward-compatibility
565# - no help entry so that we do not advertise it any more
566@predicate(b'bisected', safe=True)
567def bisected(repo, subset, x):
568    return bisect(repo, subset, x)
569
570
571@predicate(b'bookmark([name])', safe=True)
572def bookmark(repo, subset, x):
573    """The named bookmark or all bookmarks.
574
575    Pattern matching is supported for `name`. See :hg:`help revisions.patterns`.
576    """
577    # i18n: "bookmark" is a keyword
578    args = getargs(x, 0, 1, _(b'bookmark takes one or no arguments'))
579    if args:
580        bm = getstring(
581            args[0],
582            # i18n: "bookmark" is a keyword
583            _(b'the argument to bookmark must be a string'),
584        )
585        kind, pattern, matcher = stringutil.stringmatcher(bm)
586        bms = set()
587        if kind == b'literal':
588            if bm == pattern:
589                pattern = repo._bookmarks.expandname(pattern)
590            bmrev = repo._bookmarks.get(pattern, None)
591            if not bmrev:
592                raise error.RepoLookupError(
593                    _(b"bookmark '%s' does not exist") % pattern
594                )
595            bms.add(repo[bmrev].rev())
596        else:
597            matchrevs = set()
598            for name, bmrev in pycompat.iteritems(repo._bookmarks):
599                if matcher(name):
600                    matchrevs.add(bmrev)
601            for bmrev in matchrevs:
602                bms.add(repo[bmrev].rev())
603    else:
604        bms = {repo[r].rev() for r in repo._bookmarks.values()}
605    bms -= {nullrev}
606    return subset & bms
607
608
609@predicate(b'branch(string or set)', safe=True, weight=10)
610def branch(repo, subset, x):
611    """
612    All changesets belonging to the given branch or the branches of the given
613    changesets.
614
615    Pattern matching is supported for `string`. See
616    :hg:`help revisions.patterns`.
617    """
618    getbi = repo.revbranchcache().branchinfo
619
620    def getbranch(r):
621        try:
622            return getbi(r)[0]
623        except error.WdirUnsupported:
624            return repo[r].branch()
625
626    try:
627        b = getstring(x, b'')
628    except error.ParseError:
629        # not a string, but another revspec, e.g. tip()
630        pass
631    else:
632        kind, pattern, matcher = stringutil.stringmatcher(b)
633        if kind == b'literal':
634            # note: falls through to the revspec case if no branch with
635            # this name exists and pattern kind is not specified explicitly
636            if repo.branchmap().hasbranch(pattern):
637                return subset.filter(
638                    lambda r: matcher(getbranch(r)),
639                    condrepr=(b'<branch %r>', b),
640                )
641            if b.startswith(b'literal:'):
642                raise error.RepoLookupError(
643                    _(b"branch '%s' does not exist") % pattern
644                )
645        else:
646            return subset.filter(
647                lambda r: matcher(getbranch(r)), condrepr=(b'<branch %r>', b)
648            )
649
650    s = getset(repo, fullreposet(repo), x)
651    b = set()
652    for r in s:
653        b.add(getbranch(r))
654    c = s.__contains__
655    return subset.filter(
656        lambda r: c(r) or getbranch(r) in b,
657        condrepr=lambda: b'<branch %r>' % _sortedb(b),
658    )
659
660
661@predicate(b'phasedivergent()', safe=True)
662def phasedivergent(repo, subset, x):
663    """Mutable changesets marked as successors of public changesets.
664
665    Only non-public and non-obsolete changesets can be `phasedivergent`.
666    (EXPERIMENTAL)
667    """
668    # i18n: "phasedivergent" is a keyword
669    getargs(x, 0, 0, _(b"phasedivergent takes no arguments"))
670    phasedivergent = obsmod.getrevs(repo, b'phasedivergent')
671    return subset & phasedivergent
672
673
674@predicate(b'bundle()', safe=True)
675def bundle(repo, subset, x):
676    """Changesets in the bundle.
677
678    Bundle must be specified by the -R option."""
679
680    try:
681        bundlerevs = repo.changelog.bundlerevs
682    except AttributeError:
683        raise error.Abort(_(b"no bundle provided - specify with -R"))
684    return subset & bundlerevs
685
686
687def checkstatus(repo, subset, pat, field):
688    """Helper for status-related revsets (adds, removes, modifies).
689    The field parameter says which kind is desired.
690    """
691    hasset = matchmod.patkind(pat) == b'set'
692
693    mcache = [None]
694
695    def matches(x):
696        c = repo[x]
697        if not mcache[0] or hasset:
698            mcache[0] = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
699        m = mcache[0]
700        fname = None
701
702        assert m is not None  # help pytype
703        if not m.anypats() and len(m.files()) == 1:
704            fname = m.files()[0]
705        if fname is not None:
706            if fname not in c.files():
707                return False
708        else:
709            if not any(m(f) for f in c.files()):
710                return False
711        files = getattr(repo.status(c.p1().node(), c.node()), field)
712        if fname is not None:
713            if fname in files:
714                return True
715        else:
716            if any(m(f) for f in files):
717                return True
718
719    return subset.filter(
720        matches, condrepr=(b'<status.%s %r>', pycompat.sysbytes(field), pat)
721    )
722
723
724def _children(repo, subset, parentset):
725    if not parentset:
726        return baseset()
727    cs = set()
728    pr = repo.changelog.parentrevs
729    minrev = parentset.min()
730    for r in subset:
731        if r <= minrev:
732            continue
733        p1, p2 = pr(r)
734        if p1 in parentset:
735            cs.add(r)
736        if p2 != nullrev and p2 in parentset:
737            cs.add(r)
738    return baseset(cs)
739
740
741@predicate(b'children(set)', safe=True)
742def children(repo, subset, x):
743    """Child changesets of changesets in set."""
744    s = getset(repo, fullreposet(repo), x)
745    cs = _children(repo, subset, s)
746    return subset & cs
747
748
749@predicate(b'closed()', safe=True, weight=10)
750def closed(repo, subset, x):
751    """Changeset is closed."""
752    # i18n: "closed" is a keyword
753    getargs(x, 0, 0, _(b"closed takes no arguments"))
754    return subset.filter(
755        lambda r: repo[r].closesbranch(), condrepr=b'<branch closed>'
756    )
757
758
759# for internal use
760@predicate(b'_commonancestorheads(set)', safe=True)
761def _commonancestorheads(repo, subset, x):
762    # This is an internal method is for quickly calculating "heads(::x and
763    # ::y)"
764
765    # These greatest common ancestors are the same ones that the consensus bid
766    # merge will find.
767    startrevs = getset(repo, fullreposet(repo), x, order=anyorder)
768
769    ancs = repo.changelog._commonancestorsheads(*list(startrevs))
770    return subset & baseset(ancs)
771
772
773@predicate(b'commonancestors(set)', safe=True)
774def commonancestors(repo, subset, x):
775    """Changesets that are ancestors of every changeset in set."""
776    startrevs = getset(repo, fullreposet(repo), x, order=anyorder)
777    if not startrevs:
778        return baseset()
779    for r in startrevs:
780        subset &= dagop.revancestors(repo, baseset([r]))
781    return subset
782
783
784@predicate(b'conflictlocal()', safe=True)
785def conflictlocal(repo, subset, x):
786    """The local side of the merge, if currently in an unresolved merge.
787
788    "merge" here includes merge conflicts from e.g. 'hg rebase' or 'hg graft'.
789    """
790    getargs(x, 0, 0, _(b"conflictlocal takes no arguments"))
791    from . import mergestate as mergestatemod
792
793    mergestate = mergestatemod.mergestate.read(repo)
794    if mergestate.active() and repo.changelog.hasnode(mergestate.local):
795        return subset & {repo.changelog.rev(mergestate.local)}
796
797    return baseset()
798
799
800@predicate(b'conflictother()', safe=True)
801def conflictother(repo, subset, x):
802    """The other side of the merge, if currently in an unresolved merge.
803
804    "merge" here includes merge conflicts from e.g. 'hg rebase' or 'hg graft'.
805    """
806    getargs(x, 0, 0, _(b"conflictother takes no arguments"))
807    from . import mergestate as mergestatemod
808
809    mergestate = mergestatemod.mergestate.read(repo)
810    if mergestate.active() and repo.changelog.hasnode(mergestate.other):
811        return subset & {repo.changelog.rev(mergestate.other)}
812
813    return baseset()
814
815
816@predicate(b'contains(pattern)', weight=100)
817def contains(repo, subset, x):
818    """The revision's manifest contains a file matching pattern (but might not
819    modify it). See :hg:`help patterns` for information about file patterns.
820
821    The pattern without explicit kind like ``glob:`` is expected to be
822    relative to the current directory and match against a file exactly
823    for efficiency.
824    """
825    # i18n: "contains" is a keyword
826    pat = getstring(x, _(b"contains requires a pattern"))
827
828    def matches(x):
829        if not matchmod.patkind(pat):
830            pats = pathutil.canonpath(repo.root, repo.getcwd(), pat)
831            if pats in repo[x]:
832                return True
833        else:
834            c = repo[x]
835            m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
836            for f in c.manifest():
837                if m(f):
838                    return True
839        return False
840
841    return subset.filter(matches, condrepr=(b'<contains %r>', pat))
842
843
844@predicate(b'converted([id])', safe=True)
845def converted(repo, subset, x):
846    """Changesets converted from the given identifier in the old repository if
847    present, or all converted changesets if no identifier is specified.
848    """
849
850    # There is exactly no chance of resolving the revision, so do a simple
851    # string compare and hope for the best
852
853    rev = None
854    # i18n: "converted" is a keyword
855    l = getargs(x, 0, 1, _(b'converted takes one or no arguments'))
856    if l:
857        # i18n: "converted" is a keyword
858        rev = getstring(l[0], _(b'converted requires a revision'))
859
860    def _matchvalue(r):
861        source = repo[r].extra().get(b'convert_revision', None)
862        return source is not None and (rev is None or source.startswith(rev))
863
864    return subset.filter(
865        lambda r: _matchvalue(r), condrepr=(b'<converted %r>', rev)
866    )
867
868
869@predicate(b'date(interval)', safe=True, weight=10)
870def date(repo, subset, x):
871    """Changesets within the interval, see :hg:`help dates`."""
872    # i18n: "date" is a keyword
873    ds = getstring(x, _(b"date requires a string"))
874    dm = dateutil.matchdate(ds)
875    return subset.filter(
876        lambda x: dm(repo[x].date()[0]), condrepr=(b'<date %r>', ds)
877    )
878
879
880@predicate(b'desc(string)', safe=True, weight=10)
881def desc(repo, subset, x):
882    """Search commit message for string. The match is case-insensitive.
883
884    Pattern matching is supported for `string`. See
885    :hg:`help revisions.patterns`.
886    """
887    # i18n: "desc" is a keyword
888    ds = getstring(x, _(b"desc requires a string"))
889
890    kind, pattern, matcher = _substringmatcher(ds, casesensitive=False)
891
892    return subset.filter(
893        lambda r: matcher(repo[r].description()), condrepr=(b'<desc %r>', ds)
894    )
895
896
897def _descendants(
898    repo, subset, x, followfirst=False, startdepth=None, stopdepth=None
899):
900    roots = getset(repo, fullreposet(repo), x)
901    if not roots:
902        return baseset()
903    s = dagop.revdescendants(repo, roots, followfirst, startdepth, stopdepth)
904    return subset & s
905
906
907@predicate(b'descendants(set[, depth])', safe=True)
908def descendants(repo, subset, x):
909    """Changesets which are descendants of changesets in set, including the
910    given changesets themselves.
911
912    If depth is specified, the result only includes changesets up to
913    the specified generation.
914    """
915    # startdepth is for internal use only until we can decide the UI
916    args = getargsdict(x, b'descendants', b'set depth startdepth')
917    if b'set' not in args:
918        # i18n: "descendants" is a keyword
919        raise error.ParseError(_(b'descendants takes at least 1 argument'))
920    startdepth = stopdepth = None
921    if b'startdepth' in args:
922        n = getinteger(
923            args[b'startdepth'], b"descendants expects an integer startdepth"
924        )
925        if n < 0:
926            raise error.ParseError(b"negative startdepth")
927        startdepth = n
928    if b'depth' in args:
929        # i18n: "descendants" is a keyword
930        n = getinteger(
931            args[b'depth'], _(b"descendants expects an integer depth")
932        )
933        if n < 0:
934            raise error.ParseError(_(b"negative depth"))
935        stopdepth = n + 1
936    return _descendants(
937        repo, subset, args[b'set'], startdepth=startdepth, stopdepth=stopdepth
938    )
939
940
941@predicate(b'_firstdescendants', safe=True)
942def _firstdescendants(repo, subset, x):
943    # ``_firstdescendants(set)``
944    # Like ``descendants(set)`` but follows only the first parents.
945    return _descendants(repo, subset, x, followfirst=True)
946
947
948@predicate(b'destination([set])', safe=True, weight=10)
949def destination(repo, subset, x):
950    """Changesets that were created by a graft, transplant or rebase operation,
951    with the given revisions specified as the source.  Omitting the optional set
952    is the same as passing all().
953    """
954    if x is not None:
955        sources = getset(repo, fullreposet(repo), x)
956    else:
957        sources = fullreposet(repo)
958
959    dests = set()
960
961    # subset contains all of the possible destinations that can be returned, so
962    # iterate over them and see if their source(s) were provided in the arg set.
963    # Even if the immediate src of r is not in the arg set, src's source (or
964    # further back) may be.  Scanning back further than the immediate src allows
965    # transitive transplants and rebases to yield the same results as transitive
966    # grafts.
967    for r in subset:
968        src = _getrevsource(repo, r)
969        lineage = None
970
971        while src is not None:
972            if lineage is None:
973                lineage = list()
974
975            lineage.append(r)
976
977            # The visited lineage is a match if the current source is in the arg
978            # set.  Since every candidate dest is visited by way of iterating
979            # subset, any dests further back in the lineage will be tested by a
980            # different iteration over subset.  Likewise, if the src was already
981            # selected, the current lineage can be selected without going back
982            # further.
983            if src in sources or src in dests:
984                dests.update(lineage)
985                break
986
987            r = src
988            src = _getrevsource(repo, r)
989
990    return subset.filter(
991        dests.__contains__,
992        condrepr=lambda: b'<destination %r>' % _sortedb(dests),
993    )
994
995
996@predicate(b'diffcontains(pattern)', weight=110)
997def diffcontains(repo, subset, x):
998    """Search revision differences for when the pattern was added or removed.
999
1000    The pattern may be a substring literal or a regular expression. See
1001    :hg:`help revisions.patterns`.
1002    """
1003    args = getargsdict(x, b'diffcontains', b'pattern')
1004    if b'pattern' not in args:
1005        # i18n: "diffcontains" is a keyword
1006        raise error.ParseError(_(b'diffcontains takes at least 1 argument'))
1007
1008    pattern = getstring(
1009        args[b'pattern'], _(b'diffcontains requires a string pattern')
1010    )
1011    regexp = stringutil.substringregexp(pattern, re.M)
1012
1013    # TODO: add support for file pattern and --follow. For example,
1014    # diffcontains(pattern[, set]) where set may be file(pattern) or
1015    # follow(pattern), and we'll eventually add a support for narrowing
1016    # files by revset?
1017    fmatch = matchmod.always()
1018
1019    def makefilematcher(ctx):
1020        return fmatch
1021
1022    # TODO: search in a windowed way
1023    searcher = grepmod.grepsearcher(repo.ui, repo, regexp, diff=True)
1024
1025    def testdiff(rev):
1026        # consume the generator to discard revfiles/matches cache
1027        found = False
1028        for fn, ctx, pstates, states in searcher.searchfiles(
1029            baseset([rev]), makefilematcher
1030        ):
1031            if next(grepmod.difflinestates(pstates, states), None):
1032                found = True
1033        return found
1034
1035    return subset.filter(testdiff, condrepr=(b'<diffcontains %r>', pattern))
1036
1037
1038@predicate(b'contentdivergent()', safe=True)
1039def contentdivergent(repo, subset, x):
1040    """
1041    Final successors of changesets with an alternative set of final
1042    successors. (EXPERIMENTAL)
1043    """
1044    # i18n: "contentdivergent" is a keyword
1045    getargs(x, 0, 0, _(b"contentdivergent takes no arguments"))
1046    contentdivergent = obsmod.getrevs(repo, b'contentdivergent')
1047    return subset & contentdivergent
1048
1049
1050@predicate(b'expectsize(set[, size])', safe=True, takeorder=True)
1051def expectsize(repo, subset, x, order):
1052    """Return the given revset if size matches the revset size.
1053    Abort if the revset doesn't expect given size.
1054    size can either be an integer range or an integer.
1055
1056    For example, ``expectsize(0:1, 3:5)`` will abort as revset size is 2 and
1057    2 is not between 3 and 5 inclusive."""
1058
1059    args = getargsdict(x, b'expectsize', b'set size')
1060    minsize = 0
1061    maxsize = len(repo) + 1
1062    err = b''
1063    if b'size' not in args or b'set' not in args:
1064        raise error.ParseError(_(b'invalid set of arguments'))
1065    minsize, maxsize = getintrange(
1066        args[b'size'],
1067        _(b'expectsize requires a size range or a positive integer'),
1068        _(b'size range bounds must be integers'),
1069        minsize,
1070        maxsize,
1071    )
1072    if minsize < 0 or maxsize < 0:
1073        raise error.ParseError(_(b'negative size'))
1074    rev = getset(repo, fullreposet(repo), args[b'set'], order=order)
1075    if minsize != maxsize and (len(rev) < minsize or len(rev) > maxsize):
1076        err = _(b'revset size mismatch. expected between %d and %d, got %d') % (
1077            minsize,
1078            maxsize,
1079            len(rev),
1080        )
1081    elif minsize == maxsize and len(rev) != minsize:
1082        err = _(b'revset size mismatch. expected %d, got %d') % (
1083            minsize,
1084            len(rev),
1085        )
1086    if err:
1087        raise error.RepoLookupError(err)
1088    if order == followorder:
1089        return subset & rev
1090    else:
1091        return rev & subset
1092
1093
1094@predicate(b'extdata(source)', safe=False, weight=100)
1095def extdata(repo, subset, x):
1096    """Changesets in the specified extdata source. (EXPERIMENTAL)"""
1097    # i18n: "extdata" is a keyword
1098    args = getargsdict(x, b'extdata', b'source')
1099    source = getstring(
1100        args.get(b'source'),
1101        # i18n: "extdata" is a keyword
1102        _(b'extdata takes at least 1 string argument'),
1103    )
1104    data = scmutil.extdatasource(repo, source)
1105    return subset & baseset(data)
1106
1107
1108@predicate(b'extinct()', safe=True)
1109def extinct(repo, subset, x):
1110    """Obsolete changesets with obsolete descendants only. (EXPERIMENTAL)"""
1111    # i18n: "extinct" is a keyword
1112    getargs(x, 0, 0, _(b"extinct takes no arguments"))
1113    extincts = obsmod.getrevs(repo, b'extinct')
1114    return subset & extincts
1115
1116
1117@predicate(b'extra(label, [value])', safe=True)
1118def extra(repo, subset, x):
1119    """Changesets with the given label in the extra metadata, with the given
1120    optional value.
1121
1122    Pattern matching is supported for `value`. See
1123    :hg:`help revisions.patterns`.
1124    """
1125    args = getargsdict(x, b'extra', b'label value')
1126    if b'label' not in args:
1127        # i18n: "extra" is a keyword
1128        raise error.ParseError(_(b'extra takes at least 1 argument'))
1129    # i18n: "extra" is a keyword
1130    label = getstring(
1131        args[b'label'], _(b'first argument to extra must be a string')
1132    )
1133    value = None
1134
1135    if b'value' in args:
1136        # i18n: "extra" is a keyword
1137        value = getstring(
1138            args[b'value'], _(b'second argument to extra must be a string')
1139        )
1140        kind, value, matcher = stringutil.stringmatcher(value)
1141
1142    def _matchvalue(r):
1143        extra = repo[r].extra()
1144        return label in extra and (value is None or matcher(extra[label]))
1145
1146    return subset.filter(
1147        lambda r: _matchvalue(r), condrepr=(b'<extra[%r] %r>', label, value)
1148    )
1149
1150
1151@predicate(b'filelog(pattern)', safe=True)
1152def filelog(repo, subset, x):
1153    """Changesets connected to the specified filelog.
1154
1155    For performance reasons, visits only revisions mentioned in the file-level
1156    filelog, rather than filtering through all changesets (much faster, but
1157    doesn't include deletes or duplicate changes). For a slower, more accurate
1158    result, use ``file()``.
1159
1160    The pattern without explicit kind like ``glob:`` is expected to be
1161    relative to the current directory and match against a file exactly
1162    for efficiency.
1163    """
1164
1165    # i18n: "filelog" is a keyword
1166    pat = getstring(x, _(b"filelog requires a pattern"))
1167    s = set()
1168    cl = repo.changelog
1169
1170    if not matchmod.patkind(pat):
1171        f = pathutil.canonpath(repo.root, repo.getcwd(), pat)
1172        files = [f]
1173    else:
1174        m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None])
1175        files = (f for f in repo[None] if m(f))
1176
1177    for f in files:
1178        fl = repo.file(f)
1179        known = {}
1180        scanpos = 0
1181        for fr in list(fl):
1182            fn = fl.node(fr)
1183            if fn in known:
1184                s.add(known[fn])
1185                continue
1186
1187            lr = fl.linkrev(fr)
1188            if lr in cl:
1189                s.add(lr)
1190            elif scanpos is not None:
1191                # lowest matching changeset is filtered, scan further
1192                # ahead in changelog
1193                start = max(lr, scanpos) + 1
1194                scanpos = None
1195                for r in cl.revs(start):
1196                    # minimize parsing of non-matching entries
1197                    if f in cl.revision(r) and f in cl.readfiles(r):
1198                        try:
1199                            # try to use manifest delta fastpath
1200                            n = repo[r].filenode(f)
1201                            if n not in known:
1202                                if n == fn:
1203                                    s.add(r)
1204                                    scanpos = r
1205                                    break
1206                                else:
1207                                    known[n] = r
1208                        except error.ManifestLookupError:
1209                            # deletion in changelog
1210                            continue
1211
1212    return subset & s
1213
1214
1215@predicate(b'first(set, [n])', safe=True, takeorder=True, weight=0)
1216def first(repo, subset, x, order):
1217    """An alias for limit()."""
1218    return limit(repo, subset, x, order)
1219
1220
1221def _follow(repo, subset, x, name, followfirst=False):
1222    args = getargsdict(x, name, b'file startrev')
1223    revs = None
1224    if b'startrev' in args:
1225        revs = getset(repo, fullreposet(repo), args[b'startrev'])
1226    if b'file' in args:
1227        x = getstring(args[b'file'], _(b"%s expected a pattern") % name)
1228        if revs is None:
1229            revs = [None]
1230        fctxs = []
1231        for r in revs:
1232            ctx = mctx = repo[r]
1233            if r is None:
1234                ctx = repo[b'.']
1235            m = matchmod.match(
1236                repo.root, repo.getcwd(), [x], ctx=mctx, default=b'path'
1237            )
1238            fctxs.extend(ctx[f].introfilectx() for f in ctx.manifest().walk(m))
1239        s = dagop.filerevancestors(fctxs, followfirst)
1240    else:
1241        if revs is None:
1242            revs = baseset([repo[b'.'].rev()])
1243        s = dagop.revancestors(repo, revs, followfirst)
1244
1245    return subset & s
1246
1247
1248@predicate(b'follow([file[, startrev]])', safe=True)
1249def follow(repo, subset, x):
1250    """
1251    An alias for ``::.`` (ancestors of the working directory's first parent).
1252    If file pattern is specified, the histories of files matching given
1253    pattern in the revision given by startrev are followed, including copies.
1254    """
1255    return _follow(repo, subset, x, b'follow')
1256
1257
1258@predicate(b'_followfirst', safe=True)
1259def _followfirst(repo, subset, x):
1260    # ``followfirst([file[, startrev]])``
1261    # Like ``follow([file[, startrev]])`` but follows only the first parent
1262    # of every revisions or files revisions.
1263    return _follow(repo, subset, x, b'_followfirst', followfirst=True)
1264
1265
1266@predicate(
1267    b'followlines(file, fromline:toline[, startrev=., descend=False])',
1268    safe=True,
1269)
1270def followlines(repo, subset, x):
1271    """Changesets modifying `file` in line range ('fromline', 'toline').
1272
1273    Line range corresponds to 'file' content at 'startrev' and should hence be
1274    consistent with file size. If startrev is not specified, working directory's
1275    parent is used.
1276
1277    By default, ancestors of 'startrev' are returned. If 'descend' is True,
1278    descendants of 'startrev' are returned though renames are (currently) not
1279    followed in this direction.
1280    """
1281    args = getargsdict(x, b'followlines', b'file *lines startrev descend')
1282    if len(args[b'lines']) != 1:
1283        raise error.ParseError(_(b"followlines requires a line range"))
1284
1285    rev = b'.'
1286    if b'startrev' in args:
1287        revs = getset(repo, fullreposet(repo), args[b'startrev'])
1288        if len(revs) != 1:
1289            raise error.ParseError(
1290                # i18n: "followlines" is a keyword
1291                _(b"followlines expects exactly one revision")
1292            )
1293        rev = revs.last()
1294
1295    pat = getstring(args[b'file'], _(b"followlines requires a pattern"))
1296    # i18n: "followlines" is a keyword
1297    msg = _(b"followlines expects exactly one file")
1298    fname = scmutil.parsefollowlinespattern(repo, rev, pat, msg)
1299    fromline, toline = util.processlinerange(
1300        *getintrange(
1301            args[b'lines'][0],
1302            # i18n: "followlines" is a keyword
1303            _(b"followlines expects a line number or a range"),
1304            _(b"line range bounds must be integers"),
1305        )
1306    )
1307
1308    fctx = repo[rev].filectx(fname)
1309    descend = False
1310    if b'descend' in args:
1311        descend = getboolean(
1312            args[b'descend'],
1313            # i18n: "descend" is a keyword
1314            _(b"descend argument must be a boolean"),
1315        )
1316    if descend:
1317        rs = generatorset(
1318            (
1319                c.rev()
1320                for c, _linerange in dagop.blockdescendants(
1321                    fctx, fromline, toline
1322                )
1323            ),
1324            iterasc=True,
1325        )
1326    else:
1327        rs = generatorset(
1328            (
1329                c.rev()
1330                for c, _linerange in dagop.blockancestors(
1331                    fctx, fromline, toline
1332                )
1333            ),
1334            iterasc=False,
1335        )
1336    return subset & rs
1337
1338
1339@predicate(b'nodefromfile(path)')
1340def nodefromfile(repo, subset, x):
1341    """
1342    An alias for ``::.`` (ancestors of the working directory's first parent).
1343    If file pattern is specified, the histories of files matching given
1344    pattern in the revision given by startrev are followed, including copies.
1345    """
1346    path = getstring(x, _(b"nodefromfile require a file path"))
1347    listed_rev = set()
1348    try:
1349        with pycompat.open(path, 'rb') as f:
1350            for line in f:
1351                n = line.strip()
1352                rn = _node(repo, n)
1353                if rn is not None:
1354                    listed_rev.add(rn)
1355    except IOError as exc:
1356        m = _(b'cannot open nodes file "%s": %s')
1357        m %= (path, encoding.strtolocal(exc.strerror))
1358        raise error.Abort(m)
1359    return subset & baseset(listed_rev)
1360
1361
1362@predicate(b'all()', safe=True)
1363def getall(repo, subset, x):
1364    """All changesets, the same as ``0:tip``."""
1365    # i18n: "all" is a keyword
1366    getargs(x, 0, 0, _(b"all takes no arguments"))
1367    return subset & spanset(repo)  # drop "null" if any
1368
1369
1370@predicate(b'grep(regex)', weight=10)
1371def grep(repo, subset, x):
1372    """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
1373    to ensure special escape characters are handled correctly. Unlike
1374    ``keyword(string)``, the match is case-sensitive.
1375    """
1376    try:
1377        # i18n: "grep" is a keyword
1378        gr = re.compile(getstring(x, _(b"grep requires a string")))
1379    except re.error as e:
1380        raise error.ParseError(
1381            _(b'invalid match pattern: %s') % stringutil.forcebytestr(e)
1382        )
1383
1384    def matches(x):
1385        c = repo[x]
1386        for e in c.files() + [c.user(), c.description()]:
1387            if gr.search(e):
1388                return True
1389        return False
1390
1391    return subset.filter(matches, condrepr=(b'<grep %r>', gr.pattern))
1392
1393
1394@predicate(b'_matchfiles', safe=True)
1395def _matchfiles(repo, subset, x):
1396    # _matchfiles takes a revset list of prefixed arguments:
1397    #
1398    #   [p:foo, i:bar, x:baz]
1399    #
1400    # builds a match object from them and filters subset. Allowed
1401    # prefixes are 'p:' for regular patterns, 'i:' for include
1402    # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
1403    # a revision identifier, or the empty string to reference the
1404    # working directory, from which the match object is
1405    # initialized. Use 'd:' to set the default matching mode, default
1406    # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
1407
1408    l = getargs(x, 1, -1, b"_matchfiles requires at least one argument")
1409    pats, inc, exc = [], [], []
1410    rev, default = None, None
1411    for arg in l:
1412        s = getstring(arg, b"_matchfiles requires string arguments")
1413        prefix, value = s[:2], s[2:]
1414        if prefix == b'p:':
1415            pats.append(value)
1416        elif prefix == b'i:':
1417            inc.append(value)
1418        elif prefix == b'x:':
1419            exc.append(value)
1420        elif prefix == b'r:':
1421            if rev is not None:
1422                raise error.ParseError(
1423                    b'_matchfiles expected at most one revision'
1424                )
1425            if value == b'':  # empty means working directory
1426                rev = wdirrev
1427            else:
1428                rev = value
1429        elif prefix == b'd:':
1430            if default is not None:
1431                raise error.ParseError(
1432                    b'_matchfiles expected at most one default mode'
1433                )
1434            default = value
1435        else:
1436            raise error.ParseError(b'invalid _matchfiles prefix: %s' % prefix)
1437    if not default:
1438        default = b'glob'
1439    hasset = any(matchmod.patkind(p) == b'set' for p in pats + inc + exc)
1440
1441    mcache = [None]
1442
1443    # This directly read the changelog data as creating changectx for all
1444    # revisions is quite expensive.
1445    getfiles = repo.changelog.readfiles
1446
1447    def matches(x):
1448        if x == wdirrev:
1449            files = repo[x].files()
1450        else:
1451            files = getfiles(x)
1452
1453        if not mcache[0] or (hasset and rev is None):
1454            r = x if rev is None else rev
1455            mcache[0] = matchmod.match(
1456                repo.root,
1457                repo.getcwd(),
1458                pats,
1459                include=inc,
1460                exclude=exc,
1461                ctx=repo[r],
1462                default=default,
1463            )
1464        m = mcache[0]
1465
1466        for f in files:
1467            if m(f):
1468                return True
1469        return False
1470
1471    return subset.filter(
1472        matches,
1473        condrepr=(
1474            b'<matchfiles patterns=%r, include=%r '
1475            b'exclude=%r, default=%r, rev=%r>',
1476            pats,
1477            inc,
1478            exc,
1479            default,
1480            rev,
1481        ),
1482    )
1483
1484
1485@predicate(b'file(pattern)', safe=True, weight=10)
1486def hasfile(repo, subset, x):
1487    """Changesets affecting files matched by pattern.
1488
1489    For a faster but less accurate result, consider using ``filelog()``
1490    instead.
1491
1492    This predicate uses ``glob:`` as the default kind of pattern.
1493    """
1494    # i18n: "file" is a keyword
1495    pat = getstring(x, _(b"file requires a pattern"))
1496    return _matchfiles(repo, subset, (b'string', b'p:' + pat))
1497
1498
1499@predicate(b'head()', safe=True)
1500def head(repo, subset, x):
1501    """Changeset is a named branch head."""
1502    # i18n: "head" is a keyword
1503    getargs(x, 0, 0, _(b"head takes no arguments"))
1504    hs = set()
1505    cl = repo.changelog
1506    for ls in repo.branchmap().iterheads():
1507        hs.update(cl.rev(h) for h in ls)
1508    return subset & baseset(hs)
1509
1510
1511@predicate(b'heads(set)', safe=True, takeorder=True)
1512def heads(repo, subset, x, order):
1513    """Members of set with no children in set."""
1514    # argument set should never define order
1515    if order == defineorder:
1516        order = followorder
1517    inputset = getset(repo, fullreposet(repo), x, order=order)
1518    wdirparents = None
1519    if wdirrev in inputset:
1520        # a bit slower, but not common so good enough for now
1521        wdirparents = [p.rev() for p in repo[None].parents()]
1522        inputset = set(inputset)
1523        inputset.discard(wdirrev)
1524    heads = repo.changelog.headrevs(inputset)
1525    if wdirparents is not None:
1526        heads.difference_update(wdirparents)
1527        heads.add(wdirrev)
1528    heads = baseset(heads)
1529    return subset & heads
1530
1531
1532@predicate(b'hidden()', safe=True)
1533def hidden(repo, subset, x):
1534    """Hidden changesets."""
1535    # i18n: "hidden" is a keyword
1536    getargs(x, 0, 0, _(b"hidden takes no arguments"))
1537    hiddenrevs = repoview.filterrevs(repo, b'visible')
1538    return subset & hiddenrevs
1539
1540
1541@predicate(b'keyword(string)', safe=True, weight=10)
1542def keyword(repo, subset, x):
1543    """Search commit message, user name, and names of changed files for
1544    string. The match is case-insensitive.
1545
1546    For a regular expression or case sensitive search of these fields, use
1547    ``grep(regex)``.
1548    """
1549    # i18n: "keyword" is a keyword
1550    kw = encoding.lower(getstring(x, _(b"keyword requires a string")))
1551
1552    def matches(r):
1553        c = repo[r]
1554        return any(
1555            kw in encoding.lower(t)
1556            for t in c.files() + [c.user(), c.description()]
1557        )
1558
1559    return subset.filter(matches, condrepr=(b'<keyword %r>', kw))
1560
1561
1562@predicate(b'limit(set[, n[, offset]])', safe=True, takeorder=True, weight=0)
1563def limit(repo, subset, x, order):
1564    """First n members of set, defaulting to 1, starting from offset."""
1565    args = getargsdict(x, b'limit', b'set n offset')
1566    if b'set' not in args:
1567        # i18n: "limit" is a keyword
1568        raise error.ParseError(_(b"limit requires one to three arguments"))
1569    # i18n: "limit" is a keyword
1570    lim = getinteger(args.get(b'n'), _(b"limit expects a number"), default=1)
1571    if lim < 0:
1572        raise error.ParseError(_(b"negative number to select"))
1573    # i18n: "limit" is a keyword
1574    ofs = getinteger(
1575        args.get(b'offset'), _(b"limit expects a number"), default=0
1576    )
1577    if ofs < 0:
1578        raise error.ParseError(_(b"negative offset"))
1579    os = getset(repo, fullreposet(repo), args[b'set'])
1580    ls = os.slice(ofs, ofs + lim)
1581    if order == followorder and lim > 1:
1582        return subset & ls
1583    return ls & subset
1584
1585
1586@predicate(b'last(set, [n])', safe=True, takeorder=True)
1587def last(repo, subset, x, order):
1588    """Last n members of set, defaulting to 1."""
1589    # i18n: "last" is a keyword
1590    l = getargs(x, 1, 2, _(b"last requires one or two arguments"))
1591    lim = 1
1592    if len(l) == 2:
1593        # i18n: "last" is a keyword
1594        lim = getinteger(l[1], _(b"last expects a number"))
1595    if lim < 0:
1596        raise error.ParseError(_(b"negative number to select"))
1597    os = getset(repo, fullreposet(repo), l[0])
1598    os.reverse()
1599    ls = os.slice(0, lim)
1600    if order == followorder and lim > 1:
1601        return subset & ls
1602    ls.reverse()
1603    return ls & subset
1604
1605
1606@predicate(b'max(set)', safe=True)
1607def maxrev(repo, subset, x):
1608    """Changeset with highest revision number in set."""
1609    os = getset(repo, fullreposet(repo), x)
1610    try:
1611        m = os.max()
1612        if m in subset:
1613            return baseset([m], datarepr=(b'<max %r, %r>', subset, os))
1614    except ValueError:
1615        # os.max() throws a ValueError when the collection is empty.
1616        # Same as python's max().
1617        pass
1618    return baseset(datarepr=(b'<max %r, %r>', subset, os))
1619
1620
1621@predicate(b'merge()', safe=True)
1622def merge(repo, subset, x):
1623    """Changeset is a merge changeset."""
1624    # i18n: "merge" is a keyword
1625    getargs(x, 0, 0, _(b"merge takes no arguments"))
1626    cl = repo.changelog
1627
1628    def ismerge(r):
1629        try:
1630            return cl.parentrevs(r)[1] != nullrev
1631        except error.WdirUnsupported:
1632            return bool(repo[r].p2())
1633
1634    return subset.filter(ismerge, condrepr=b'<merge>')
1635
1636
1637@predicate(b'branchpoint()', safe=True)
1638def branchpoint(repo, subset, x):
1639    """Changesets with more than one child."""
1640    # i18n: "branchpoint" is a keyword
1641    getargs(x, 0, 0, _(b"branchpoint takes no arguments"))
1642    cl = repo.changelog
1643    if not subset:
1644        return baseset()
1645    # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset
1646    # (and if it is not, it should.)
1647    baserev = min(subset)
1648    parentscount = [0] * (len(repo) - baserev)
1649    for r in cl.revs(start=baserev + 1):
1650        for p in cl.parentrevs(r):
1651            if p >= baserev:
1652                parentscount[p - baserev] += 1
1653    return subset.filter(
1654        lambda r: parentscount[r - baserev] > 1, condrepr=b'<branchpoint>'
1655    )
1656
1657
1658@predicate(b'min(set)', safe=True)
1659def minrev(repo, subset, x):
1660    """Changeset with lowest revision number in set."""
1661    os = getset(repo, fullreposet(repo), x)
1662    try:
1663        m = os.min()
1664        if m in subset:
1665            return baseset([m], datarepr=(b'<min %r, %r>', subset, os))
1666    except ValueError:
1667        # os.min() throws a ValueError when the collection is empty.
1668        # Same as python's min().
1669        pass
1670    return baseset(datarepr=(b'<min %r, %r>', subset, os))
1671
1672
1673@predicate(b'modifies(pattern)', safe=True, weight=30)
1674def modifies(repo, subset, x):
1675    """Changesets modifying files matched by pattern.
1676
1677    The pattern without explicit kind like ``glob:`` is expected to be
1678    relative to the current directory and match against a file or a
1679    directory.
1680    """
1681    # i18n: "modifies" is a keyword
1682    pat = getstring(x, _(b"modifies requires a pattern"))
1683    return checkstatus(repo, subset, pat, 'modified')
1684
1685
1686@predicate(b'named(namespace)')
1687def named(repo, subset, x):
1688    """The changesets in a given namespace.
1689
1690    Pattern matching is supported for `namespace`. See
1691    :hg:`help revisions.patterns`.
1692    """
1693    # i18n: "named" is a keyword
1694    args = getargs(x, 1, 1, _(b'named requires a namespace argument'))
1695
1696    ns = getstring(
1697        args[0],
1698        # i18n: "named" is a keyword
1699        _(b'the argument to named must be a string'),
1700    )
1701    kind, pattern, matcher = stringutil.stringmatcher(ns)
1702    namespaces = set()
1703    if kind == b'literal':
1704        if pattern not in repo.names:
1705            raise error.RepoLookupError(
1706                _(b"namespace '%s' does not exist") % ns
1707            )
1708        namespaces.add(repo.names[pattern])
1709    else:
1710        for name, ns in pycompat.iteritems(repo.names):
1711            if matcher(name):
1712                namespaces.add(ns)
1713
1714    names = set()
1715    for ns in namespaces:
1716        for name in ns.listnames(repo):
1717            if name not in ns.deprecated:
1718                names.update(repo[n].rev() for n in ns.nodes(repo, name))
1719
1720    names -= {nullrev}
1721    return subset & names
1722
1723
1724def _node(repo, n):
1725    """process a node input"""
1726    rn = None
1727    if len(n) == 2 * repo.nodeconstants.nodelen:
1728        try:
1729            rn = repo.changelog.rev(bin(n))
1730        except error.WdirUnsupported:
1731            rn = wdirrev
1732        except (LookupError, TypeError):
1733            rn = None
1734    else:
1735        try:
1736            pm = scmutil.resolvehexnodeidprefix(repo, n)
1737            if pm is not None:
1738                rn = repo.changelog.rev(pm)
1739        except LookupError:
1740            pass
1741        except error.WdirUnsupported:
1742            rn = wdirrev
1743    return rn
1744
1745
1746@predicate(b'id(string)', safe=True)
1747def node_(repo, subset, x):
1748    """Revision non-ambiguously specified by the given hex string prefix."""
1749    # i18n: "id" is a keyword
1750    l = getargs(x, 1, 1, _(b"id requires one argument"))
1751    # i18n: "id" is a keyword
1752    n = getstring(l[0], _(b"id requires a string"))
1753    rn = _node(repo, n)
1754
1755    if rn is None:
1756        return baseset()
1757    result = baseset([rn])
1758    return result & subset
1759
1760
1761@predicate(b'none()', safe=True)
1762def none(repo, subset, x):
1763    """No changesets."""
1764    # i18n: "none" is a keyword
1765    getargs(x, 0, 0, _(b"none takes no arguments"))
1766    return baseset()
1767
1768
1769@predicate(b'obsolete()', safe=True)
1770def obsolete(repo, subset, x):
1771    """Mutable changeset with a newer version. (EXPERIMENTAL)"""
1772    # i18n: "obsolete" is a keyword
1773    getargs(x, 0, 0, _(b"obsolete takes no arguments"))
1774    obsoletes = obsmod.getrevs(repo, b'obsolete')
1775    return subset & obsoletes
1776
1777
1778@predicate(b'only(set, [set])', safe=True)
1779def only(repo, subset, x):
1780    """Changesets that are ancestors of the first set that are not ancestors
1781    of any other head in the repo. If a second set is specified, the result
1782    is ancestors of the first set that are not ancestors of the second set
1783    (i.e. ::<set1> - ::<set2>).
1784    """
1785    cl = repo.changelog
1786    # i18n: "only" is a keyword
1787    args = getargs(x, 1, 2, _(b'only takes one or two arguments'))
1788    include = getset(repo, fullreposet(repo), args[0])
1789    if len(args) == 1:
1790        if not include:
1791            return baseset()
1792
1793        descendants = set(dagop.revdescendants(repo, include, False))
1794        exclude = [
1795            rev
1796            for rev in cl.headrevs()
1797            if not rev in descendants and not rev in include
1798        ]
1799    else:
1800        exclude = getset(repo, fullreposet(repo), args[1])
1801
1802    results = set(cl.findmissingrevs(common=exclude, heads=include))
1803    # XXX we should turn this into a baseset instead of a set, smartset may do
1804    # some optimizations from the fact this is a baseset.
1805    return subset & results
1806
1807
1808@predicate(b'origin([set])', safe=True)
1809def origin(repo, subset, x):
1810    """
1811    Changesets that were specified as a source for the grafts, transplants or
1812    rebases that created the given revisions.  Omitting the optional set is the
1813    same as passing all().  If a changeset created by these operations is itself
1814    specified as a source for one of these operations, only the source changeset
1815    for the first operation is selected.
1816    """
1817    if x is not None:
1818        dests = getset(repo, fullreposet(repo), x)
1819    else:
1820        dests = fullreposet(repo)
1821
1822    def _firstsrc(rev):
1823        src = _getrevsource(repo, rev)
1824        if src is None:
1825            return None
1826
1827        while True:
1828            prev = _getrevsource(repo, src)
1829
1830            if prev is None:
1831                return src
1832            src = prev
1833
1834    o = {_firstsrc(r) for r in dests}
1835    o -= {None}
1836    # XXX we should turn this into a baseset instead of a set, smartset may do
1837    # some optimizations from the fact this is a baseset.
1838    return subset & o
1839
1840
1841@predicate(b'outgoing([path])', safe=False, weight=10)
1842def outgoing(repo, subset, x):
1843    """Changesets not found in the specified destination repository, or the
1844    default push location.
1845
1846    If the location resolve to multiple repositories, the union of all
1847    outgoing changeset will be used.
1848    """
1849    # Avoid cycles.
1850    from . import (
1851        discovery,
1852        hg,
1853    )
1854
1855    # i18n: "outgoing" is a keyword
1856    l = getargs(x, 0, 1, _(b"outgoing takes one or no arguments"))
1857    # i18n: "outgoing" is a keyword
1858    dest = (
1859        l and getstring(l[0], _(b"outgoing requires a repository path")) or b''
1860    )
1861    if dest:
1862        dests = [dest]
1863    else:
1864        dests = []
1865    missing = set()
1866    for path in urlutil.get_push_paths(repo, repo.ui, dests):
1867        dest = path.pushloc or path.loc
1868        branches = path.branch, []
1869
1870        revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1871        if revs:
1872            revs = [repo.lookup(rev) for rev in revs]
1873        other = hg.peer(repo, {}, dest)
1874        try:
1875            with repo.ui.silent():
1876                outgoing = discovery.findcommonoutgoing(
1877                    repo, other, onlyheads=revs
1878                )
1879        finally:
1880            other.close()
1881        missing.update(outgoing.missing)
1882    cl = repo.changelog
1883    o = {cl.rev(r) for r in missing}
1884    return subset & o
1885
1886
1887@predicate(b'p1([set])', safe=True)
1888def p1(repo, subset, x):
1889    """First parent of changesets in set, or the working directory."""
1890    if x is None:
1891        p = repo[x].p1().rev()
1892        if p >= 0:
1893            return subset & baseset([p])
1894        return baseset()
1895
1896    ps = set()
1897    cl = repo.changelog
1898    for r in getset(repo, fullreposet(repo), x):
1899        try:
1900            ps.add(cl.parentrevs(r)[0])
1901        except error.WdirUnsupported:
1902            ps.add(repo[r].p1().rev())
1903    ps -= {nullrev}
1904    # XXX we should turn this into a baseset instead of a set, smartset may do
1905    # some optimizations from the fact this is a baseset.
1906    return subset & ps
1907
1908
1909@predicate(b'p2([set])', safe=True)
1910def p2(repo, subset, x):
1911    """Second parent of changesets in set, or the working directory."""
1912    if x is None:
1913        ps = repo[x].parents()
1914        try:
1915            p = ps[1].rev()
1916            if p >= 0:
1917                return subset & baseset([p])
1918            return baseset()
1919        except IndexError:
1920            return baseset()
1921
1922    ps = set()
1923    cl = repo.changelog
1924    for r in getset(repo, fullreposet(repo), x):
1925        try:
1926            ps.add(cl.parentrevs(r)[1])
1927        except error.WdirUnsupported:
1928            parents = repo[r].parents()
1929            if len(parents) == 2:
1930                ps.add(parents[1])
1931    ps -= {nullrev}
1932    # XXX we should turn this into a baseset instead of a set, smartset may do
1933    # some optimizations from the fact this is a baseset.
1934    return subset & ps
1935
1936
1937def parentpost(repo, subset, x, order):
1938    return p1(repo, subset, x)
1939
1940
1941@predicate(b'parents([set])', safe=True)
1942def parents(repo, subset, x):
1943    """
1944    The set of all parents for all changesets in set, or the working directory.
1945    """
1946    if x is None:
1947        ps = {p.rev() for p in repo[x].parents()}
1948    else:
1949        ps = set()
1950        cl = repo.changelog
1951        up = ps.update
1952        parentrevs = cl.parentrevs
1953        for r in getset(repo, fullreposet(repo), x):
1954            try:
1955                up(parentrevs(r))
1956            except error.WdirUnsupported:
1957                up(p.rev() for p in repo[r].parents())
1958    ps -= {nullrev}
1959    return subset & ps
1960
1961
1962def _phase(repo, subset, *targets):
1963    """helper to select all rev in <targets> phases"""
1964    return repo._phasecache.getrevset(repo, targets, subset)
1965
1966
1967@predicate(b'_phase(idx)', safe=True)
1968def phase(repo, subset, x):
1969    l = getargs(x, 1, 1, b"_phase requires one argument")
1970    target = getinteger(l[0], b"_phase expects a number")
1971    return _phase(repo, subset, target)
1972
1973
1974@predicate(b'draft()', safe=True)
1975def draft(repo, subset, x):
1976    """Changeset in draft phase."""
1977    # i18n: "draft" is a keyword
1978    getargs(x, 0, 0, _(b"draft takes no arguments"))
1979    target = phases.draft
1980    return _phase(repo, subset, target)
1981
1982
1983@predicate(b'secret()', safe=True)
1984def secret(repo, subset, x):
1985    """Changeset in secret phase."""
1986    # i18n: "secret" is a keyword
1987    getargs(x, 0, 0, _(b"secret takes no arguments"))
1988    target = phases.secret
1989    return _phase(repo, subset, target)
1990
1991
1992@predicate(b'stack([revs])', safe=True)
1993def stack(repo, subset, x):
1994    """Experimental revset for the stack of changesets or working directory
1995    parent. (EXPERIMENTAL)
1996    """
1997    if x is None:
1998        stacks = stackmod.getstack(repo)
1999    else:
2000        stacks = smartset.baseset([])
2001        for revision in getset(repo, fullreposet(repo), x):
2002            currentstack = stackmod.getstack(repo, revision)
2003            stacks = stacks + currentstack
2004
2005    return subset & stacks
2006
2007
2008def parentspec(repo, subset, x, n, order):
2009    """``set^0``
2010    The set.
2011    ``set^1`` (or ``set^``), ``set^2``
2012    First or second parent, respectively, of all changesets in set.
2013    """
2014    try:
2015        n = int(n[1])
2016        if n not in (0, 1, 2):
2017            raise ValueError
2018    except (TypeError, ValueError):
2019        raise error.ParseError(_(b"^ expects a number 0, 1, or 2"))
2020    ps = set()
2021    cl = repo.changelog
2022    for r in getset(repo, fullreposet(repo), x):
2023        if n == 0:
2024            ps.add(r)
2025        elif n == 1:
2026            try:
2027                ps.add(cl.parentrevs(r)[0])
2028            except error.WdirUnsupported:
2029                ps.add(repo[r].p1().rev())
2030        else:
2031            try:
2032                parents = cl.parentrevs(r)
2033                if parents[1] != nullrev:
2034                    ps.add(parents[1])
2035            except error.WdirUnsupported:
2036                parents = repo[r].parents()
2037                if len(parents) == 2:
2038                    ps.add(parents[1].rev())
2039    return subset & ps
2040
2041
2042@predicate(b'present(set)', safe=True, takeorder=True)
2043def present(repo, subset, x, order):
2044    """An empty set, if any revision in set isn't found; otherwise,
2045    all revisions in set.
2046
2047    If any of specified revisions is not present in the local repository,
2048    the query is normally aborted. But this predicate allows the query
2049    to continue even in such cases.
2050    """
2051    try:
2052        return getset(repo, subset, x, order)
2053    except error.RepoLookupError:
2054        return baseset()
2055
2056
2057# for internal use
2058@predicate(b'_notpublic', safe=True)
2059def _notpublic(repo, subset, x):
2060    getargs(x, 0, 0, b"_notpublic takes no arguments")
2061    return _phase(repo, subset, phases.draft, phases.secret)
2062
2063
2064# for internal use
2065@predicate(b'_phaseandancestors(phasename, set)', safe=True)
2066def _phaseandancestors(repo, subset, x):
2067    # equivalent to (phasename() & ancestors(set)) but more efficient
2068    # phasename could be one of 'draft', 'secret', or '_notpublic'
2069    args = getargs(x, 2, 2, b"_phaseandancestors requires two arguments")
2070    phasename = getsymbol(args[0])
2071    s = getset(repo, fullreposet(repo), args[1])
2072
2073    draft = phases.draft
2074    secret = phases.secret
2075    phasenamemap = {
2076        b'_notpublic': draft,
2077        b'draft': draft,  # follow secret's ancestors
2078        b'secret': secret,
2079    }
2080    if phasename not in phasenamemap:
2081        raise error.ParseError(b'%r is not a valid phasename' % phasename)
2082
2083    minimalphase = phasenamemap[phasename]
2084    getphase = repo._phasecache.phase
2085
2086    def cutfunc(rev):
2087        return getphase(repo, rev) < minimalphase
2088
2089    revs = dagop.revancestors(repo, s, cutfunc=cutfunc)
2090
2091    if phasename == b'draft':  # need to remove secret changesets
2092        revs = revs.filter(lambda r: getphase(repo, r) == draft)
2093    return subset & revs
2094
2095
2096@predicate(b'public()', safe=True)
2097def public(repo, subset, x):
2098    """Changeset in public phase."""
2099    # i18n: "public" is a keyword
2100    getargs(x, 0, 0, _(b"public takes no arguments"))
2101    return _phase(repo, subset, phases.public)
2102
2103
2104@predicate(b'remote([id [,path]])', safe=False)
2105def remote(repo, subset, x):
2106    """Local revision that corresponds to the given identifier in a
2107    remote repository, if present. Here, the '.' identifier is a
2108    synonym for the current local branch.
2109    """
2110
2111    from . import hg  # avoid start-up nasties
2112
2113    # i18n: "remote" is a keyword
2114    l = getargs(x, 0, 2, _(b"remote takes zero, one, or two arguments"))
2115
2116    q = b'.'
2117    if len(l) > 0:
2118        # i18n: "remote" is a keyword
2119        q = getstring(l[0], _(b"remote requires a string id"))
2120    if q == b'.':
2121        q = repo[b'.'].branch()
2122
2123    dest = b''
2124    if len(l) > 1:
2125        # i18n: "remote" is a keyword
2126        dest = getstring(l[1], _(b"remote requires a repository path"))
2127    if not dest:
2128        dest = b'default'
2129    dest, branches = urlutil.get_unique_pull_path(
2130        b'remote', repo, repo.ui, dest
2131    )
2132
2133    other = hg.peer(repo, {}, dest)
2134    n = other.lookup(q)
2135    if n in repo:
2136        r = repo[n].rev()
2137        if r in subset:
2138            return baseset([r])
2139    return baseset()
2140
2141
2142@predicate(b'removes(pattern)', safe=True, weight=30)
2143def removes(repo, subset, x):
2144    """Changesets which remove files matching pattern.
2145
2146    The pattern without explicit kind like ``glob:`` is expected to be
2147    relative to the current directory and match against a file or a
2148    directory.
2149    """
2150    # i18n: "removes" is a keyword
2151    pat = getstring(x, _(b"removes requires a pattern"))
2152    return checkstatus(repo, subset, pat, 'removed')
2153
2154
2155@predicate(b'rev(number)', safe=True)
2156def rev(repo, subset, x):
2157    """Revision with the given numeric identifier."""
2158    try:
2159        return _rev(repo, subset, x)
2160    except error.RepoLookupError:
2161        return baseset()
2162
2163
2164@predicate(b'_rev(number)', safe=True)
2165def _rev(repo, subset, x):
2166    # internal version of "rev(x)" that raise error if "x" is invalid
2167    # i18n: "rev" is a keyword
2168    l = getargs(x, 1, 1, _(b"rev requires one argument"))
2169    try:
2170        # i18n: "rev" is a keyword
2171        l = int(getstring(l[0], _(b"rev requires a number")))
2172    except (TypeError, ValueError):
2173        # i18n: "rev" is a keyword
2174        raise error.ParseError(_(b"rev expects a number"))
2175    if l not in _virtualrevs:
2176        try:
2177            repo.changelog.node(l)  # check that the rev exists
2178        except IndexError:
2179            raise error.RepoLookupError(_(b"unknown revision '%d'") % l)
2180    return subset & baseset([l])
2181
2182
2183@predicate(b'revset(set)', safe=True, takeorder=True)
2184def revsetpredicate(repo, subset, x, order):
2185    """Strictly interpret the content as a revset.
2186
2187    The content of this special predicate will be strictly interpreted as a
2188    revset. For example, ``revset(id(0))`` will be interpreted as "id(0)"
2189    without possible ambiguity with a "id(0)" bookmark or tag.
2190    """
2191    return getset(repo, subset, x, order)
2192
2193
2194@predicate(b'matching(revision [, field])', safe=True)
2195def matching(repo, subset, x):
2196    """Changesets in which a given set of fields match the set of fields in the
2197    selected revision or set.
2198
2199    To match more than one field pass the list of fields to match separated
2200    by spaces (e.g. ``author description``).
2201
2202    Valid fields are most regular revision fields and some special fields.
2203
2204    Regular revision fields are ``description``, ``author``, ``branch``,
2205    ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
2206    and ``diff``.
2207    Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
2208    contents of the revision. Two revisions matching their ``diff`` will
2209    also match their ``files``.
2210
2211    Special fields are ``summary`` and ``metadata``:
2212    ``summary`` matches the first line of the description.
2213    ``metadata`` is equivalent to matching ``description user date``
2214    (i.e. it matches the main metadata fields).
2215
2216    ``metadata`` is the default field which is used when no fields are
2217    specified. You can match more than one field at a time.
2218    """
2219    # i18n: "matching" is a keyword
2220    l = getargs(x, 1, 2, _(b"matching takes 1 or 2 arguments"))
2221
2222    revs = getset(repo, fullreposet(repo), l[0])
2223
2224    fieldlist = [b'metadata']
2225    if len(l) > 1:
2226        fieldlist = getstring(
2227            l[1],
2228            # i18n: "matching" is a keyword
2229            _(b"matching requires a string as its second argument"),
2230        ).split()
2231
2232    # Make sure that there are no repeated fields,
2233    # expand the 'special' 'metadata' field type
2234    # and check the 'files' whenever we check the 'diff'
2235    fields = []
2236    for field in fieldlist:
2237        if field == b'metadata':
2238            fields += [b'user', b'description', b'date']
2239        elif field == b'diff':
2240            # a revision matching the diff must also match the files
2241            # since matching the diff is very costly, make sure to
2242            # also match the files first
2243            fields += [b'files', b'diff']
2244        else:
2245            if field == b'author':
2246                field = b'user'
2247            fields.append(field)
2248    fields = set(fields)
2249    if b'summary' in fields and b'description' in fields:
2250        # If a revision matches its description it also matches its summary
2251        fields.discard(b'summary')
2252
2253    # We may want to match more than one field
2254    # Not all fields take the same amount of time to be matched
2255    # Sort the selected fields in order of increasing matching cost
2256    fieldorder = [
2257        b'phase',
2258        b'parents',
2259        b'user',
2260        b'date',
2261        b'branch',
2262        b'summary',
2263        b'files',
2264        b'description',
2265        b'substate',
2266        b'diff',
2267    ]
2268
2269    def fieldkeyfunc(f):
2270        try:
2271            return fieldorder.index(f)
2272        except ValueError:
2273            # assume an unknown field is very costly
2274            return len(fieldorder)
2275
2276    fields = list(fields)
2277    fields.sort(key=fieldkeyfunc)
2278
2279    # Each field will be matched with its own "getfield" function
2280    # which will be added to the getfieldfuncs array of functions
2281    getfieldfuncs = []
2282    _funcs = {
2283        b'user': lambda r: repo[r].user(),
2284        b'branch': lambda r: repo[r].branch(),
2285        b'date': lambda r: repo[r].date(),
2286        b'description': lambda r: repo[r].description(),
2287        b'files': lambda r: repo[r].files(),
2288        b'parents': lambda r: repo[r].parents(),
2289        b'phase': lambda r: repo[r].phase(),
2290        b'substate': lambda r: repo[r].substate,
2291        b'summary': lambda r: repo[r].description().splitlines()[0],
2292        b'diff': lambda r: list(
2293            repo[r].diff(opts=diffutil.diffallopts(repo.ui, {b'git': True}))
2294        ),
2295    }
2296    for info in fields:
2297        getfield = _funcs.get(info, None)
2298        if getfield is None:
2299            raise error.ParseError(
2300                # i18n: "matching" is a keyword
2301                _(b"unexpected field name passed to matching: %s")
2302                % info
2303            )
2304        getfieldfuncs.append(getfield)
2305    # convert the getfield array of functions into a "getinfo" function
2306    # which returns an array of field values (or a single value if there
2307    # is only one field to match)
2308    getinfo = lambda r: [f(r) for f in getfieldfuncs]
2309
2310    def matches(x):
2311        for rev in revs:
2312            target = getinfo(rev)
2313            match = True
2314            for n, f in enumerate(getfieldfuncs):
2315                if target[n] != f(x):
2316                    match = False
2317            if match:
2318                return True
2319        return False
2320
2321    return subset.filter(matches, condrepr=(b'<matching%r %r>', fields, revs))
2322
2323
2324@predicate(b'reverse(set)', safe=True, takeorder=True, weight=0)
2325def reverse(repo, subset, x, order):
2326    """Reverse order of set."""
2327    l = getset(repo, subset, x, order)
2328    if order == defineorder:
2329        l.reverse()
2330    return l
2331
2332
2333@predicate(b'roots(set)', safe=True)
2334def roots(repo, subset, x):
2335    """Changesets in set with no parent changeset in set."""
2336    s = getset(repo, fullreposet(repo), x)
2337    parents = repo.changelog.parentrevs
2338
2339    def filter(r):
2340        for p in parents(r):
2341            if 0 <= p and p in s:
2342                return False
2343        return True
2344
2345    return subset & s.filter(filter, condrepr=b'<roots>')
2346
2347
2348_sortkeyfuncs = {
2349    b'rev': scmutil.intrev,
2350    b'branch': lambda c: c.branch(),
2351    b'desc': lambda c: c.description(),
2352    b'user': lambda c: c.user(),
2353    b'author': lambda c: c.user(),
2354    b'date': lambda c: c.date()[0],
2355    b'node': scmutil.binnode,
2356}
2357
2358
2359def _getsortargs(x):
2360    """Parse sort options into (set, [(key, reverse)], opts)"""
2361    args = getargsdict(x, b'sort', b'set keys topo.firstbranch')
2362    if b'set' not in args:
2363        # i18n: "sort" is a keyword
2364        raise error.ParseError(_(b'sort requires one or two arguments'))
2365    keys = b"rev"
2366    if b'keys' in args:
2367        # i18n: "sort" is a keyword
2368        keys = getstring(args[b'keys'], _(b"sort spec must be a string"))
2369
2370    keyflags = []
2371    for k in keys.split():
2372        fk = k
2373        reverse = k.startswith(b'-')
2374        if reverse:
2375            k = k[1:]
2376        if k not in _sortkeyfuncs and k != b'topo':
2377            raise error.ParseError(
2378                _(b"unknown sort key %r") % pycompat.bytestr(fk)
2379            )
2380        keyflags.append((k, reverse))
2381
2382    if len(keyflags) > 1 and any(k == b'topo' for k, reverse in keyflags):
2383        # i18n: "topo" is a keyword
2384        raise error.ParseError(
2385            _(b'topo sort order cannot be combined with other sort keys')
2386        )
2387
2388    opts = {}
2389    if b'topo.firstbranch' in args:
2390        if any(k == b'topo' for k, reverse in keyflags):
2391            opts[b'topo.firstbranch'] = args[b'topo.firstbranch']
2392        else:
2393            # i18n: "topo" and "topo.firstbranch" are keywords
2394            raise error.ParseError(
2395                _(
2396                    b'topo.firstbranch can only be used '
2397                    b'when using the topo sort key'
2398                )
2399            )
2400
2401    return args[b'set'], keyflags, opts
2402
2403
2404@predicate(
2405    b'sort(set[, [-]key... [, ...]])', safe=True, takeorder=True, weight=10
2406)
2407def sort(repo, subset, x, order):
2408    """Sort set by keys. The default sort order is ascending, specify a key
2409    as ``-key`` to sort in descending order.
2410
2411    The keys can be:
2412
2413    - ``rev`` for the revision number,
2414    - ``branch`` for the branch name,
2415    - ``desc`` for the commit message (description),
2416    - ``user`` for user name (``author`` can be used as an alias),
2417    - ``date`` for the commit date
2418    - ``topo`` for a reverse topographical sort
2419    - ``node`` the nodeid of the revision
2420
2421    The ``topo`` sort order cannot be combined with other sort keys. This sort
2422    takes one optional argument, ``topo.firstbranch``, which takes a revset that
2423    specifies what topographical branches to prioritize in the sort.
2424
2425    """
2426    s, keyflags, opts = _getsortargs(x)
2427    revs = getset(repo, subset, s, order)
2428
2429    if not keyflags or order != defineorder:
2430        return revs
2431    if len(keyflags) == 1 and keyflags[0][0] == b"rev":
2432        revs.sort(reverse=keyflags[0][1])
2433        return revs
2434    elif keyflags[0][0] == b"topo":
2435        firstbranch = ()
2436        if b'topo.firstbranch' in opts:
2437            firstbranch = getset(repo, subset, opts[b'topo.firstbranch'])
2438        revs = baseset(
2439            dagop.toposort(revs, repo.changelog.parentrevs, firstbranch),
2440            istopo=True,
2441        )
2442        if keyflags[0][1]:
2443            revs.reverse()
2444        return revs
2445
2446    # sort() is guaranteed to be stable
2447    ctxs = [repo[r] for r in revs]
2448    for k, reverse in reversed(keyflags):
2449        ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse)
2450    return baseset([c.rev() for c in ctxs])
2451
2452
2453@predicate(b'subrepo([pattern])')
2454def subrepo(repo, subset, x):
2455    """Changesets that add, modify or remove the given subrepo.  If no subrepo
2456    pattern is named, any subrepo changes are returned.
2457    """
2458    # i18n: "subrepo" is a keyword
2459    args = getargs(x, 0, 1, _(b'subrepo takes at most one argument'))
2460    pat = None
2461    if len(args) != 0:
2462        pat = getstring(args[0], _(b"subrepo requires a pattern"))
2463
2464    m = matchmod.exact([b'.hgsubstate'])
2465
2466    def submatches(names):
2467        k, p, m = stringutil.stringmatcher(pat)
2468        for name in names:
2469            if m(name):
2470                yield name
2471
2472    def matches(x):
2473        c = repo[x]
2474        s = repo.status(c.p1().node(), c.node(), match=m)
2475
2476        if pat is None:
2477            return s.added or s.modified or s.removed
2478
2479        if s.added:
2480            return any(submatches(c.substate.keys()))
2481
2482        if s.modified:
2483            subs = set(c.p1().substate.keys())
2484            subs.update(c.substate.keys())
2485
2486            for path in submatches(subs):
2487                if c.p1().substate.get(path) != c.substate.get(path):
2488                    return True
2489
2490        if s.removed:
2491            return any(submatches(c.p1().substate.keys()))
2492
2493        return False
2494
2495    return subset.filter(matches, condrepr=(b'<subrepo %r>', pat))
2496
2497
2498def _mapbynodefunc(repo, s, f):
2499    """(repo, smartset, [node] -> [node]) -> smartset
2500
2501    Helper method to map a smartset to another smartset given a function only
2502    talking about nodes. Handles converting between rev numbers and nodes, and
2503    filtering.
2504    """
2505    cl = repo.unfiltered().changelog
2506    torev = cl.index.get_rev
2507    tonode = cl.node
2508    result = {torev(n) for n in f(tonode(r) for r in s)}
2509    result.discard(None)
2510    return smartset.baseset(result - repo.changelog.filteredrevs)
2511
2512
2513@predicate(b'successors(set)', safe=True)
2514def successors(repo, subset, x):
2515    """All successors for set, including the given set themselves.
2516    (EXPERIMENTAL)"""
2517    s = getset(repo, fullreposet(repo), x)
2518    f = lambda nodes: obsutil.allsuccessors(repo.obsstore, nodes)
2519    d = _mapbynodefunc(repo, s, f)
2520    return subset & d
2521
2522
2523def _substringmatcher(pattern, casesensitive=True):
2524    kind, pattern, matcher = stringutil.stringmatcher(
2525        pattern, casesensitive=casesensitive
2526    )
2527    if kind == b'literal':
2528        if not casesensitive:
2529            pattern = encoding.lower(pattern)
2530            matcher = lambda s: pattern in encoding.lower(s)
2531        else:
2532            matcher = lambda s: pattern in s
2533    return kind, pattern, matcher
2534
2535
2536@predicate(b'tag([name])', safe=True)
2537def tag(repo, subset, x):
2538    """The specified tag by name, or all tagged revisions if no name is given.
2539
2540    Pattern matching is supported for `name`. See
2541    :hg:`help revisions.patterns`.
2542    """
2543    # i18n: "tag" is a keyword
2544    args = getargs(x, 0, 1, _(b"tag takes one or no arguments"))
2545    cl = repo.changelog
2546    if args:
2547        pattern = getstring(
2548            args[0],
2549            # i18n: "tag" is a keyword
2550            _(b'the argument to tag must be a string'),
2551        )
2552        kind, pattern, matcher = stringutil.stringmatcher(pattern)
2553        if kind == b'literal':
2554            # avoid resolving all tags
2555            tn = repo._tagscache.tags.get(pattern, None)
2556            if tn is None:
2557                raise error.RepoLookupError(
2558                    _(b"tag '%s' does not exist") % pattern
2559                )
2560            s = {repo[tn].rev()}
2561        else:
2562            s = {cl.rev(n) for t, n in repo.tagslist() if matcher(t)}
2563    else:
2564        s = {cl.rev(n) for t, n in repo.tagslist() if t != b'tip'}
2565    return subset & s
2566
2567
2568@predicate(b'tagged', safe=True)
2569def tagged(repo, subset, x):
2570    return tag(repo, subset, x)
2571
2572
2573@predicate(b'orphan()', safe=True)
2574def orphan(repo, subset, x):
2575    """Non-obsolete changesets with obsolete ancestors. (EXPERIMENTAL)"""
2576    # i18n: "orphan" is a keyword
2577    getargs(x, 0, 0, _(b"orphan takes no arguments"))
2578    orphan = obsmod.getrevs(repo, b'orphan')
2579    return subset & orphan
2580
2581
2582@predicate(b'unstable()', safe=True)
2583def unstable(repo, subset, x):
2584    """Changesets with instabilities. (EXPERIMENTAL)"""
2585    # i18n: "unstable" is a keyword
2586    getargs(x, 0, 0, b'unstable takes no arguments')
2587    _unstable = set()
2588    _unstable.update(obsmod.getrevs(repo, b'orphan'))
2589    _unstable.update(obsmod.getrevs(repo, b'phasedivergent'))
2590    _unstable.update(obsmod.getrevs(repo, b'contentdivergent'))
2591    return subset & baseset(_unstable)
2592
2593
2594@predicate(b'user(string)', safe=True, weight=10)
2595def user(repo, subset, x):
2596    """User name contains string. The match is case-insensitive.
2597
2598    Pattern matching is supported for `string`. See
2599    :hg:`help revisions.patterns`.
2600    """
2601    return author(repo, subset, x)
2602
2603
2604@predicate(b'wdir()', safe=True, weight=0)
2605def wdir(repo, subset, x):
2606    """Working directory. (EXPERIMENTAL)"""
2607    # i18n: "wdir" is a keyword
2608    getargs(x, 0, 0, _(b"wdir takes no arguments"))
2609    if wdirrev in subset or isinstance(subset, fullreposet):
2610        return baseset([wdirrev])
2611    return baseset()
2612
2613
2614def _orderedlist(repo, subset, x):
2615    s = getstring(x, b"internal error")
2616    if not s:
2617        return baseset()
2618    # remove duplicates here. it's difficult for caller to deduplicate sets
2619    # because different symbols can point to the same rev.
2620    cl = repo.changelog
2621    ls = []
2622    seen = set()
2623    for t in s.split(b'\0'):
2624        try:
2625            # fast path for integer revision
2626            r = int(t)
2627            if (b'%d' % r) != t or r not in cl:
2628                raise ValueError
2629            revs = [r]
2630        except ValueError:
2631            revs = stringset(repo, subset, t, defineorder)
2632
2633        for r in revs:
2634            if r in seen:
2635                continue
2636            if (
2637                r in subset
2638                or r in _virtualrevs
2639                and isinstance(subset, fullreposet)
2640            ):
2641                ls.append(r)
2642            seen.add(r)
2643    return baseset(ls)
2644
2645
2646# for internal use
2647@predicate(b'_list', safe=True, takeorder=True)
2648def _list(repo, subset, x, order):
2649    if order == followorder:
2650        # slow path to take the subset order
2651        return subset & _orderedlist(repo, fullreposet(repo), x)
2652    else:
2653        return _orderedlist(repo, subset, x)
2654
2655
2656def _orderedintlist(repo, subset, x):
2657    s = getstring(x, b"internal error")
2658    if not s:
2659        return baseset()
2660    ls = [int(r) for r in s.split(b'\0')]
2661    s = subset
2662    return baseset([r for r in ls if r in s])
2663
2664
2665# for internal use
2666@predicate(b'_intlist', safe=True, takeorder=True, weight=0)
2667def _intlist(repo, subset, x, order):
2668    if order == followorder:
2669        # slow path to take the subset order
2670        return subset & _orderedintlist(repo, fullreposet(repo), x)
2671    else:
2672        return _orderedintlist(repo, subset, x)
2673
2674
2675def _orderedhexlist(repo, subset, x):
2676    s = getstring(x, b"internal error")
2677    if not s:
2678        return baseset()
2679    cl = repo.changelog
2680    ls = [cl.rev(bin(r)) for r in s.split(b'\0')]
2681    s = subset
2682    return baseset([r for r in ls if r in s])
2683
2684
2685# for internal use
2686@predicate(b'_hexlist', safe=True, takeorder=True)
2687def _hexlist(repo, subset, x, order):
2688    if order == followorder:
2689        # slow path to take the subset order
2690        return subset & _orderedhexlist(repo, fullreposet(repo), x)
2691    else:
2692        return _orderedhexlist(repo, subset, x)
2693
2694
2695methods = {
2696    b"range": rangeset,
2697    b"rangeall": rangeall,
2698    b"rangepre": rangepre,
2699    b"rangepost": rangepost,
2700    b"dagrange": dagrange,
2701    b"string": stringset,
2702    b"symbol": stringset,
2703    b"and": andset,
2704    b"andsmally": andsmallyset,
2705    b"or": orset,
2706    b"not": notset,
2707    b"difference": differenceset,
2708    b"relation": relationset,
2709    b"relsubscript": relsubscriptset,
2710    b"subscript": subscriptset,
2711    b"list": listset,
2712    b"keyvalue": keyvaluepair,
2713    b"func": func,
2714    b"ancestor": ancestorspec,
2715    b"parent": parentspec,
2716    b"parentpost": parentpost,
2717    b"smartset": rawsmartset,
2718}
2719
2720relations = {
2721    b"g": generationsrel,
2722    b"generations": generationsrel,
2723}
2724
2725subscriptrelations = {
2726    b"g": generationssubrel,
2727    b"generations": generationssubrel,
2728}
2729
2730
2731def lookupfn(repo):
2732    def fn(symbol):
2733        try:
2734            return scmutil.isrevsymbol(repo, symbol)
2735        except error.AmbiguousPrefixLookupError:
2736            raise error.InputError(
2737                b'ambiguous revision identifier: %s' % symbol
2738            )
2739
2740    return fn
2741
2742
2743def match(ui, spec, lookup=None):
2744    """Create a matcher for a single revision spec"""
2745    return matchany(ui, [spec], lookup=lookup)
2746
2747
2748def matchany(ui, specs, lookup=None, localalias=None):
2749    """Create a matcher that will include any revisions matching one of the
2750    given specs
2751
2752    If lookup function is not None, the parser will first attempt to handle
2753    old-style ranges, which may contain operator characters.
2754
2755    If localalias is not None, it is a dict {name: definitionstring}. It takes
2756    precedence over [revsetalias] config section.
2757    """
2758    if not specs:
2759
2760        def mfunc(repo, subset=None):
2761            return baseset()
2762
2763        return mfunc
2764    if not all(specs):
2765        raise error.ParseError(_(b"empty query"))
2766    if len(specs) == 1:
2767        tree = revsetlang.parse(specs[0], lookup)
2768    else:
2769        tree = (
2770            b'or',
2771            (b'list',) + tuple(revsetlang.parse(s, lookup) for s in specs),
2772        )
2773
2774    aliases = []
2775    warn = None
2776    if ui:
2777        aliases.extend(ui.configitems(b'revsetalias'))
2778        warn = ui.warn
2779    if localalias:
2780        aliases.extend(localalias.items())
2781    if aliases:
2782        tree = revsetlang.expandaliases(tree, aliases, warn=warn)
2783    tree = revsetlang.foldconcat(tree)
2784    tree = revsetlang.analyze(tree)
2785    tree = revsetlang.optimize(tree)
2786    return makematcher(tree)
2787
2788
2789def makematcher(tree):
2790    """Create a matcher from an evaluatable tree"""
2791
2792    def mfunc(repo, subset=None, order=None):
2793        if order is None:
2794            if subset is None:
2795                order = defineorder  # 'x'
2796            else:
2797                order = followorder  # 'subset & x'
2798        if subset is None:
2799            subset = fullreposet(repo)
2800        return getset(repo, subset, tree, order)
2801
2802    return mfunc
2803
2804
2805def loadpredicate(ui, extname, registrarobj):
2806    """Load revset predicates from specified registrarobj"""
2807    for name, func in pycompat.iteritems(registrarobj._table):
2808        symbols[name] = func
2809        if func._safe:
2810            safesymbols.add(name)
2811
2812
2813# load built-in predicates explicitly to setup safesymbols
2814loadpredicate(None, None, predicate)
2815
2816# tell hggettext to extract docstrings from these functions:
2817i18nfunctions = symbols.values()
2818