revset.py
2755 lines
| 84.8 KiB
| text/x-python
|
PythonLexer
/ mercurial / revset.py
|
r11275 | # revset.py - revision set queries for mercurial | ||
# | ||||
# Copyright 2010 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r25971 | from __future__ import absolute_import | ||
import re | ||||
from .i18n import _ | ||||
|
r43359 | from .pycompat import getattr | ||
|
r25971 | from . import ( | ||
|
r32903 | dagop, | ||
|
r26713 | destutil, | ||
|
r38607 | diffutil, | ||
|
r25971 | encoding, | ||
error, | ||||
hbisect, | ||||
match as matchmod, | ||||
node, | ||||
obsolete as obsmod, | ||||
|
r33377 | obsutil, | ||
|
r25971 | pathutil, | ||
phases, | ||||
|
r35368 | pycompat, | ||
|
r27584 | registrar, | ||
|
r25971 | repoview, | ||
|
r31024 | revsetlang, | ||
|
r32659 | scmutil, | ||
|
r30881 | smartset, | ||
|
r37407 | stack as stackmod, | ||
|
r25971 | util, | ||
) | ||||
|
r37102 | from .utils import ( | ||
dateutil, | ||||
stringutil, | ||||
) | ||||
|
r11275 | |||
|
r31024 | # helpers for processing parsed tree | ||
getsymbol = revsetlang.getsymbol | ||||
getstring = revsetlang.getstring | ||||
getinteger = revsetlang.getinteger | ||||
|
r31998 | getboolean = revsetlang.getboolean | ||
|
r31024 | getlist = revsetlang.getlist | ||
|
r41702 | getintrange = revsetlang.getintrange | ||
|
r31024 | getargs = revsetlang.getargs | ||
getargsdict = revsetlang.getargsdict | ||||
|
r30881 | baseset = smartset.baseset | ||
generatorset = smartset.generatorset | ||||
spanset = smartset.spanset | ||||
fullreposet = smartset.fullreposet | ||||
|
r42448 | # revisions not included in all(), but populated if specified | ||
_virtualrevs = (node.nullrev, node.wdirrev) | ||||
|
r34018 | # Constants for ordering requirement, used in getset(): | ||
# | ||||
# If 'define', any nested functions and operations MAY change the ordering of | ||||
# the entries in the set (but if changes the ordering, it MUST ALWAYS change | ||||
# it). If 'follow', any nested functions and operations MUST take the ordering | ||||
# specified by the first operand to the '&' operator. | ||||
# | ||||
# For instance, | ||||
# | ||||
# X & (Y | Z) | ||||
# ^ ^^^^^^^ | ||||
# | follow | ||||
# define | ||||
# | ||||
# will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order | ||||
# of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't. | ||||
# | ||||
# 'any' means the order doesn't matter. For instance, | ||||
# | ||||
|
r34019 | # (X & !Y) | ancestors(Z) | ||
# ^ ^ | ||||
# any any | ||||
|
r34018 | # | ||
|
r34019 | # For 'X & !Y', 'X' decides the order and 'Y' is subtracted from 'X', so the | ||
# order of 'Y' does not matter. For 'ancestors(Z)', Z's order does not matter | ||||
# since 'ancestors' does not care about the order of its argument. | ||||
|
r34018 | # | ||
# Currently, most revsets do not care about the order, so 'define' is | ||||
# equivalent to 'follow' for them, and the resulting order is based on the | ||||
# 'subset' parameter passed down to them: | ||||
# | ||||
|
r34020 | # m = revset.match(...) | ||
# m(repo, subset, order=defineorder) | ||||
|
r34018 | # ^^^^^^ | ||
# For most revsets, 'define' means using the order this subset provides | ||||
# | ||||
# There are a few revsets that always redefine the order if 'define' is | ||||
# specified: 'sort(X)', 'reverse(X)', 'x:y'. | ||||
|
r43347 | anyorder = b'any' # don't care the order, could be even random-shuffled | ||
defineorder = b'define' # ALWAYS redefine, or ALWAYS follow the current order | ||||
followorder = b'follow' # MUST follow the current order | ||||
|
r34018 | |||
|
r11275 | # helpers | ||
|
r43346 | |||
|
r34013 | def getset(repo, subset, x, order=defineorder): | ||
|
r11275 | if not x: | ||
|
r43347 | raise error.ParseError(_(b"missing argument")) | ||
|
r34013 | return methods[x[0]](repo, subset, *x[1:], order=order) | ||
|
r11275 | |||
|
r43346 | |||
|
r17003 | def _getrevsource(repo, r): | ||
extra = repo[r].extra() | ||||
|
r43347 | for label in (b'source', b'transplant_source', b'rebase_source'): | ||
|
r17003 | if label in extra: | ||
try: | ||||
return repo[extra[label]].rev() | ||||
except error.RepoLookupError: | ||||
pass | ||||
return None | ||||
|
r43346 | |||
|
r35922 | def _sortedb(xs): | ||
|
r38594 | return sorted(pycompat.rapply(pycompat.maybebytestr, xs)) | ||
|
r35922 | |||
|
r43346 | |||
|
r11275 | # operator methods | ||
|
r43346 | |||
|
r34013 | def stringset(repo, subset, x, order): | ||
|
r37281 | if not x: | ||
|
r43347 | raise error.ParseError(_(b"empty string is not a valid revision")) | ||
|
r37289 | x = scmutil.intrev(scmutil.revsymbol(repo, x)) | ||
|
r42449 | if x in subset or x in _virtualrevs and isinstance(subset, fullreposet): | ||
|
r20364 | return baseset([x]) | ||
|
r22802 | return baseset() | ||
|
r11275 | |||
|
r43346 | |||
|
r41258 | def rawsmartset(repo, subset, x, order): | ||
"""argument is already a smartset, use that directly""" | ||||
if order == followorder: | ||||
return subset & x | ||||
else: | ||||
return x & subset | ||||
|
r43346 | |||
|
r29932 | def rangeset(repo, subset, x, y, order): | ||
|
r23162 | m = getset(repo, fullreposet(repo), x) | ||
n = getset(repo, fullreposet(repo), y) | ||||
|
r11456 | |||
if not m or not n: | ||||
|
r22802 | return baseset() | ||
|
r30043 | return _makerangeset(repo, subset, m.first(), n.last(), order) | ||
|
r43346 | |||
|
r30803 | def rangeall(repo, subset, x, order): | ||
assert x is None | ||||
|
r35691 | return _makerangeset(repo, subset, 0, repo.changelog.tiprev(), order) | ||
|
r30803 | |||
|
r43346 | |||
|
r30044 | def rangepre(repo, subset, y, order): | ||
# ':y' can't be rewritten to '0:y' since '0' may be hidden | ||||
n = getset(repo, fullreposet(repo), y) | ||||
if not n: | ||||
return baseset() | ||||
return _makerangeset(repo, subset, 0, n.last(), order) | ||||
|
r43346 | |||
|
r30803 | def rangepost(repo, subset, x, order): | ||
m = getset(repo, fullreposet(repo), x) | ||||
if not m: | ||||
return baseset() | ||||
|
r43346 | return _makerangeset( | ||
repo, subset, m.first(), repo.changelog.tiprev(), order | ||||
) | ||||
|
r30803 | |||
|
r30043 | def _makerangeset(repo, subset, m, n, order): | ||
|
r25766 | if m == n: | ||
r = baseset([m]) | ||||
elif n == node.wdirrev: | ||||
r = spanset(repo, m, len(repo)) + baseset([n]) | ||||
elif m == node.wdirrev: | ||||
|
r35691 | r = baseset([m]) + spanset(repo, repo.changelog.tiprev(), n - 1) | ||
|
r25766 | elif m < n: | ||
|
r20526 | r = spanset(repo, m, n + 1) | ||
|
r11456 | else: | ||
|
r20526 | r = spanset(repo, m, n - 1) | ||
|
r29944 | |||
if order == defineorder: | ||||
return r & subset | ||||
else: | ||||
# carrying the sorting over when possible would be more efficient | ||||
return subset & r | ||||
|
r11275 | |||
|
r43346 | |||
|
r29932 | def dagrange(repo, subset, x, y, order): | ||
|
r24115 | r = fullreposet(repo) | ||
|
r43346 | xs = dagop.reachableroots( | ||
repo, getset(repo, r, x), getset(repo, r, y), includepath=True | ||||
) | ||||
|
r29139 | return subset & xs | ||
|
r16860 | |||
|
r43346 | |||
|
r29932 | def andset(repo, subset, x, y, order): | ||
|
r34013 | if order == anyorder: | ||
yorder = anyorder | ||||
else: | ||||
yorder = followorder | ||||
return getset(repo, getset(repo, subset, x, order), y, yorder) | ||||
|
r43346 | |||
|
r34022 | def andsmallyset(repo, subset, x, y, order): | ||
# 'andsmally(x, y)' is equivalent to 'and(x, y)', but faster when y is small | ||||
|
r34013 | if order == anyorder: | ||
yorder = anyorder | ||||
else: | ||||
yorder = followorder | ||||
return getset(repo, getset(repo, subset, y, yorder), x, order) | ||||
|
r11275 | |||
|
r43346 | |||
|
r29932 | def differenceset(repo, subset, x, y, order): | ||
|
r34013 | return getset(repo, subset, x, order) - getset(repo, subset, y, anyorder) | ||
|
r28217 | |||
|
r43346 | |||
|
r34013 | def _orsetlist(repo, subset, xs, order): | ||
|
r25929 | assert xs | ||
if len(xs) == 1: | ||||
|
r34013 | return getset(repo, subset, xs[0], order) | ||
|
r25929 | p = len(xs) // 2 | ||
|
r34013 | a = _orsetlist(repo, subset, xs[:p], order) | ||
b = _orsetlist(repo, subset, xs[p:], order) | ||||
|
r25929 | return a + b | ||
|
r11275 | |||
|
r43346 | |||
|
r29932 | def orset(repo, subset, x, order): | ||
|
r29934 | xs = getlist(x) | ||
|
r38508 | if not xs: | ||
return baseset() | ||||
|
r29934 | if order == followorder: | ||
# slow path to take the subset order | ||||
|
r34013 | return subset & _orsetlist(repo, fullreposet(repo), xs, anyorder) | ||
|
r29934 | else: | ||
|
r34013 | return _orsetlist(repo, subset, xs, order) | ||
|
r29929 | |||
|
r43346 | |||
|
r29932 | def notset(repo, subset, x, order): | ||
|
r34013 | return subset - getset(repo, subset, x, anyorder) | ||
|
r11275 | |||
|
r43346 | |||
|
r33416 | def relationset(repo, subset, x, y, order): | ||
r45204 | # this is pretty basic implementation of 'x#y' operator, still | |||
# experimental so undocumented. see the wiki for further ideas. | ||||
# https://www.mercurial-scm.org/wiki/RevsetOperatorPlan | ||||
rel = getsymbol(y) | ||||
if rel in relations: | ||||
return relations[rel](repo, subset, x, rel, order) | ||||
relnames = [r for r in relations.keys() if len(r) > 1] | ||||
raise error.UnknownIdentifier(rel, relnames) | ||||
|
r33416 | |||
|
r43346 | |||
r41395 | def _splitrange(a, b): | |||
"""Split range with bounds a and b into two ranges at 0 and return two | ||||
tuples of numbers for use as startdepth and stopdepth arguments of | ||||
revancestors and revdescendants. | ||||
>>> _splitrange(-10, -5) # [-10:-5] | ||||
((5, 11), (None, None)) | ||||
>>> _splitrange(5, 10) # [5:10] | ||||
((None, None), (5, 11)) | ||||
>>> _splitrange(-10, 10) # [-10:10] | ||||
((0, 11), (0, 11)) | ||||
>>> _splitrange(-10, 0) # [-10:0] | ||||
((0, 11), (None, None)) | ||||
>>> _splitrange(0, 10) # [0:10] | ||||
((None, None), (0, 11)) | ||||
>>> _splitrange(0, 0) # [0:0] | ||||
((0, 1), (None, None)) | ||||
>>> _splitrange(1, -1) # [1:-1] | ||||
((None, None), (None, None)) | ||||
""" | ||||
ancdepths = (None, None) | ||||
descdepths = (None, None) | ||||
if a == b == 0: | ||||
ancdepths = (0, 1) | ||||
if a < 0: | ||||
ancdepths = (-min(b, 0), -a + 1) | ||||
if b > 0: | ||||
descdepths = (max(a, 0), b + 1) | ||||
return ancdepths, descdepths | ||||
|
r43346 | |||
r45204 | def generationsrel(repo, subset, x, rel, order): | |||
z = (b'rangeall', None) | ||||
return generationssubrel(repo, subset, x, rel, z, order) | ||||
r45203 | def generationssubrel(repo, subset, x, rel, z, order): | |||
r41395 | # TODO: rewrite tests, and drop startdepth argument from ancestors() and | |||
# descendants() predicates | ||||
|
r43346 | a, b = getintrange( | ||
z, | ||||
|
r43347 | _(b'relation subscript must be an integer or a range'), | ||
_(b'relation subscript bounds must be integers'), | ||||
|
r43346 | deffirst=-(dagop.maxlogdepth - 1), | ||
deflast=+(dagop.maxlogdepth - 1), | ||||
) | ||||
r41395 | (ancstart, ancstop), (descstart, descstop) = _splitrange(a, b) | |||
if ancstart is None and descstart is None: | ||||
return baseset() | ||||
revs = getset(repo, fullreposet(repo), x) | ||||
if not revs: | ||||
return baseset() | ||||
if ancstart is not None and descstart is not None: | ||||
s = dagop.revancestors(repo, revs, False, ancstart, ancstop) | ||||
s += dagop.revdescendants(repo, revs, False, descstart, descstop) | ||||
elif ancstart is not None: | ||||
s = dagop.revancestors(repo, revs, False, ancstart, ancstop) | ||||
elif descstart is not None: | ||||
s = dagop.revdescendants(repo, revs, False, descstart, descstop) | ||||
return subset & s | ||||
r40967 | ||||
|
r43346 | |||
|
r33416 | def relsubscriptset(repo, subset, x, y, z, order): | ||
|
r33417 | # this is pretty basic implementation of 'x#y[z]' operator, still | ||
# experimental so undocumented. see the wiki for further ideas. | ||||
# https://www.mercurial-scm.org/wiki/RevsetOperatorPlan | ||||
rel = getsymbol(y) | ||||
r40967 | if rel in subscriptrelations: | |||
|
r41704 | return subscriptrelations[rel](repo, subset, x, rel, z, order) | ||
|
r33417 | |||
r40967 | relnames = [r for r in subscriptrelations.keys() if len(r) > 1] | |||
raise error.UnknownIdentifier(rel, relnames) | ||||
|
r33416 | |||
|
r43346 | |||
|
r33416 | def subscriptset(repo, subset, x, y, order): | ||
|
r43347 | raise error.ParseError(_(b"can't use a subscript in this context")) | ||
|
r33416 | |||
|
r43346 | |||
|
r34013 | def listset(repo, subset, *xs, **opts): | ||
|
r43346 | raise error.ParseError( | ||
|
r43347 | _(b"can't use a list in this context"), | ||
hint=_(b'see \'hg help "revsets.x or y"\''), | ||||
|
r43346 | ) | ||
|
r11275 | |||
|
r34013 | def keyvaluepair(repo, subset, k, v, order): | ||
|
r43347 | raise error.ParseError(_(b"can't use a key-value pair in this context")) | ||
|
r25704 | |||
|
r43346 | |||
|
r29932 | def func(repo, subset, a, b, order): | ||
|
r29441 | f = getsymbol(a) | ||
if f in symbols: | ||||
|
r30392 | func = symbols[f] | ||
if getattr(func, '_takeorder', False): | ||||
return func(repo, subset, b, order) | ||||
return func(repo, subset, b) | ||||
|
r25632 | |||
keep = lambda fn: getattr(fn, '__doc__', None) is not None | ||||
syms = [s for (s, fn) in symbols.items() if keep(fn)] | ||||
|
r29441 | raise error.UnknownIdentifier(f, syms) | ||
|
r11275 | |||
|
r43346 | |||
|
r11275 | # functions | ||
|
r27584 | # symbols are callables like: | ||
# fn(repo, subset, x) | ||||
# with: | ||||
# repo - current repository instance | ||||
# subset - of revisions to be examined | ||||
# x - argument in tree form | ||||
|
r34274 | symbols = revsetlang.symbols | ||
|
r27584 | |||
|
r27587 | # symbols which can't be used for a DoS attack for any given input | ||
# (e.g. those which accept regexes as plain strings shouldn't be included) | ||||
# functions that just return a lot of changesets (like all) don't count here | ||||
safesymbols = set() | ||||
|
r28395 | predicate = registrar.revsetpredicate() | ||
|
r27587 | |||
|
r43346 | |||
|
r43347 | @predicate(b'_destupdate') | ||
|
r26713 | def _destupdate(repo, subset, x): | ||
# experimental revset for update destination | ||||
|
r43347 | args = getargsdict(x, b'limit', b'clean') | ||
|
r43346 | return subset & baseset( | ||
[destutil.destupdate(repo, **pycompat.strkwargs(args))[0]] | ||||
) | ||||
|
r26713 | |||
|
r43347 | @predicate(b'_destmerge') | ||
|
r26716 | def _destmerge(repo, subset, x): | ||
# experimental revset for merge destination | ||||
|
r28139 | sourceset = None | ||
if x is not None: | ||||
sourceset = getset(repo, fullreposet(repo), x) | ||||
return subset & baseset([destutil.destmerge(repo, sourceset=sourceset)]) | ||||
|
r26303 | |||
|
r43346 | |||
|
r43347 | @predicate(b'adds(pattern)', safe=True, weight=30) | ||
|
r13915 | def adds(repo, subset, x): | ||
|
r27584 | """Changesets that add a file matching pattern. | ||
|
r20289 | |||
The pattern without explicit kind like ``glob:`` is expected to be | ||||
relative to the current directory and match against a file or a | ||||
directory. | ||||
|
r13915 | """ | ||
# i18n: "adds" is a keyword | ||||
|
r43347 | pat = getstring(x, _(b"adds requires a pattern")) | ||
|
r13915 | return checkstatus(repo, subset, pat, 1) | ||
|
r43346 | |||
|
r43347 | @predicate(b'ancestor(*changeset)', safe=True, weight=0.5) | ||
|
r13915 | def ancestor(repo, subset, x): | ||
|
r27584 | """A greatest common ancestor of the changesets. | ||
|
r18536 | |||
Accepts 0 or more changesets. | ||||
Will return empty list when passed no args. | ||||
Greatest common ancestor of a single changeset is that changeset. | ||||
|
r13915 | """ | ||
|
r38509 | reviter = iter(orset(repo, fullreposet(repo), x, order=anyorder)) | ||
try: | ||||
anc = repo[next(reviter)] | ||||
except StopIteration: | ||||
return baseset() | ||||
for r in reviter: | ||||
anc = anc.ancestor(repo[r]) | ||||
|
r20991 | |||
|
r38541 | r = scmutil.intrev(anc) | ||
if r in subset: | ||||
return baseset([r]) | ||||
|
r22802 | return baseset() | ||
|
r13915 | |||
|
r43346 | |||
def _ancestors( | ||||
repo, subset, x, followfirst=False, startdepth=None, stopdepth=None | ||||
): | ||||
|
r24115 | heads = getset(repo, fullreposet(repo), x) | ||
|
r22944 | if not heads: | ||
|
r22802 | return baseset() | ||
|
r33003 | s = dagop.revancestors(repo, heads, followfirst, startdepth, stopdepth) | ||
|
r23003 | return subset & s | ||
|
r16409 | |||
|
r43346 | |||
|
r43347 | @predicate(b'ancestors(set[, depth])', safe=True) | ||
|
r13915 | def ancestors(repo, subset, x): | ||
|
r32905 | """Changesets that are ancestors of changesets in set, including the | ||
given changesets themselves. | ||||
|
r33002 | |||
If depth is specified, the result only includes changesets up to | ||||
the specified generation. | ||||
|
r13915 | """ | ||
|
r33003 | # startdepth is for internal use only until we can decide the UI | ||
|
r43347 | args = getargsdict(x, b'ancestors', b'set depth startdepth') | ||
if b'set' not in args: | ||||
|
r32914 | # i18n: "ancestors" is a keyword | ||
|
r43347 | raise error.ParseError(_(b'ancestors takes at least 1 argument')) | ||
|
r33003 | startdepth = stopdepth = None | ||
|
r43347 | if b'startdepth' in args: | ||
|
r43346 | n = getinteger( | ||
|
r43347 | args[b'startdepth'], b"ancestors expects an integer startdepth" | ||
|
r43346 | ) | ||
|
r33003 | if n < 0: | ||
|
r43347 | raise error.ParseError(b"negative startdepth") | ||
|
r33003 | startdepth = n | ||
|
r43347 | if b'depth' in args: | ||
|
r33002 | # i18n: "ancestors" is a keyword | ||
|
r43347 | n = getinteger(args[b'depth'], _(b"ancestors expects an integer depth")) | ||
|
r33002 | if n < 0: | ||
|
r43347 | raise error.ParseError(_(b"negative depth")) | ||
|
r33002 | stopdepth = n + 1 | ||
|
r43346 | return _ancestors( | ||
|
r43347 | repo, subset, args[b'set'], startdepth=startdepth, stopdepth=stopdepth | ||
|
r43346 | ) | ||
|
r16409 | |||
|
r43347 | @predicate(b'_firstancestors', safe=True) | ||
|
r16409 | def _firstancestors(repo, subset, x): | ||
# ``_firstancestors(set)`` | ||||
# Like ``ancestors(set)`` but follows only the first parents. | ||||
return _ancestors(repo, subset, x, followfirst=True) | ||||
|
r13915 | |||
|
r43346 | |||
|
r32699 | def _childrenspec(repo, subset, x, n, order): | ||
"""Changesets that are the Nth child of a changeset | ||||
in set. | ||||
""" | ||||
cs = set() | ||||
for r in getset(repo, fullreposet(repo), x): | ||||
for i in range(n): | ||||
c = repo[r].children() | ||||
if len(c) == 0: | ||||
break | ||||
if len(c) > 1: | ||||
raise error.RepoLookupError( | ||||
|
r43347 | _(b"revision in set has more than one child") | ||
|
r43346 | ) | ||
|
r32885 | r = c[0].rev() | ||
|
r32699 | else: | ||
cs.add(r) | ||||
return subset & cs | ||||
|
r43346 | |||
|
r29932 | def ancestorspec(repo, subset, x, n, order): | ||
|
r14070 | """``set~n`` | ||
|
r16683 | Changesets that are the Nth ancestor (first parents only) of a changeset | ||
in set. | ||||
|
r14070 | """ | ||
|
r43347 | n = getinteger(n, _(b"~ expects a number")) | ||
|
r32699 | if n < 0: | ||
# children lookup | ||||
return _childrenspec(repo, subset, x, -n, order) | ||||
|
r14070 | ps = set() | ||
cl = repo.changelog | ||||
|
r23163 | for r in getset(repo, fullreposet(repo), x): | ||
|
r14070 | for i in range(n): | ||
|
r32441 | try: | ||
r = cl.parentrevs(r)[0] | ||||
except error.WdirUnsupported: | ||||
|
r41442 | r = repo[r].p1().rev() | ||
|
r14070 | ps.add(r) | ||
|
r22531 | return subset & ps | ||
|
r14070 | |||
|
r43346 | |||
|
r43347 | @predicate(b'author(string)', safe=True, weight=10) | ||
|
r13915 | def author(repo, subset, x): | ||
|
r27584 | """Alias for ``user(string)``. | ||
|
r13915 | """ | ||
# i18n: "author" is a keyword | ||||
|
r43347 | n = getstring(x, _(b"author requires a string")) | ||
|
r30782 | kind, pattern, matcher = _substringmatcher(n, casesensitive=False) | ||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda x: matcher(repo[x].user()), condrepr=(b'<user %r>', n) | ||
|
r43346 | ) | ||
|
r13915 | |||
|
r43347 | @predicate(b'bisect(string)', safe=True) | ||
|
r15134 | def bisect(repo, subset, x): | ||
|
r27584 | """Changesets marked in the specified bisect status: | ||
|
r15136 | |||
|
r15153 | - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip | ||
|
r17424 | - ``goods``, ``bads`` : csets topologically good/bad | ||
|
r15153 | - ``range`` : csets taking part in the bisection | ||
- ``pruned`` : csets that are goods, bads or skipped | ||||
- ``untested`` : csets whose fate is yet unknown | ||||
- ``ignored`` : csets ignored due to DAG topology | ||||
|
r16647 | - ``current`` : the cset currently being bisected | ||
|
r13915 | """ | ||
|
r17259 | # i18n: "bisect" is a keyword | ||
|
r43347 | status = getstring(x, _(b"bisect requires a string")).lower() | ||
|
r16467 | state = set(hbisect.get(repo, status)) | ||
|
r22532 | return subset & state | ||
|
r13915 | |||
|
r43346 | |||
|
r15134 | # Backward-compatibility | ||
# - no help entry so that we do not advertise it any more | ||||
|
r43347 | @predicate(b'bisected', safe=True) | ||
|
r15134 | def bisected(repo, subset, x): | ||
return bisect(repo, subset, x) | ||||
|
r43346 | |||
|
r43347 | @predicate(b'bookmark([name])', safe=True) | ||
|
r13915 | def bookmark(repo, subset, x): | ||
|
r27584 | """The named bookmark or all bookmarks. | ||
|
r16822 | |||
|
r30799 | Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. | ||
|
r13915 | """ | ||
# i18n: "bookmark" is a keyword | ||||
|
r43347 | args = getargs(x, 0, 1, _(b'bookmark takes one or no arguments')) | ||
|
r13915 | if args: | ||
|
r43346 | bm = getstring( | ||
args[0], | ||||
# i18n: "bookmark" is a keyword | ||||
|
r43347 | _(b'the argument to bookmark must be a string'), | ||
|
r43346 | ) | ||
|
r37102 | kind, pattern, matcher = stringutil.stringmatcher(bm) | ||
|
r22499 | bms = set() | ||
|
r43347 | if kind == b'literal': | ||
|
r39339 | if bm == pattern: | ||
pattern = repo._bookmarks.expandname(pattern) | ||||
|
r22105 | bmrev = repo._bookmarks.get(pattern, None) | ||
|
r16822 | if not bmrev: | ||
|
r43346 | raise error.RepoLookupError( | ||
|
r43347 | _(b"bookmark '%s' does not exist") % pattern | ||
|
r43346 | ) | ||
|
r22499 | bms.add(repo[bmrev].rev()) | ||
|
r16822 | else: | ||
matchrevs = set() | ||||
|
r43376 | for name, bmrev in pycompat.iteritems(repo._bookmarks): | ||
|
r16822 | if matcher(name): | ||
matchrevs.add(bmrev) | ||||
for bmrev in matchrevs: | ||||
|
r22499 | bms.add(repo[bmrev].rev()) | ||
else: | ||||
|
r32291 | bms = {repo[r].rev() for r in repo._bookmarks.values()} | ||
bms -= {node.nullrev} | ||||
|
r22530 | return subset & bms | ||
|
r13915 | |||
|
r43346 | |||
|
r43347 | @predicate(b'branch(string or set)', safe=True, weight=10) | ||
|
r13915 | def branch(repo, subset, x): | ||
|
r27584 | """ | ||
|
r13915 | All changesets belonging to the given branch or the branches of the given | ||
changesets. | ||||
|
r16821 | |||
|
r30784 | Pattern matching is supported for `string`. See | ||
|
r30799 | :hg:`help revisions.patterns`. | ||
|
r13915 | """ | ||
|
r24374 | getbi = repo.revbranchcache().branchinfo | ||
|
r43346 | |||
|
r32683 | def getbranch(r): | ||
try: | ||||
return getbi(r)[0] | ||||
except error.WdirUnsupported: | ||||
return repo[r].branch() | ||||
|
r23787 | |||
|
r13915 | try: | ||
|
r43347 | b = getstring(x, b'') | ||
|
r13915 | except error.ParseError: | ||
# not a string, but another revspec, e.g. tip() | ||||
pass | ||||
|
r16821 | else: | ||
|
r37102 | kind, pattern, matcher = stringutil.stringmatcher(b) | ||
|
r43347 | if kind == b'literal': | ||
|
r16821 | # note: falls through to the revspec case if no branch with | ||
|
r26537 | # this name exists and pattern kind is not specified explicitly | ||
|
r42171 | if repo.branchmap().hasbranch(pattern): | ||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda r: matcher(getbranch(r)), | ||
condrepr=(b'<branch %r>', b), | ||||
|
r43346 | ) | ||
|
r43347 | if b.startswith(b'literal:'): | ||
|
r43346 | raise error.RepoLookupError( | ||
|
r43347 | _(b"branch '%s' does not exist") % pattern | ||
|
r43346 | ) | ||
|
r16821 | else: | ||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda r: matcher(getbranch(r)), condrepr=(b'<branch %r>', b) | ||
|
r43346 | ) | ||
|
r13915 | |||
|
r24115 | s = getset(repo, fullreposet(repo), x) | ||
|
r13915 | b = set() | ||
for r in s: | ||||
|
r32683 | b.add(getbranch(r)) | ||
|
r22867 | c = s.__contains__ | ||
|
r43346 | return subset.filter( | ||
lambda r: c(r) or getbranch(r) in b, | ||||
|
r43347 | condrepr=lambda: b'<branch %r>' % _sortedb(b), | ||
|
r43346 | ) | ||
|
r13915 | |||
|
r43347 | @predicate(b'phasedivergent()', safe=True) | ||
|
r33771 | def phasedivergent(repo, subset, x): | ||
|
r27584 | """Mutable changesets marked as successors of public changesets. | ||
|
r17829 | |||
|
r33771 | Only non-public and non-obsolete changesets can be `phasedivergent`. | ||
|
r33855 | (EXPERIMENTAL) | ||
|
r17829 | """ | ||
|
r33771 | # i18n: "phasedivergent" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"phasedivergent takes no arguments")) | ||
phasedivergent = obsmod.getrevs(repo, b'phasedivergent') | ||||
|
r33774 | return subset & phasedivergent | ||
|
r17829 | |||
|
r43346 | |||
|
r43347 | @predicate(b'bundle()', safe=True) | ||
|
r17913 | def bundle(repo, subset, x): | ||
|
r27584 | """Changesets in the bundle. | ||
|
r17913 | |||
Bundle must be specified by the -R option.""" | ||||
try: | ||||
|
r18411 | bundlerevs = repo.changelog.bundlerevs | ||
|
r17913 | except AttributeError: | ||
|
r43347 | raise error.Abort(_(b"no bundle provided - specify with -R")) | ||
|
r20367 | return subset & bundlerevs | ||
|
r17913 | |||
|
r43346 | |||
|
r13915 | def checkstatus(repo, subset, pat, field): | ||
|
r42273 | """Helper for status-related revsets (adds, removes, modifies). | ||
The field parameter says which kind is desired: | ||||
0: modified | ||||
1: added | ||||
2: removed | ||||
""" | ||||
|
r44049 | label = {0: 'modified', 1: 'added', 2: 'removed'}[field] | ||
|
r43347 | hasset = matchmod.patkind(pat) == b'set' | ||
|
r20457 | |||
|
r23115 | mcache = [None] | ||
|
r43346 | |||
|
r20457 | def matches(x): | ||
c = repo[x] | ||||
|
r23115 | if not mcache[0] or hasset: | ||
mcache[0] = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c) | ||||
m = mcache[0] | ||||
fname = None | ||||
|
r44119 | |||
assert m is not None # help pytype | ||||
|
r23115 | if not m.anypats() and len(m.files()) == 1: | ||
fname = m.files()[0] | ||||
|
r16521 | if fname is not None: | ||
if fname not in c.files(): | ||||
|
r20457 | return False | ||
|
r13915 | else: | ||
|
r43989 | if not any(m(f) for f in c.files()): | ||
|
r20457 | return False | ||
|
r44049 | files = getattr(repo.status(c.p1().node(), c.node()), label) | ||
|
r16521 | if fname is not None: | ||
if fname in files: | ||||
|
r20457 | return True | ||
|
r13915 | else: | ||
|
r43989 | if any(m(f) for f in files): | ||
return True | ||||
|
r20457 | |||
|
r43347 | return subset.filter(matches, condrepr=(b'<status[%r] %r>', field, pat)) | ||
|
r13915 | |||
|
r43346 | |||
|
r29406 | def _children(repo, subset, parentset): | ||
|
r25550 | if not parentset: | ||
return baseset() | ||||
|
r15899 | cs = set() | ||
pr = repo.changelog.parentrevs | ||||
|
r25567 | minrev = parentset.min() | ||
|
r30699 | nullrev = node.nullrev | ||
|
r29406 | for r in subset: | ||
|
r18063 | if r <= minrev: | ||
continue | ||||
|
r30699 | p1, p2 = pr(r) | ||
if p1 in parentset: | ||||
cs.add(r) | ||||
if p2 != nullrev and p2 in parentset: | ||||
cs.add(r) | ||||
|
r20709 | return baseset(cs) | ||
|
r15899 | |||
|
r43346 | |||
|
r43347 | @predicate(b'children(set)', safe=True) | ||
|
r13915 | def children(repo, subset, x): | ||
|
r27584 | """Child changesets of changesets in set. | ||
|
r13915 | """ | ||
|
r23164 | s = getset(repo, fullreposet(repo), x) | ||
|
r15899 | cs = _children(repo, subset, s) | ||
|
r20367 | return subset & cs | ||
|
r13915 | |||
|
r43346 | |||
|
r43347 | @predicate(b'closed()', safe=True, weight=10) | ||
|
r13915 | def closed(repo, subset, x): | ||
|
r27584 | """Changeset is closed. | ||
|
r13915 | """ | ||
# i18n: "closed" is a keyword | ||||
|
r43347 | getargs(x, 0, 0, _(b"closed takes no arguments")) | ||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda r: repo[r].closesbranch(), condrepr=b'<branch closed>' | ||
|
r43346 | ) | ||
|
r13915 | |||
|
r38644 | # for internal use | ||
|
r43347 | @predicate(b'_commonancestorheads(set)', safe=True) | ||
|
r38644 | def _commonancestorheads(repo, subset, x): | ||
# This is an internal method is for quickly calculating "heads(::x and | ||||
# ::y)" | ||||
|
r39863 | # These greatest common ancestors are the same ones that the consensus bid | ||
|
r38644 | # merge will find. | ||
|
r39863 | startrevs = getset(repo, fullreposet(repo), x, order=anyorder) | ||
|
r38644 | |||
|
r39863 | ancs = repo.changelog._commonancestorsheads(*list(startrevs)) | ||
|
r38644 | return subset & baseset(ancs) | ||
|
r43346 | |||
|
r43347 | @predicate(b'commonancestors(set)', safe=True) | ||
|
r38643 | def commonancestors(repo, subset, x): | ||
|
r39861 | """Changesets that are ancestors of every changeset in set. | ||
|
r38643 | """ | ||
|
r39859 | startrevs = getset(repo, fullreposet(repo), x, order=anyorder) | ||
if not startrevs: | ||||
|
r38727 | return baseset() | ||
|
r39859 | for r in startrevs: | ||
|
r38643 | subset &= dagop.revancestors(repo, baseset([r])) | ||
return subset | ||||
|
r43346 | |||
|
r44817 | @predicate(b'conflictlocal()', safe=True) | ||
def conflictlocal(repo, subset, x): | ||||
"""The local side of the merge, if currently in an unresolved merge. | ||||
"merge" here includes merge conflicts from e.g. 'hg rebase' or 'hg graft'. | ||||
""" | ||||
getargs(x, 0, 0, _(b"conflictlocal takes no arguments")) | ||||
from . import merge | ||||
mergestate = merge.mergestate.read(repo) | ||||
if mergestate.active() and repo.changelog.hasnode(mergestate.local): | ||||
return subset & {repo.changelog.rev(mergestate.local)} | ||||
return baseset() | ||||
@predicate(b'conflictother()', safe=True) | ||||
def conflictother(repo, subset, x): | ||||
"""The other side of the merge, if currently in an unresolved merge. | ||||
"merge" here includes merge conflicts from e.g. 'hg rebase' or 'hg graft'. | ||||
""" | ||||
getargs(x, 0, 0, _(b"conflictother takes no arguments")) | ||||
from . import merge | ||||
mergestate = merge.mergestate.read(repo) | ||||
if mergestate.active() and repo.changelog.hasnode(mergestate.other): | ||||
return subset & {repo.changelog.rev(mergestate.other)} | ||||
return baseset() | ||||
|
r43347 | @predicate(b'contains(pattern)', weight=100) | ||
|
r13915 | def contains(repo, subset, x): | ||
|
r27584 | """The revision's manifest contains a file matching pattern (but might not | ||
|
r21199 | modify it). See :hg:`help patterns` for information about file patterns. | ||
|
r20289 | |||
The pattern without explicit kind like ``glob:`` is expected to be | ||||
relative to the current directory and match against a file exactly | ||||
for efficiency. | ||||
|
r13915 | """ | ||
# i18n: "contains" is a keyword | ||||
|
r43347 | pat = getstring(x, _(b"contains requires a pattern")) | ||
|
r20461 | |||
def matches(x): | ||||
if not matchmod.patkind(pat): | ||||
pats = pathutil.canonpath(repo.root, repo.getcwd(), pat) | ||||
if pats in repo[x]: | ||||
return True | ||||
else: | ||||
c = repo[x] | ||||
m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c) | ||||
|
r15964 | for f in c.manifest(): | ||
|
r13915 | if m(f): | ||
|
r20461 | return True | ||
return False | ||||
|
r43347 | return subset.filter(matches, condrepr=(b'<contains %r>', pat)) | ||
@predicate(b'converted([id])', safe=True) | ||||
|
r17002 | def converted(repo, subset, x): | ||
|
r27584 | """Changesets converted from the given identifier in the old repository if | ||
|
r17002 | present, or all converted changesets if no identifier is specified. | ||
""" | ||||
# There is exactly no chance of resolving the revision, so do a simple | ||||
# string compare and hope for the best | ||||
|
r17259 | rev = None | ||
|
r17002 | # i18n: "converted" is a keyword | ||
|
r43347 | l = getargs(x, 0, 1, _(b'converted takes one or no arguments')) | ||
|
r17002 | if l: | ||
|
r17259 | # i18n: "converted" is a keyword | ||
|
r43347 | rev = getstring(l[0], _(b'converted requires a revision')) | ||
|
r17002 | |||
def _matchvalue(r): | ||||
|
r43347 | source = repo[r].extra().get(b'convert_revision', None) | ||
|
r17002 | return source is not None and (rev is None or source.startswith(rev)) | ||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda r: _matchvalue(r), condrepr=(b'<converted %r>', rev) | ||
|
r43346 | ) | ||
|
r17002 | |||
|
r43347 | @predicate(b'date(interval)', safe=True, weight=10) | ||
|
r13915 | def date(repo, subset, x): | ||
|
r27584 | """Changesets within the interval, see :hg:`help dates`. | ||
|
r13915 | """ | ||
# i18n: "date" is a keyword | ||||
|
r43347 | ds = getstring(x, _(b"date requires a string")) | ||
|
r36625 | dm = dateutil.matchdate(ds) | ||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda x: dm(repo[x].date()[0]), condrepr=(b'<date %r>', ds) | ||
|
r43346 | ) | ||
|
r13915 | |||
|
r43347 | @predicate(b'desc(string)', safe=True, weight=10) | ||
|
r14650 | def desc(repo, subset, x): | ||
|
r27584 | """Search commit message for string. The match is case-insensitive. | ||
|
r30783 | |||
|
r30784 | Pattern matching is supported for `string`. See | ||
|
r30799 | :hg:`help revisions.patterns`. | ||
|
r14650 | """ | ||
# i18n: "desc" is a keyword | ||||
|
r43347 | ds = getstring(x, _(b"desc requires a string")) | ||
|
r30783 | |||
kind, pattern, matcher = _substringmatcher(ds, casesensitive=False) | ||||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda r: matcher(repo[r].description()), condrepr=(b'<desc %r>', ds) | ||
|
r43346 | ) | ||
def _descendants( | ||||
repo, subset, x, followfirst=False, startdepth=None, stopdepth=None | ||||
): | ||||
|
r24115 | roots = getset(repo, fullreposet(repo), x) | ||
|
r22944 | if not roots: | ||
|
r22802 | return baseset() | ||
|
r33080 | s = dagop.revdescendants(repo, roots, followfirst, startdepth, stopdepth) | ||
|
r33075 | return subset & s | ||
|
r16409 | |||
|
r43346 | |||
|
r43347 | @predicate(b'descendants(set[, depth])', safe=True) | ||
|
r13915 | def descendants(repo, subset, x): | ||
|
r32905 | """Changesets which are descendants of changesets in set, including the | ||
given changesets themselves. | ||||
|
r33080 | |||
If depth is specified, the result only includes changesets up to | ||||
the specified generation. | ||||
|
r13915 | """ | ||
|
r33080 | # startdepth is for internal use only until we can decide the UI | ||
|
r43347 | args = getargsdict(x, b'descendants', b'set depth startdepth') | ||
if b'set' not in args: | ||||
|
r32914 | # i18n: "descendants" is a keyword | ||
|
r43347 | raise error.ParseError(_(b'descendants takes at least 1 argument')) | ||
|
r33080 | startdepth = stopdepth = None | ||
|
r43347 | if b'startdepth' in args: | ||
|
r43346 | n = getinteger( | ||
|
r43347 | args[b'startdepth'], b"descendants expects an integer startdepth" | ||
|
r43346 | ) | ||
|
r33080 | if n < 0: | ||
|
r43347 | raise error.ParseError(b"negative startdepth") | ||
|
r33080 | startdepth = n | ||
|
r43347 | if b'depth' in args: | ||
|
r33080 | # i18n: "descendants" is a keyword | ||
|
r43347 | n = getinteger( | ||
args[b'depth'], _(b"descendants expects an integer depth") | ||||
) | ||||
|
r33080 | if n < 0: | ||
|
r43347 | raise error.ParseError(_(b"negative depth")) | ||
|
r33080 | stopdepth = n + 1 | ||
|
r43346 | return _descendants( | ||
|
r43347 | repo, subset, args[b'set'], startdepth=startdepth, stopdepth=stopdepth | ||
|
r43346 | ) | ||
|
r16409 | |||
|
r43347 | @predicate(b'_firstdescendants', safe=True) | ||
|
r16409 | def _firstdescendants(repo, subset, x): | ||
# ``_firstdescendants(set)`` | ||||
# Like ``descendants(set)`` but follows only the first parents. | ||||
return _descendants(repo, subset, x, followfirst=True) | ||||
|
r13915 | |||
|
r43346 | |||
|
r43347 | @predicate(b'destination([set])', safe=True, weight=10) | ||
|
r17186 | def destination(repo, subset, x): | ||
|
r27584 | """Changesets that were created by a graft, transplant or rebase operation, | ||
|
r17186 | with the given revisions specified as the source. Omitting the optional set | ||
is the same as passing all(). | ||||
""" | ||||
if x is not None: | ||||
|
r24115 | sources = getset(repo, fullreposet(repo), x) | ||
|
r17186 | else: | ||
|
r24201 | sources = fullreposet(repo) | ||
|
r17186 | |||
dests = set() | ||||
# subset contains all of the possible destinations that can be returned, so | ||||
|
r22944 | # iterate over them and see if their source(s) were provided in the arg set. | ||
# Even if the immediate src of r is not in the arg set, src's source (or | ||||
|
r17186 | # further back) may be. Scanning back further than the immediate src allows | ||
# transitive transplants and rebases to yield the same results as transitive | ||||
# grafts. | ||||
for r in subset: | ||||
src = _getrevsource(repo, r) | ||||
lineage = None | ||||
while src is not None: | ||||
if lineage is None: | ||||
lineage = list() | ||||
lineage.append(r) | ||||
# The visited lineage is a match if the current source is in the arg | ||||
# set. Since every candidate dest is visited by way of iterating | ||||
|
r17494 | # subset, any dests further back in the lineage will be tested by a | ||
|
r17186 | # different iteration over subset. Likewise, if the src was already | ||
# selected, the current lineage can be selected without going back | ||||
# further. | ||||
|
r22944 | if src in sources or src in dests: | ||
|
r17186 | dests.update(lineage) | ||
break | ||||
r = src | ||||
src = _getrevsource(repo, r) | ||||
|
r43346 | return subset.filter( | ||
dests.__contains__, | ||||
|
r43347 | condrepr=lambda: b'<destination %r>' % _sortedb(dests), | ||
|
r43346 | ) | ||
|
r17186 | |||
|
r43347 | @predicate(b'contentdivergent()', safe=True) | ||
|
r33770 | def contentdivergent(repo, subset, x): | ||
|
r27584 | """ | ||
|
r33855 | Final successors of changesets with an alternative set of final | ||
successors. (EXPERIMENTAL) | ||||
|
r18071 | """ | ||
|
r33770 | # i18n: "contentdivergent" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"contentdivergent takes no arguments")) | ||
contentdivergent = obsmod.getrevs(repo, b'contentdivergent') | ||||
|
r33773 | return subset & contentdivergent | ||
|
r18071 | |||
|
r43346 | |||
|
r43347 | @predicate(b'expectsize(set[, size])', safe=True, takeorder=True) | ||
|
r41829 | def expectsize(repo, subset, x, order): | ||
|
r41847 | """Return the given revset if size matches the revset size. | ||
Abort if the revset doesn't expect given size. | ||||
size can either be an integer range or an integer. | ||||
For example, ``expectsize(0:1, 3:5)`` will abort as revset size is 2 and | ||||
2 is not between 3 and 5 inclusive.""" | ||||
|
r43347 | args = getargsdict(x, b'expectsize', b'set size') | ||
|
r41829 | minsize = 0 | ||
maxsize = len(repo) + 1 | ||||
|
r43347 | err = b'' | ||
if b'size' not in args or b'set' not in args: | ||||
raise error.ParseError(_(b'invalid set of arguments')) | ||||
|
r43346 | minsize, maxsize = getintrange( | ||
|
r43347 | args[b'size'], | ||
|
r43387 | _(b'expectsize requires a size range or a positive integer'), | ||
|
r43347 | _(b'size range bounds must be integers'), | ||
|
r43346 | minsize, | ||
maxsize, | ||||
) | ||||
|
r41829 | if minsize < 0 or maxsize < 0: | ||
|
r43347 | raise error.ParseError(_(b'negative size')) | ||
rev = getset(repo, fullreposet(repo), args[b'set'], order=order) | ||||
|
r41829 | if minsize != maxsize and (len(rev) < minsize or len(rev) > maxsize): | ||
|
r43387 | err = _(b'revset size mismatch. expected between %d and %d, got %d') % ( | ||
minsize, | ||||
maxsize, | ||||
len(rev), | ||||
) | ||||
|
r41829 | elif minsize == maxsize and len(rev) != minsize: | ||
|
r43387 | err = _(b'revset size mismatch. expected %d, got %d') % ( | ||
|
r43346 | minsize, | ||
len(rev), | ||||
) | ||||
|
r41829 | if err: | ||
raise error.RepoLookupError(err) | ||||
if order == followorder: | ||||
return subset & rev | ||||
else: | ||||
return rev & subset | ||||
|
r43346 | |||
|
r43347 | @predicate(b'extdata(source)', safe=False, weight=100) | ||
|
r34458 | def extdata(repo, subset, x): | ||
"""Changesets in the specified extdata source. (EXPERIMENTAL)""" | ||||
# i18n: "extdata" is a keyword | ||||
|
r43347 | args = getargsdict(x, b'extdata', b'source') | ||
|
r43346 | source = getstring( | ||
|
r43347 | args.get(b'source'), | ||
|
r43346 | # i18n: "extdata" is a keyword | ||
|
r43347 | _(b'extdata takes at least 1 string argument'), | ||
|
r43346 | ) | ||
|
r34458 | data = scmutil.extdatasource(repo, source) | ||
return subset & baseset(data) | ||||
|
r43346 | |||
|
r43347 | @predicate(b'extinct()', safe=True) | ||
|
r17173 | def extinct(repo, subset, x): | ||
|
r45190 | """Obsolete changesets with obsolete descendants only. (EXPERIMENTAL) | ||
|
r17291 | """ | ||
|
r17259 | # i18n: "extinct" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"extinct takes no arguments")) | ||
extincts = obsmod.getrevs(repo, b'extinct') | ||||
|
r20367 | return subset & extincts | ||
|
r17173 | |||
|
r43346 | |||
|
r43347 | @predicate(b'extra(label, [value])', safe=True) | ||
|
r16661 | def extra(repo, subset, x): | ||
|
r27584 | """Changesets with the given label in the extra metadata, with the given | ||
|
r16824 | optional value. | ||
|
r30784 | Pattern matching is supported for `value`. See | ||
|
r30799 | :hg:`help revisions.patterns`. | ||
|
r16824 | """ | ||
|
r43347 | args = getargsdict(x, b'extra', b'label value') | ||
if b'label' not in args: | ||||
|
r25706 | # i18n: "extra" is a keyword | ||
|
r43347 | raise error.ParseError(_(b'extra takes at least 1 argument')) | ||
|
r17259 | # i18n: "extra" is a keyword | ||
|
r43346 | label = getstring( | ||
|
r43387 | args[b'label'], _(b'first argument to extra must be a string') | ||
|
r43346 | ) | ||
|
r16661 | value = None | ||
|
r43347 | if b'value' in args: | ||
|
r17259 | # i18n: "extra" is a keyword | ||
|
r43346 | value = getstring( | ||
|
r43387 | args[b'value'], _(b'second argument to extra must be a string') | ||
|
r43346 | ) | ||
|
r37102 | kind, value, matcher = stringutil.stringmatcher(value) | ||
|
r16661 | |||
def _matchvalue(r): | ||||
extra = repo[r].extra() | ||||
|
r16824 | return label in extra and (value is None or matcher(extra[label])) | ||
|
r16661 | |||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda r: _matchvalue(r), condrepr=(b'<extra[%r] %r>', label, value) | ||
|
r43346 | ) | ||
|
r15819 | |||
|
r43347 | @predicate(b'filelog(pattern)', safe=True) | ||
|
r14342 | def filelog(repo, subset, x): | ||
|
r27584 | """Changesets connected to the specified filelog. | ||
|
r17244 | |||
|
r21199 | For performance reasons, visits only revisions mentioned in the file-level | ||
filelog, rather than filtering through all changesets (much faster, but | ||||
doesn't include deletes or duplicate changes). For a slower, more accurate | ||||
result, use ``file()``. | ||||
|
r20289 | |||
The pattern without explicit kind like ``glob:`` is expected to be | ||||
relative to the current directory and match against a file exactly | ||||
for efficiency. | ||||
|
r14342 | """ | ||
|
r17259 | # i18n: "filelog" is a keyword | ||
|
r43347 | pat = getstring(x, _(b"filelog requires a pattern")) | ||
|
r14342 | s = set() | ||
|
r23719 | cl = repo.changelog | ||
|
r14342 | |||
|
r15964 | if not matchmod.patkind(pat): | ||
|
r20288 | f = pathutil.canonpath(repo.root, repo.getcwd(), pat) | ||
|
r23719 | files = [f] | ||
|
r14342 | else: | ||
|
r20288 | m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None]) | ||
|
r23719 | files = (f for f in repo[None] if m(f)) | ||
for f in files: | ||||
fl = repo.file(f) | ||||
|
r27945 | known = {} | ||
scanpos = 0 | ||||
|
r23719 | for fr in list(fl): | ||
|
r27945 | fn = fl.node(fr) | ||
if fn in known: | ||||
s.add(known[fn]) | ||||
|
r23821 | continue | ||
|
r27945 | |||
lr = fl.linkrev(fr) | ||||
if lr in cl: | ||||
s.add(lr) | ||||
elif scanpos is not None: | ||||
# lowest matching changeset is filtered, scan further | ||||
# ahead in changelog | ||||
start = max(lr, scanpos) + 1 | ||||
scanpos = None | ||||
for r in cl.revs(start): | ||||
# minimize parsing of non-matching entries | ||||
if f in cl.revision(r) and f in cl.readfiles(r): | ||||
try: | ||||
# try to use manifest delta fastpath | ||||
n = repo[r].filenode(f) | ||||
if n not in known: | ||||
if n == fn: | ||||
s.add(r) | ||||
scanpos = r | ||||
break | ||||
else: | ||||
known[n] = r | ||||
except error.ManifestLookupError: | ||||
# deletion in changelog | ||||
continue | ||||
|
r14342 | |||
|
r22534 | return subset & s | ||
|
r14342 | |||
|
r43346 | |||
|
r43347 | @predicate(b'first(set, [n])', safe=True, takeorder=True, weight=0) | ||
|
r32800 | def first(repo, subset, x, order): | ||
|
r27584 | """An alias for limit(). | ||
|
r15117 | """ | ||
|
r32800 | return limit(repo, subset, x, order) | ||
|
r15117 | |||
|
r43346 | |||
|
r16185 | def _follow(repo, subset, x, name, followfirst=False): | ||
|
r43347 | args = getargsdict(x, name, b'file startrev') | ||
|
r35301 | revs = None | ||
|
r43347 | if b'startrev' in args: | ||
revs = getset(repo, fullreposet(repo), args[b'startrev']) | ||||
if b'file' in args: | ||||
x = getstring(args[b'file'], _(b"%s expected a pattern") % name) | ||||
|
r35301 | if revs is None: | ||
revs = [None] | ||||
|
r35299 | fctxs = [] | ||
for r in revs: | ||||
ctx = mctx = repo[r] | ||||
if r is None: | ||||
|
r43347 | ctx = repo[b'.'] | ||
|
r43346 | m = matchmod.match( | ||
|
r43347 | repo.root, repo.getcwd(), [x], ctx=mctx, default=b'path' | ||
|
r43346 | ) | ||
|
r35299 | fctxs.extend(ctx[f].introfilectx() for f in ctx.manifest().walk(m)) | ||
|
r35297 | s = dagop.filerevancestors(fctxs, followfirst) | ||
|
r16185 | else: | ||
|
r35301 | if revs is None: | ||
|
r43347 | revs = baseset([repo[b'.'].rev()]) | ||
|
r35301 | s = dagop.revancestors(repo, revs, followfirst) | ||
|
r16185 | |||
|
r22535 | return subset & s | ||
|
r16185 | |||
|
r43346 | |||
|
r43347 | @predicate(b'follow([file[, startrev]])', safe=True) | ||
|
r13915 | def follow(repo, subset, x): | ||
|
r27584 | """ | ||
|
r24366 | An alias for ``::.`` (ancestors of the working directory's first parent). | ||
|
r35300 | If file pattern is specified, the histories of files matching given | ||
|
r29814 | pattern in the revision given by startrev are followed, including copies. | ||
|
r14343 | """ | ||
|
r43347 | return _follow(repo, subset, x, b'follow') | ||
@predicate(b'_followfirst', safe=True) | ||||
|
r16174 | def _followfirst(repo, subset, x): | ||
|
r35300 | # ``followfirst([file[, startrev]])`` | ||
# Like ``follow([file[, startrev]])`` but follows only the first parent | ||||
|
r29814 | # of every revisions or files revisions. | ||
|
r43347 | return _follow(repo, subset, x, b'_followfirst', followfirst=True) | ||
|
r14343 | |||
|
r43346 | |||
@predicate( | ||||
|
r43347 | b'followlines(file, fromline:toline[, startrev=., descend=False])', | ||
safe=True, | ||||
|
r43346 | ) | ||
|
r30719 | def followlines(repo, subset, x): | ||
"""Changesets modifying `file` in line range ('fromline', 'toline'). | ||||
|
r30800 | Line range corresponds to 'file' content at 'startrev' and should hence be | ||
consistent with file size. If startrev is not specified, working directory's | ||||
|
r30719 | parent is used. | ||
|
r31938 | |||
By default, ancestors of 'startrev' are returned. If 'descend' is True, | ||||
descendants of 'startrev' are returned though renames are (currently) not | ||||
followed in this direction. | ||||
|
r30719 | """ | ||
|
r43347 | args = getargsdict(x, b'followlines', b'file *lines startrev descend') | ||
if len(args[b'lines']) != 1: | ||||
raise error.ParseError(_(b"followlines requires a line range")) | ||||
rev = b'.' | ||||
if b'startrev' in args: | ||||
revs = getset(repo, fullreposet(repo), args[b'startrev']) | ||||
|
r30754 | if len(revs) != 1: | ||
raise error.ParseError( | ||||
|
r32086 | # i18n: "followlines" is a keyword | ||
|
r43347 | _(b"followlines expects exactly one revision") | ||
|
r43346 | ) | ||
|
r30754 | rev = revs.last() | ||
|
r43347 | pat = getstring(args[b'file'], _(b"followlines requires a pattern")) | ||
|
r34855 | # i18n: "followlines" is a keyword | ||
|
r43347 | msg = _(b"followlines expects exactly one file") | ||
|
r34855 | fname = scmutil.parsefollowlinespattern(repo, rev, pat, msg) | ||
|
r41702 | fromline, toline = util.processlinerange( | ||
|
r43346 | *getintrange( | ||
|
r43347 | args[b'lines'][0], | ||
|
r43346 | # i18n: "followlines" is a keyword | ||
|
r43347 | _(b"followlines expects a line number or a range"), | ||
_(b"line range bounds must be integers"), | ||||
|
r43346 | ) | ||
) | ||||
|
r30719 | |||
fctx = repo[rev].filectx(fname) | ||||
|
r31998 | descend = False | ||
|
r43347 | if b'descend' in args: | ||
|
r43346 | descend = getboolean( | ||
|
r43347 | args[b'descend'], | ||
|
r43346 | # i18n: "descend" is a keyword | ||
|
r43347 | _(b"descend argument must be a boolean"), | ||
|
r43346 | ) | ||
|
r31998 | if descend: | ||
|
r31938 | rs = generatorset( | ||
|
r43346 | ( | ||
c.rev() | ||||
for c, _linerange in dagop.blockdescendants( | ||||
fctx, fromline, toline | ||||
) | ||||
), | ||||
iterasc=True, | ||||
) | ||||
|
r31938 | else: | ||
rs = generatorset( | ||||
|
r43346 | ( | ||
c.rev() | ||||
for c, _linerange in dagop.blockancestors( | ||||
fctx, fromline, toline | ||||
) | ||||
), | ||||
iterasc=False, | ||||
) | ||||
|
r31938 | return subset & rs | ||
|
r30719 | |||
|
r43346 | |||
|
r43347 | @predicate(b'all()', safe=True) | ||
|
r13915 | def getall(repo, subset, x): | ||
|
r27584 | """All changesets, the same as ``0:tip``. | ||
|
r13915 | """ | ||
# i18n: "all" is a keyword | ||||
|
r43347 | getargs(x, 0, 0, _(b"all takes no arguments")) | ||
|
r24202 | return subset & spanset(repo) # drop "null" if any | ||
|
r13915 | |||
|
r43346 | |||
|
r43347 | @predicate(b'grep(regex)', weight=10) | ||
|
r13915 | def grep(repo, subset, x): | ||
|
r27584 | """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')`` | ||
|
r14357 | to ensure special escape characters are handled correctly. Unlike | ||
``keyword(string)``, the match is case-sensitive. | ||||
|
r13915 | """ | ||
try: | ||||
# i18n: "grep" is a keyword | ||||
|
r43347 | gr = re.compile(getstring(x, _(b"grep requires a string"))) | ||
|
r25660 | except re.error as e: | ||
|
r36598 | raise error.ParseError( | ||
|
r43347 | _(b'invalid match pattern: %s') % stringutil.forcebytestr(e) | ||
|
r43346 | ) | ||
|
r20453 | |||
def matches(x): | ||||
c = repo[x] | ||||
|
r13915 | for e in c.files() + [c.user(), c.description()]: | ||
if gr.search(e): | ||||
|
r20453 | return True | ||
return False | ||||
|
r43347 | return subset.filter(matches, condrepr=(b'<grep %r>', gr.pattern)) | ||
@predicate(b'_matchfiles', safe=True) | ||||
|
r16161 | def _matchfiles(repo, subset, x): | ||
# _matchfiles takes a revset list of prefixed arguments: | ||||
# | ||||
# [p:foo, i:bar, x:baz] | ||||
# | ||||
# builds a match object from them and filters subset. Allowed | ||||
# prefixes are 'p:' for regular patterns, 'i:' for include | ||||
|
r16181 | # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass | ||
# a revision identifier, or the empty string to reference the | ||||
# working directory, from which the match object is | ||||
|
r16411 | # initialized. Use 'd:' to set the default matching mode, default | ||
# to 'glob'. At most one 'r:' and 'd:' argument can be passed. | ||||
|
r16161 | |||
|
r43347 | l = getargs(x, 1, -1, b"_matchfiles requires at least one argument") | ||
|
r16161 | pats, inc, exc = [], [], [] | ||
|
r16411 | rev, default = None, None | ||
|
r16161 | for arg in l: | ||
|
r43347 | s = getstring(arg, b"_matchfiles requires string arguments") | ||
|
r16161 | prefix, value = s[:2], s[2:] | ||
|
r43347 | if prefix == b'p:': | ||
|
r16161 | pats.append(value) | ||
|
r43347 | elif prefix == b'i:': | ||
|
r16161 | inc.append(value) | ||
|
r43347 | elif prefix == b'x:': | ||
|
r16161 | exc.append(value) | ||
|
r43347 | elif prefix == b'r:': | ||
|
r16181 | if rev is not None: | ||
|
r43346 | raise error.ParseError( | ||
|
r43387 | b'_matchfiles expected at most one revision' | ||
|
r43346 | ) | ||
|
r43347 | if value == b'': # empty means working directory | ||
|
r35835 | rev = node.wdirrev | ||
else: | ||||
|
r23950 | rev = value | ||
|
r43347 | elif prefix == b'd:': | ||
|
r16411 | if default is not None: | ||
|
r43346 | raise error.ParseError( | ||
|
r43387 | b'_matchfiles expected at most one default mode' | ||
|
r43346 | ) | ||
|
r16411 | default = value | ||
|
r16161 | else: | ||
|
r43347 | raise error.ParseError(b'invalid _matchfiles prefix: %s' % prefix) | ||
|
r16411 | if not default: | ||
|
r43347 | default = b'glob' | ||
hasset = any(matchmod.patkind(p) == b'set' for p in pats + inc + exc) | ||||
|
r20458 | |||
|
r35835 | mcache = [None] | ||
|
r23061 | |||
|
r27028 | # This directly read the changelog data as creating changectx for all | ||
# revisions is quite expensive. | ||||
|
r27440 | getfiles = repo.changelog.readfiles | ||
|
r27028 | wdirrev = node.wdirrev | ||
|
r43346 | |||
|
r20458 | def matches(x): | ||
|
r27028 | if x == wdirrev: | ||
files = repo[x].files() | ||||
else: | ||||
|
r27440 | files = getfiles(x) | ||
|
r35835 | |||
if not mcache[0] or (hasset and rev is None): | ||||
r = x if rev is None else rev | ||||
|
r43346 | mcache[0] = matchmod.match( | ||
repo.root, | ||||
repo.getcwd(), | ||||
pats, | ||||
include=inc, | ||||
exclude=exc, | ||||
ctx=repo[r], | ||||
default=default, | ||||
) | ||||
|
r35835 | m = mcache[0] | ||
|
r27028 | for f in files: | ||
|
r16161 | if m(f): | ||
|
r20458 | return True | ||
return False | ||||
|
r43346 | return subset.filter( | ||
matches, | ||||
condrepr=( | ||||
|
r43347 | b'<matchfiles patterns=%r, include=%r ' | ||
b'exclude=%r, default=%r, rev=%r>', | ||||
|
r43346 | pats, | ||
inc, | ||||
exc, | ||||
default, | ||||
rev, | ||||
), | ||||
) | ||||
|
r16161 | |||
|
r43347 | @predicate(b'file(pattern)', safe=True, weight=10) | ||
|
r13915 | def hasfile(repo, subset, x): | ||
|
r27584 | """Changesets affecting files matched by pattern. | ||
|
r17244 | |||
|
r17265 | For a faster but less accurate result, consider using ``filelog()`` | ||
instead. | ||||
|
r20289 | |||
This predicate uses ``glob:`` as the default kind of pattern. | ||||
|
r13915 | """ | ||
# i18n: "file" is a keyword | ||||
|
r43347 | pat = getstring(x, _(b"file requires a pattern")) | ||
return _matchfiles(repo, subset, (b'string', b'p:' + pat)) | ||||
@predicate(b'head()', safe=True) | ||||
|
r13915 | def head(repo, subset, x): | ||
|
r27584 | """Changeset is a named branch head. | ||
|
r13915 | """ | ||
# i18n: "head" is a keyword | ||||
|
r43347 | getargs(x, 0, 0, _(b"head takes no arguments")) | ||
|
r13915 | hs = set() | ||
|
r25620 | cl = repo.changelog | ||
|
r42169 | for ls in repo.branchmap().iterheads(): | ||
|
r25620 | hs.update(cl.rev(h) for h in ls) | ||
|
r29408 | return subset & baseset(hs) | ||
|
r13915 | |||
|
r43346 | |||
|
r43347 | @predicate(b'heads(set)', safe=True, takeorder=True) | ||
|
r38498 | def heads(repo, subset, x, order): | ||
|
r27584 | """Members of set with no children in set. | ||
|
r13915 | """ | ||
|
r38498 | # argument set should never define order | ||
if order == defineorder: | ||||
order = followorder | ||||
|
r41310 | inputset = getset(repo, fullreposet(repo), x, order=order) | ||
|
r41312 | wdirparents = None | ||
if node.wdirrev in inputset: | ||||
# a bit slower, but not common so good enough for now | ||||
wdirparents = [p.rev() for p in repo[None].parents()] | ||||
inputset = set(inputset) | ||||
inputset.discard(node.wdirrev) | ||||
heads = repo.changelog.headrevs(inputset) | ||||
if wdirparents is not None: | ||||
heads.difference_update(wdirparents) | ||||
heads.add(node.wdirrev) | ||||
heads = baseset(heads) | ||||
return subset & heads | ||||
|
r13915 | |||
|
r43346 | |||
|
r43347 | @predicate(b'hidden()', safe=True) | ||
|
r17390 | def hidden(repo, subset, x): | ||
|
r27584 | """Hidden changesets. | ||
|
r17390 | """ | ||
# i18n: "hidden" is a keyword | ||||
|
r43347 | getargs(x, 0, 0, _(b"hidden takes no arguments")) | ||
hiddenrevs = repoview.filterrevs(repo, b'visible') | ||||
|
r20367 | return subset & hiddenrevs | ||
|
r17390 | |||
|
r43346 | |||
|
r43347 | @predicate(b'keyword(string)', safe=True, weight=10) | ||
|
r13915 | def keyword(repo, subset, x): | ||
|
r27584 | """Search commit message, user name, and names of changed files for | ||
|
r14357 | string. The match is case-insensitive. | ||
|
r30772 | |||
For a regular expression or case sensitive search of these fields, use | ||||
``grep(regex)``. | ||||
|
r13915 | """ | ||
# i18n: "keyword" is a keyword | ||||
|
r43347 | kw = encoding.lower(getstring(x, _(b"keyword requires a string"))) | ||
|
r20447 | |||
def matches(r): | ||||
|
r13915 | c = repo[r] | ||
|
r43346 | return any( | ||
kw in encoding.lower(t) | ||||
for t in c.files() + [c.user(), c.description()] | ||||
) | ||||
|
r20447 | |||
|
r43347 | return subset.filter(matches, condrepr=(b'<keyword %r>', kw)) | ||
@predicate(b'limit(set[, n[, offset]])', safe=True, takeorder=True, weight=0) | ||||
|
r32800 | def limit(repo, subset, x, order): | ||
|
r27584 | """First n members of set, defaulting to 1, starting from offset. | ||
|
r13915 | """ | ||
|
r43347 | args = getargsdict(x, b'limit', b'set n offset') | ||
if b'set' not in args: | ||||
|
r26637 | # i18n: "limit" is a keyword | ||
|
r43347 | raise error.ParseError(_(b"limit requires one to three arguments")) | ||
|
r30802 | # i18n: "limit" is a keyword | ||
|
r43347 | lim = getinteger(args.get(b'n'), _(b"limit expects a number"), default=1) | ||
|
r32798 | if lim < 0: | ||
|
r43347 | raise error.ParseError(_(b"negative number to select")) | ||
|
r30802 | # i18n: "limit" is a keyword | ||
|
r43347 | ofs = getinteger( | ||
args.get(b'offset'), _(b"limit expects a number"), default=0 | ||||
) | ||||
|
r30801 | if ofs < 0: | ||
|
r43347 | raise error.ParseError(_(b"negative offset")) | ||
os = getset(repo, fullreposet(repo), args[b'set']) | ||||
|
r32819 | ls = os.slice(ofs, ofs + lim) | ||
|
r32800 | if order == followorder and lim > 1: | ||
return subset & ls | ||||
|
r32799 | return ls & subset | ||
|
r13915 | |||
|
r43346 | |||
|
r43347 | @predicate(b'last(set, [n])', safe=True, takeorder=True) | ||
|
r32800 | def last(repo, subset, x, order): | ||
|
r27584 | """Last n members of set, defaulting to 1. | ||
|
r14061 | """ | ||
# i18n: "last" is a keyword | ||||
|
r43347 | l = getargs(x, 1, 2, _(b"last requires one or two arguments")) | ||
|
r30801 | lim = 1 | ||
if len(l) == 2: | ||||
|
r14061 | # i18n: "last" is a keyword | ||
|
r43347 | lim = getinteger(l[1], _(b"last expects a number")) | ||
|
r32798 | if lim < 0: | ||
|
r43347 | raise error.ParseError(_(b"negative number to select")) | ||
|
r24115 | os = getset(repo, fullreposet(repo), l[0]) | ||
|
r20534 | os.reverse() | ||
|
r32819 | ls = os.slice(0, lim) | ||
|
r32800 | if order == followorder and lim > 1: | ||
return subset & ls | ||||
|
r32799 | ls.reverse() | ||
return ls & subset | ||||
|
r14061 | |||
|
r43346 | |||
|
r43347 | @predicate(b'max(set)', safe=True) | ||
|
r13915 | def maxrev(repo, subset, x): | ||
|
r27584 | """Changeset with highest revision number in set. | ||
|
r13915 | """ | ||
|
r24115 | os = getset(repo, fullreposet(repo), x) | ||
|
r26305 | try: | ||
|
r20754 | m = os.max() | ||
|
r13915 | if m in subset: | ||
|
r43347 | return baseset([m], datarepr=(b'<max %r, %r>', subset, os)) | ||
|
r26305 | except ValueError: | ||
# os.max() throws a ValueError when the collection is empty. | ||||
# Same as python's max(). | ||||
pass | ||||
|
r43347 | return baseset(datarepr=(b'<max %r, %r>', subset, os)) | ||
@predicate(b'merge()', safe=True) | ||||
|
r13915 | def merge(repo, subset, x): | ||
|
r27584 | """Changeset is a merge changeset. | ||
|
r13915 | """ | ||
# i18n: "merge" is a keyword | ||||
|
r43347 | getargs(x, 0, 0, _(b"merge takes no arguments")) | ||
|
r13915 | cl = repo.changelog | ||
|
r42633 | nullrev = node.nullrev | ||
|
r43346 | |||
|
r42634 | def ismerge(r): | ||
try: | ||||
return cl.parentrevs(r)[1] != nullrev | ||||
except error.WdirUnsupported: | ||||
return bool(repo[r].p2()) | ||||
|
r43346 | |||
|
r43347 | return subset.filter(ismerge, condrepr=b'<merge>') | ||
@predicate(b'branchpoint()', safe=True) | ||||
|
r17753 | def branchpoint(repo, subset, x): | ||
|
r27584 | """Changesets with more than one child. | ||
|
r17753 | """ | ||
# i18n: "branchpoint" is a keyword | ||||
|
r43347 | getargs(x, 0, 0, _(b"branchpoint takes no arguments")) | ||
|
r17753 | cl = repo.changelog | ||
if not subset: | ||||
|
r22802 | return baseset() | ||
|
r25549 | # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset | ||
# (and if it is not, it should.) | ||||
|
r17753 | baserev = min(subset) | ||
|
r43346 | parentscount = [0] * (len(repo) - baserev) | ||
|
r17785 | for r in cl.revs(start=baserev + 1): | ||
|
r17753 | for p in cl.parentrevs(r): | ||
if p >= baserev: | ||||
parentscount[p - baserev] += 1 | ||||
|
r43346 | return subset.filter( | ||
|
r43347 | lambda r: parentscount[r - baserev] > 1, condrepr=b'<branchpoint>' | ||
|
r43346 | ) | ||
|
r17753 | |||
|
r43347 | @predicate(b'min(set)', safe=True) | ||
|
r13915 | def minrev(repo, subset, x): | ||
|
r27584 | """Changeset with lowest revision number in set. | ||
|
r13915 | """ | ||
|
r24115 | os = getset(repo, fullreposet(repo), x) | ||
|
r26305 | try: | ||
|
r20754 | m = os.min() | ||
|
r13915 | if m in subset: | ||
|
r43347 | return baseset([m], datarepr=(b'<min %r, %r>', subset, os)) | ||
|
r26305 | except ValueError: | ||
# os.min() throws a ValueError when the collection is empty. | ||||
# Same as python's min(). | ||||
pass | ||||
|
r43347 | return baseset(datarepr=(b'<min %r, %r>', subset, os)) | ||
@predicate(b'modifies(pattern)', safe=True, weight=30) | ||||
|
r13915 | def modifies(repo, subset, x): | ||
|
r27584 | """Changesets modifying files matched by pattern. | ||
|
r20289 | |||
The pattern without explicit kind like ``glob:`` is expected to be | ||||
relative to the current directory and match against a file or a | ||||
directory. | ||||
|
r13915 | """ | ||
# i18n: "modifies" is a keyword | ||||
|
r43347 | pat = getstring(x, _(b"modifies requires a pattern")) | ||
|
r13915 | return checkstatus(repo, subset, pat, 0) | ||
|
r43346 | |||
|
r43347 | @predicate(b'named(namespace)') | ||
|
r23836 | def named(repo, subset, x): | ||
|
r27584 | """The changesets in a given namespace. | ||
|
r23836 | |||
|
r30784 | Pattern matching is supported for `namespace`. See | ||
|
r30799 | :hg:`help revisions.patterns`. | ||
|
r23836 | """ | ||
# i18n: "named" is a keyword | ||||
|
r43347 | args = getargs(x, 1, 1, _(b'named requires a namespace argument')) | ||
|
r23836 | |||
|
r43346 | ns = getstring( | ||
args[0], | ||||
# i18n: "named" is a keyword | ||||
|
r43347 | _(b'the argument to named must be a string'), | ||
|
r43346 | ) | ||
|
r37102 | kind, pattern, matcher = stringutil.stringmatcher(ns) | ||
|
r23836 | namespaces = set() | ||
|
r43347 | if kind == b'literal': | ||
|
r23836 | if pattern not in repo.names: | ||
|
r43347 | raise error.RepoLookupError( | ||
_(b"namespace '%s' does not exist") % ns | ||||
) | ||||
|
r23836 | namespaces.add(repo.names[pattern]) | ||
else: | ||||
|
r43376 | for name, ns in pycompat.iteritems(repo.names): | ||
|
r23836 | if matcher(name): | ||
namespaces.add(ns) | ||||
names = set() | ||||
for ns in namespaces: | ||||
for name in ns.listnames(repo): | ||||
|
r24151 | if name not in ns.deprecated: | ||
names.update(repo[n].rev() for n in ns.nodes(repo, name)) | ||||
|
r23836 | |||
|
r32291 | names -= {node.nullrev} | ||
|
r23836 | return subset & names | ||
|
r43346 | |||
|
r43347 | @predicate(b'id(string)', safe=True) | ||
|
r16417 | def node_(repo, subset, x): | ||
|
r27584 | """Revision non-ambiguously specified by the given hex string prefix. | ||
|
r12821 | """ | ||
|
r12815 | # i18n: "id" is a keyword | ||
|
r43347 | l = getargs(x, 1, 1, _(b"id requires one argument")) | ||
|
r12815 | # i18n: "id" is a keyword | ||
|
r43347 | n = getstring(l[0], _(b"id requires a string")) | ||
|
r12716 | if len(n) == 40: | ||
|
r24904 | try: | ||
rn = repo.changelog.rev(node.bin(n)) | ||||
|
r32659 | except error.WdirUnsupported: | ||
rn = node.wdirrev | ||||
|
r24904 | except (LookupError, TypeError): | ||
rn = None | ||||
|
r12716 | else: | ||
|
r16735 | rn = None | ||
|
r32684 | try: | ||
|
r37884 | pm = scmutil.resolvehexnodeidprefix(repo, n) | ||
|
r32684 | if pm is not None: | ||
|
r32659 | rn = repo.changelog.rev(pm) | ||
|
r37883 | except LookupError: | ||
pass | ||||
|
r32684 | except error.WdirUnsupported: | ||
rn = node.wdirrev | ||||
|
r16735 | |||
|
r23005 | if rn is None: | ||
return baseset() | ||||
result = baseset([rn]) | ||||
return result & subset | ||||
|
r12716 | |||
|
r43346 | |||
|
r43347 | @predicate(b'none()', safe=True) | ||
|
r38294 | def none(repo, subset, x): | ||
"""No changesets. | ||||
""" | ||||
# i18n: "none" is a keyword | ||||
|
r43347 | getargs(x, 0, 0, _(b"none takes no arguments")) | ||
|
r38294 | return baseset() | ||
|
r43346 | |||
|
r43347 | @predicate(b'obsolete()', safe=True) | ||
|
r17170 | def obsolete(repo, subset, x): | ||
|
r45188 | """Mutable changeset with a newer version. (EXPERIMENTAL)""" | ||
|
r17259 | # i18n: "obsolete" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"obsolete takes no arguments")) | ||
obsoletes = obsmod.getrevs(repo, b'obsolete') | ||||
|
r20367 | return subset & obsoletes | ||
|
r17170 | |||
|
r43346 | |||
|
r43347 | @predicate(b'only(set, [set])', safe=True) | ||
|
r23466 | def only(repo, subset, x): | ||
|
r27584 | """Changesets that are ancestors of the first set that are not ancestors | ||
|
r23466 | of any other head in the repo. If a second set is specified, the result | ||
is ancestors of the first set that are not ancestors of the second set | ||||
(i.e. ::<set1> - ::<set2>). | ||||
""" | ||||
cl = repo.changelog | ||||
# i18n: "only" is a keyword | ||||
|
r43347 | args = getargs(x, 1, 2, _(b'only takes one or two arguments')) | ||
|
r24115 | include = getset(repo, fullreposet(repo), args[0]) | ||
|
r23466 | if len(args) == 1: | ||
if not include: | ||||
return baseset() | ||||
|
r32903 | descendants = set(dagop.revdescendants(repo, include, False)) | ||
|
r43346 | exclude = [ | ||
rev | ||||
for rev in cl.headrevs() | ||||
if not rev in descendants and not rev in include | ||||
] | ||||
|
r23466 | else: | ||
|
r24115 | exclude = getset(repo, fullreposet(repo), args[1]) | ||
|
r23466 | |||
results = set(cl.findmissingrevs(common=exclude, heads=include)) | ||||
|
r25554 | # XXX we should turn this into a baseset instead of a set, smartset may do | ||
|
r30332 | # some optimizations from the fact this is a baseset. | ||
|
r23466 | return subset & results | ||
|
r43346 | |||
|
r43347 | @predicate(b'origin([set])', safe=True) | ||
|
r17185 | def origin(repo, subset, x): | ||
|
r27584 | """ | ||
|
r17185 | Changesets that were specified as a source for the grafts, transplants or | ||
rebases that created the given revisions. Omitting the optional set is the | ||||
same as passing all(). If a changeset created by these operations is itself | ||||
specified as a source for one of these operations, only the source changeset | ||||
for the first operation is selected. | ||||
""" | ||||
if x is not None: | ||||
|
r24115 | dests = getset(repo, fullreposet(repo), x) | ||
|
r17185 | else: | ||
|
r24201 | dests = fullreposet(repo) | ||
|
r17185 | |||
def _firstsrc(rev): | ||||
src = _getrevsource(repo, rev) | ||||
if src is None: | ||||
return None | ||||
while True: | ||||
prev = _getrevsource(repo, src) | ||||
if prev is None: | ||||
return src | ||||
src = prev | ||||
|
r32291 | o = {_firstsrc(r) for r in dests} | ||
o -= {None} | ||||
|
r25554 | # XXX we should turn this into a baseset instead of a set, smartset may do | ||
|
r30332 | # some optimizations from the fact this is a baseset. | ||
|
r22536 | return subset & o | ||
|
r17185 | |||
|
r43346 | |||
|
r43347 | @predicate(b'outgoing([path])', safe=False, weight=10) | ||
|
r13915 | def outgoing(repo, subset, x): | ||
|
r27584 | """Changesets not found in the specified destination repository, or the | ||
|
r13915 | default push location. | ||
|
r12821 | """ | ||
|
r24722 | # Avoid cycles. | ||
|
r25971 | from . import ( | ||
discovery, | ||||
hg, | ||||
) | ||||
|
r43346 | |||
|
r13915 | # i18n: "outgoing" is a keyword | ||
|
r43347 | l = getargs(x, 0, 1, _(b"outgoing takes one or no arguments")) | ||
|
r13915 | # i18n: "outgoing" is a keyword | ||
|
r43347 | dest = ( | ||
l and getstring(l[0], _(b"outgoing requires a repository path")) or b'' | ||||
) | ||||
|
r35454 | if not dest: | ||
# ui.paths.getpath() explicitly tests for None, not just a boolean | ||||
dest = None | ||||
|
r43347 | path = repo.ui.paths.getpath(dest, default=(b'default-push', b'default')) | ||
|
r35454 | if not path: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'default repository not configured!'), | ||
hint=_(b"see 'hg help config.paths'"), | ||||
|
r43346 | ) | ||
|
r35454 | dest = path.pushloc or path.loc | ||
branches = path.branch, [] | ||||
|
r13915 | revs, checkout = hg.addbranchrevs(repo, repo, branches, []) | ||
if revs: | ||||
revs = [repo.lookup(rev) for rev in revs] | ||||
|
r14556 | other = hg.peer(repo, {}, dest) | ||
|
r13915 | repo.ui.pushbuffer() | ||
|
r15837 | outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs) | ||
|
r13915 | repo.ui.popbuffer() | ||
cl = repo.changelog | ||||
|
r32291 | o = {cl.rev(r) for r in outgoing.missing} | ||
|
r22529 | return subset & o | ||
|
r12716 | |||
|
r43346 | |||
|
r43347 | @predicate(b'p1([set])', safe=True) | ||
|
r11275 | def p1(repo, subset, x): | ||
|
r27584 | """First parent of changesets in set, or the working directory. | ||
|
r12821 | """ | ||
|
r12928 | if x is None: | ||
|
r13878 | p = repo[x].p1().rev() | ||
|
r22538 | if p >= 0: | ||
return subset & baseset([p]) | ||||
|
r22802 | return baseset() | ||
|
r12928 | |||
|
r11275 | ps = set() | ||
cl = repo.changelog | ||||
|
r24115 | for r in getset(repo, fullreposet(repo), x): | ||
|
r32403 | try: | ||
ps.add(cl.parentrevs(r)[0]) | ||||
except error.WdirUnsupported: | ||||
|
r41442 | ps.add(repo[r].p1().rev()) | ||
|
r32291 | ps -= {node.nullrev} | ||
|
r25554 | # XXX we should turn this into a baseset instead of a set, smartset may do | ||
|
r30332 | # some optimizations from the fact this is a baseset. | ||
|
r20367 | return subset & ps | ||
|
r11275 | |||
|
r43346 | |||
|
r43347 | @predicate(b'p2([set])', safe=True) | ||
|
r11275 | def p2(repo, subset, x): | ||
|
r27584 | """Second parent of changesets in set, or the working directory. | ||
|
r12821 | """ | ||
|
r12928 | if x is None: | ||
ps = repo[x].parents() | ||||
try: | ||||
|
r12935 | p = ps[1].rev() | ||
|
r22539 | if p >= 0: | ||
return subset & baseset([p]) | ||||
|
r22802 | return baseset() | ||
|
r12928 | except IndexError: | ||
|
r22802 | return baseset() | ||
|
r12928 | |||
|
r11275 | ps = set() | ||
cl = repo.changelog | ||||
|
r24115 | for r in getset(repo, fullreposet(repo), x): | ||
|
r32440 | try: | ||
ps.add(cl.parentrevs(r)[1]) | ||||
except error.WdirUnsupported: | ||||
parents = repo[r].parents() | ||||
if len(parents) == 2: | ||||
ps.add(parents[1]) | ||||
|
r32291 | ps -= {node.nullrev} | ||
|
r25554 | # XXX we should turn this into a baseset instead of a set, smartset may do | ||
|
r30332 | # some optimizations from the fact this is a baseset. | ||
|
r20367 | return subset & ps | ||
|
r11275 | |||
|
r43346 | |||
|
r29932 | def parentpost(repo, subset, x, order): | ||
|
r29931 | return p1(repo, subset, x) | ||
|
r43346 | |||
|
r43347 | @predicate(b'parents([set])', safe=True) | ||
|
r11275 | def parents(repo, subset, x): | ||
|
r27584 | """ | ||
|
r12929 | The set of all parents for all changesets in set, or the working directory. | ||
|
r12821 | """ | ||
|
r12929 | if x is None: | ||
|
r44937 | ps = {p.rev() for p in repo[x].parents()} | ||
|
r22496 | else: | ||
ps = set() | ||||
cl = repo.changelog | ||||
|
r25716 | up = ps.update | ||
parentrevs = cl.parentrevs | ||||
|
r24115 | for r in getset(repo, fullreposet(repo), x): | ||
|
r32439 | try: | ||
up(parentrevs(r)) | ||||
except error.WdirUnsupported: | ||||
|
r25716 | up(p.rev() for p in repo[r].parents()) | ||
|
r32291 | ps -= {node.nullrev} | ||
|
r22712 | return subset & ps | ||
|
r11275 | |||
|
r43346 | |||
|
r31017 | def _phase(repo, subset, *targets): | ||
"""helper to select all rev in <targets> phases""" | ||||
|
r35331 | return repo._phasecache.getrevset(repo, targets, subset) | ||
|
r25621 | |||
|
r43346 | |||
|
r43347 | @predicate(b'_phase(idx)', safe=True) | ||
|
r39310 | def phase(repo, subset, x): | ||
|
r43347 | l = getargs(x, 1, 1, b"_phase requires one argument") | ||
target = getinteger(l[0], b"_phase expects a number") | ||||
|
r39310 | return _phase(repo, subset, target) | ||
|
r43346 | |||
|
r43347 | @predicate(b'draft()', safe=True) | ||
|
r25621 | def draft(repo, subset, x): | ||
|
r27584 | """Changeset in draft phase.""" | ||
|
r25621 | # i18n: "draft" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"draft takes no arguments")) | ||
|
r25621 | target = phases.draft | ||
return _phase(repo, subset, target) | ||||
|
r43346 | |||
|
r43347 | @predicate(b'secret()', safe=True) | ||
|
r25621 | def secret(repo, subset, x): | ||
|
r27584 | """Changeset in secret phase.""" | ||
|
r25621 | # i18n: "secret" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"secret takes no arguments")) | ||
|
r25621 | target = phases.secret | ||
return _phase(repo, subset, target) | ||||
|
r43346 | |||
|
r43347 | @predicate(b'stack([revs])', safe=True) | ||
|
r37407 | def stack(repo, subset, x): | ||
"""Experimental revset for the stack of changesets or working directory | ||||
parent. (EXPERIMENTAL) | ||||
""" | ||||
|
r37019 | if x is None: | ||
r42927 | stacks = stackmod.getstack(repo) | |||
|
r37019 | else: | ||
stacks = smartset.baseset([]) | ||||
for revision in getset(repo, fullreposet(repo), x): | ||||
|
r37407 | currentstack = stackmod.getstack(repo, revision) | ||
|
r37019 | stacks = stacks + currentstack | ||
|
r37407 | return subset & stacks | ||
|
r37019 | |||
|
r43346 | |||
|
r29932 | def parentspec(repo, subset, x, n, order): | ||
|
r14070 | """``set^0`` | ||
The set. | ||||
``set^1`` (or ``set^``), ``set^2`` | ||||
First or second parent, respectively, of all changesets in set. | ||||
|
r12821 | """ | ||
|
r12320 | try: | ||
|
r14070 | n = int(n[1]) | ||
|
r14072 | if n not in (0, 1, 2): | ||
|
r14070 | raise ValueError | ||
|
r14851 | except (TypeError, ValueError): | ||
|
r43347 | raise error.ParseError(_(b"^ expects a number 0, 1, or 2")) | ||
|
r14070 | ps = set() | ||
|
r11275 | cl = repo.changelog | ||
|
r23165 | for r in getset(repo, fullreposet(repo), x): | ||
|
r14070 | if n == 0: | ||
ps.add(r) | ||||
elif n == 1: | ||||
|
r32436 | try: | ||
ps.add(cl.parentrevs(r)[0]) | ||||
except error.WdirUnsupported: | ||||
|
r41442 | ps.add(repo[r].p1().rev()) | ||
|
r32438 | else: | ||
|
r32436 | try: | ||
parents = cl.parentrevs(r) | ||||
if parents[1] != node.nullrev: | ||||
ps.add(parents[1]) | ||||
except error.WdirUnsupported: | ||||
parents = repo[r].parents() | ||||
if len(parents) == 2: | ||||
ps.add(parents[1].rev()) | ||||
|
r20367 | return subset & ps | ||
|
r11275 | |||
|
r43346 | |||
|
r43347 | @predicate(b'present(set)', safe=True, takeorder=True) | ||
|
r34013 | def present(repo, subset, x, order): | ||
|
r27584 | """An empty set, if any revision in set isn't found; otherwise, | ||
|
r12821 | all revisions in set. | ||
|
r16748 | |||
If any of specified revisions is not present in the local repository, | ||||
the query is normally aborted. But this predicate allows the query | ||||
to continue even in such cases. | ||||
|
r12821 | """ | ||
|
r11944 | try: | ||
|
r34013 | return getset(repo, subset, x, order) | ||
|
r11944 | except error.RepoLookupError: | ||
|
r22802 | return baseset() | ||
|
r11944 | |||
|
r43346 | |||
|
r25224 | # for internal use | ||
|
r43347 | @predicate(b'_notpublic', safe=True) | ||
|
r25191 | def _notpublic(repo, subset, x): | ||
|
r43347 | getargs(x, 0, 0, b"_notpublic takes no arguments") | ||
|
r31017 | return _phase(repo, subset, phases.draft, phases.secret) | ||
|
r25191 | |||
|
r43346 | |||
|
r34067 | # for internal use | ||
|
r43347 | @predicate(b'_phaseandancestors(phasename, set)', safe=True) | ||
|
r34067 | def _phaseandancestors(repo, subset, x): | ||
# equivalent to (phasename() & ancestors(set)) but more efficient | ||||
# phasename could be one of 'draft', 'secret', or '_notpublic' | ||||
|
r43347 | args = getargs(x, 2, 2, b"_phaseandancestors requires two arguments") | ||
|
r34067 | phasename = getsymbol(args[0]) | ||
s = getset(repo, fullreposet(repo), args[1]) | ||||
draft = phases.draft | ||||
secret = phases.secret | ||||
phasenamemap = { | ||||
|
r43347 | b'_notpublic': draft, | ||
b'draft': draft, # follow secret's ancestors | ||||
b'secret': secret, | ||||
|
r34067 | } | ||
if phasename not in phasenamemap: | ||||
|
r43347 | raise error.ParseError(b'%r is not a valid phasename' % phasename) | ||
|
r34067 | |||
minimalphase = phasenamemap[phasename] | ||||
getphase = repo._phasecache.phase | ||||
def cutfunc(rev): | ||||
return getphase(repo, rev) < minimalphase | ||||
revs = dagop.revancestors(repo, s, cutfunc=cutfunc) | ||||
|
r43347 | if phasename == b'draft': # need to remove secret changesets | ||
|
r34067 | revs = revs.filter(lambda r: getphase(repo, r) == draft) | ||
return subset & revs | ||||
|
r43346 | |||
|
r43347 | @predicate(b'public()', safe=True) | ||
|
r15819 | def public(repo, subset, x): | ||
|
r27584 | """Changeset in public phase.""" | ||
|
r17259 | # i18n: "public" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"public takes no arguments")) | ||
|
r35331 | return _phase(repo, subset, phases.public) | ||
|
r15819 | |||
|
r43346 | |||
|
r43347 | @predicate(b'remote([id [,path]])', safe=False) | ||
|
r15936 | def remote(repo, subset, x): | ||
|
r27584 | """Local revision that corresponds to the given identifier in a | ||
|
r15936 | remote repository, if present. Here, the '.' identifier is a | ||
synonym for the current local branch. | ||||
""" | ||||
|
r43346 | from . import hg # avoid start-up nasties | ||
|
r15936 | # i18n: "remote" is a keyword | ||
|
r43347 | l = getargs(x, 0, 2, _(b"remote takes zero, one, or two arguments")) | ||
q = b'.' | ||||
|
r15936 | if len(l) > 0: | ||
|
r43346 | # i18n: "remote" is a keyword | ||
|
r43347 | q = getstring(l[0], _(b"remote requires a string id")) | ||
if q == b'.': | ||||
q = repo[b'.'].branch() | ||||
dest = b'' | ||||
|
r15936 | if len(l) > 1: | ||
# i18n: "remote" is a keyword | ||||
|
r43347 | dest = getstring(l[1], _(b"remote requires a repository path")) | ||
dest = repo.ui.expandpath(dest or b'default') | ||||
|
r15936 | dest, branches = hg.parseurl(dest) | ||
|
r44449 | |||
|
r15936 | other = hg.peer(repo, {}, dest) | ||
n = other.lookup(q) | ||||
if n in repo: | ||||
r = repo[n].rev() | ||||
|
r16006 | if r in subset: | ||
|
r20364 | return baseset([r]) | ||
|
r22802 | return baseset() | ||
|
r15936 | |||
|
r43346 | |||
|
r43347 | @predicate(b'removes(pattern)', safe=True, weight=30) | ||
|
r11275 | def removes(repo, subset, x): | ||
|
r27584 | """Changesets which remove files matching pattern. | ||
|
r20289 | |||
The pattern without explicit kind like ``glob:`` is expected to be | ||||
relative to the current directory and match against a file or a | ||||
directory. | ||||
|
r12821 | """ | ||
|
r12815 | # i18n: "removes" is a keyword | ||
|
r43347 | pat = getstring(x, _(b"removes requires a pattern")) | ||
|
r11275 | return checkstatus(repo, subset, pat, 2) | ||
|
r43346 | |||
|
r43347 | @predicate(b'rev(number)', safe=True) | ||
|
r13915 | def rev(repo, subset, x): | ||
|
r45075 | """Revision with the given numeric identifier.""" | ||
|
r13915 | try: | ||
|
r45075 | return _rev(repo, subset, x) | ||
except error.RepoLookupError: | ||||
|
r23062 | return baseset() | ||
|
r11275 | |||
|
r43346 | |||
|
r43347 | @predicate(b'_rev(number)', safe=True) | ||
|
r41333 | def _rev(repo, subset, x): | ||
# internal version of "rev(x)" that raise error if "x" is invalid | ||||
# i18n: "rev" is a keyword | ||||
|
r43347 | l = getargs(x, 1, 1, _(b"rev requires one argument")) | ||
|
r41333 | try: | ||
# i18n: "rev" is a keyword | ||||
|
r43347 | l = int(getstring(l[0], _(b"rev requires a number"))) | ||
|
r41333 | except (TypeError, ValueError): | ||
# i18n: "rev" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"rev expects a number")) | ||
|
r45073 | if l not in _virtualrevs: | ||
|
r45074 | try: | ||
repo.changelog.node(l) # check that the rev exists | ||||
except IndexError: | ||||
raise error.RepoLookupError(_(b"unknown revision '%d'") % l) | ||||
|
r41333 | return subset & baseset([l]) | ||
|
r43346 | |||
|
r43347 | @predicate(b'revset(set)', safe=True, takeorder=True) | ||
|
r40346 | def revsetpredicate(repo, subset, x, order): | ||
"""Strictly interpret the content as a revset. | ||||
The content of this special predicate will be strictly interpreted as a | ||||
revset. For example, ``revset(id(0))`` will be interpreted as "id(0)" | ||||
without possible ambiguity with a "id(0)" bookmark or tag. | ||||
""" | ||||
return getset(repo, subset, x, order) | ||||
|
r43346 | |||
|
r43347 | @predicate(b'matching(revision [, field])', safe=True) | ||
|
r16402 | def matching(repo, subset, x): | ||
|
r27584 | """Changesets in which a given set of fields match the set of fields in the | ||
|
r16402 | selected revision or set. | ||
|
r16528 | |||
|
r16402 | To match more than one field pass the list of fields to match separated | ||
|
r16528 | by spaces (e.g. ``author description``). | ||
Valid fields are most regular revision fields and some special fields. | ||||
Regular revision fields are ``description``, ``author``, ``branch``, | ||||
|
r17102 | ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user`` | ||
and ``diff``. | ||||
Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the | ||||
contents of the revision. Two revisions matching their ``diff`` will | ||||
also match their ``files``. | ||||
|
r16528 | |||
Special fields are ``summary`` and ``metadata``: | ||||
``summary`` matches the first line of the description. | ||||
|
r16639 | ``metadata`` is equivalent to matching ``description user date`` | ||
|
r16528 | (i.e. it matches the main metadata fields). | ||
``metadata`` is the default field which is used when no fields are | ||||
specified. You can match more than one field at a time. | ||||
|
r16402 | """ | ||
|
r17259 | # i18n: "matching" is a keyword | ||
|
r43347 | l = getargs(x, 1, 2, _(b"matching takes 1 or 2 arguments")) | ||
|
r16402 | |||
|
r23166 | revs = getset(repo, fullreposet(repo), l[0]) | ||
|
r16402 | |||
|
r43347 | fieldlist = [b'metadata'] | ||
|
r16402 | if len(l) > 1: | ||
|
r43346 | fieldlist = getstring( | ||
l[1], | ||||
# i18n: "matching" is a keyword | ||||
|
r43387 | _(b"matching requires a string as its second argument"), | ||
|
r43346 | ).split() | ||
|
r16402 | |||
|
r17102 | # Make sure that there are no repeated fields, | ||
# expand the 'special' 'metadata' field type | ||||
# and check the 'files' whenever we check the 'diff' | ||||
|
r16402 | fields = [] | ||
for field in fieldlist: | ||||
|
r43347 | if field == b'metadata': | ||
fields += [b'user', b'description', b'date'] | ||||
elif field == b'diff': | ||||
|
r17102 | # a revision matching the diff must also match the files | ||
# since matching the diff is very costly, make sure to | ||||
# also match the files first | ||||
|
r43347 | fields += [b'files', b'diff'] | ||
|
r16402 | else: | ||
|
r43347 | if field == b'author': | ||
field = b'user' | ||||
|
r16402 | fields.append(field) | ||
fields = set(fields) | ||||
|
r43347 | if b'summary' in fields and b'description' in fields: | ||
|
r16444 | # If a revision matches its description it also matches its summary | ||
|
r43347 | fields.discard(b'summary') | ||
|
r16402 | |||
# We may want to match more than one field | ||||
|
r16446 | # Not all fields take the same amount of time to be matched | ||
# Sort the selected fields in order of increasing matching cost | ||||
|
r43346 | fieldorder = [ | ||
|
r43347 | b'phase', | ||
b'parents', | ||||
b'user', | ||||
b'date', | ||||
b'branch', | ||||
b'summary', | ||||
b'files', | ||||
b'description', | ||||
b'substate', | ||||
b'diff', | ||||
|
r43346 | ] | ||
|
r16446 | def fieldkeyfunc(f): | ||
try: | ||||
return fieldorder.index(f) | ||||
except ValueError: | ||||
# assume an unknown field is very costly | ||||
return len(fieldorder) | ||||
|
r43346 | |||
|
r16446 | fields = list(fields) | ||
fields.sort(key=fieldkeyfunc) | ||||
|
r16402 | # Each field will be matched with its own "getfield" function | ||
# which will be added to the getfieldfuncs array of functions | ||||
getfieldfuncs = [] | ||||
_funcs = { | ||||
|
r43347 | b'user': lambda r: repo[r].user(), | ||
b'branch': lambda r: repo[r].branch(), | ||||
b'date': lambda r: repo[r].date(), | ||||
b'description': lambda r: repo[r].description(), | ||||
b'files': lambda r: repo[r].files(), | ||||
b'parents': lambda r: repo[r].parents(), | ||||
b'phase': lambda r: repo[r].phase(), | ||||
b'substate': lambda r: repo[r].substate, | ||||
b'summary': lambda r: repo[r].description().splitlines()[0], | ||||
b'diff': lambda r: list( | ||||
repo[r].diff(opts=diffutil.diffallopts(repo.ui, {b'git': True})) | ||||
|
r43346 | ), | ||
|
r16402 | } | ||
for info in fields: | ||||
getfield = _funcs.get(info, None) | ||||
if getfield is None: | ||||
raise error.ParseError( | ||||
|
r17259 | # i18n: "matching" is a keyword | ||
|
r43347 | _(b"unexpected field name passed to matching: %s") | ||
|
r43346 | % info | ||
) | ||||
|
r16402 | getfieldfuncs.append(getfield) | ||
# convert the getfield array of functions into a "getinfo" function | ||||
# which returns an array of field values (or a single value if there | ||||
# is only one field to match) | ||||
|
r16445 | getinfo = lambda r: [f(r) for f in getfieldfuncs] | ||
|
r16402 | |||
|
r20459 | def matches(x): | ||
for rev in revs: | ||||
target = getinfo(rev) | ||||
|
r16445 | match = True | ||
for n, f in enumerate(getfieldfuncs): | ||||
|
r20459 | if target[n] != f(x): | ||
|
r16445 | match = False | ||
if match: | ||||
|
r20459 | return True | ||
return False | ||||
|
r43347 | return subset.filter(matches, condrepr=(b'<matching%r %r>', fields, revs)) | ||
@predicate(b'reverse(set)', safe=True, takeorder=True, weight=0) | ||||
|
r29945 | def reverse(repo, subset, x, order): | ||
|
r27584 | """Reverse order of set. | ||
|
r12821 | """ | ||
|
r34013 | l = getset(repo, subset, x, order) | ||
|
r29945 | if order == defineorder: | ||
l.reverse() | ||||
|
r11275 | return l | ||
|
r43346 | |||
|
r43347 | @predicate(b'roots(set)', safe=True) | ||
|
r13915 | def roots(repo, subset, x): | ||
|
r27584 | """Changesets in set with no parent changeset in set. | ||
|
r12821 | """ | ||
|
r24115 | s = getset(repo, fullreposet(repo), x) | ||
|
r25647 | parents = repo.changelog.parentrevs | ||
|
r43346 | |||
|
r25647 | def filter(r): | ||
for p in parents(r): | ||||
if 0 <= p and p in s: | ||||
return False | ||||
return True | ||||
|
r43346 | |||
|
r43347 | return subset & s.filter(filter, condrepr=b'<roots>') | ||
|
r11944 | |||
|
r43346 | |||
|
r29265 | _sortkeyfuncs = { | ||
|
r43347 | b'rev': lambda c: c.rev(), | ||
b'branch': lambda c: c.branch(), | ||||
b'desc': lambda c: c.description(), | ||||
b'user': lambda c: c.user(), | ||||
b'author': lambda c: c.user(), | ||||
b'date': lambda c: c.date()[0], | ||||
|
r29265 | } | ||
|
r43346 | |||
|
r29365 | def _getsortargs(x): | ||
"""Parse sort options into (set, [(key, reverse)], opts)""" | ||||
|
r43347 | args = getargsdict(x, b'sort', b'set keys topo.firstbranch') | ||
if b'set' not in args: | ||||
|
r29238 | # i18n: "sort" is a keyword | ||
|
r43347 | raise error.ParseError(_(b'sort requires one or two arguments')) | ||
keys = b"rev" | ||||
if b'keys' in args: | ||||
|
r17259 | # i18n: "sort" is a keyword | ||
|
r43347 | keys = getstring(args[b'keys'], _(b"sort spec must be a string")) | ||
|
r29238 | |||
|
r29363 | keyflags = [] | ||
for k in keys.split(): | ||||
fk = k | ||||
|
r43347 | reverse = k.startswith(b'-') | ||
|
r29363 | if reverse: | ||
k = k[1:] | ||||
|
r43347 | if k not in _sortkeyfuncs and k != b'topo': | ||
|
r36598 | raise error.ParseError( | ||
|
r43347 | _(b"unknown sort key %r") % pycompat.bytestr(fk) | ||
|
r43346 | ) | ||
|
r29363 | keyflags.append((k, reverse)) | ||
|
r43347 | if len(keyflags) > 1 and any(k == b'topo' for k, reverse in keyflags): | ||
|
r29348 | # i18n: "topo" is a keyword | ||
|
r43346 | raise error.ParseError( | ||
|
r43387 | _(b'topo sort order cannot be combined with other sort keys') | ||
|
r43346 | ) | ||
|
r29348 | |||
|
r29364 | opts = {} | ||
|
r43347 | if b'topo.firstbranch' in args: | ||
if any(k == b'topo' for k, reverse in keyflags): | ||||
opts[b'topo.firstbranch'] = args[b'topo.firstbranch'] | ||||
|
r29348 | else: | ||
# i18n: "topo" and "topo.firstbranch" are keywords | ||||
|
r43346 | raise error.ParseError( | ||
_( | ||||
|
r43347 | b'topo.firstbranch can only be used ' | ||
b'when using the topo sort key' | ||||
|
r43346 | ) | ||
) | ||||
|
r29348 | |||
|
r43347 | return args[b'set'], keyflags, opts | ||
|
r29365 | |||
|
r43346 | |||
@predicate( | ||||
|
r43347 | b'sort(set[, [-]key... [, ...]])', safe=True, takeorder=True, weight=10 | ||
|
r43346 | ) | ||
|
r29946 | def sort(repo, subset, x, order): | ||
|
r29365 | """Sort set by keys. The default sort order is ascending, specify a key | ||
as ``-key`` to sort in descending order. | ||||
The keys can be: | ||||
- ``rev`` for the revision number, | ||||
- ``branch`` for the branch name, | ||||
- ``desc`` for the commit message (description), | ||||
- ``user`` for user name (``author`` can be used as an alias), | ||||
- ``date`` for the commit date | ||||
- ``topo`` for a reverse topographical sort | ||||
The ``topo`` sort order cannot be combined with other sort keys. This sort | ||||
takes one optional argument, ``topo.firstbranch``, which takes a revset that | ||||
specifies what topographical branches to prioritize in the sort. | ||||
""" | ||||
s, keyflags, opts = _getsortargs(x) | ||||
|
r34013 | revs = getset(repo, subset, s, order) | ||
|
r29364 | |||
|
r29946 | if not keyflags or order != defineorder: | ||
|
r20719 | return revs | ||
|
r43347 | if len(keyflags) == 1 and keyflags[0][0] == b"rev": | ||
|
r29363 | revs.sort(reverse=keyflags[0][1]) | ||
|
r20719 | return revs | ||
|
r43347 | elif keyflags[0][0] == b"topo": | ||
|
r29364 | firstbranch = () | ||
|
r43347 | if b'topo.firstbranch' in opts: | ||
firstbranch = getset(repo, subset, opts[b'topo.firstbranch']) | ||||
|
r43346 | revs = baseset( | ||
dagop.toposort(revs, repo.changelog.parentrevs, firstbranch), | ||||
istopo=True, | ||||
) | ||||
|
r29363 | if keyflags[0][1]: | ||
|
r29348 | revs.reverse() | ||
return revs | ||||
|
r29001 | # sort() is guaranteed to be stable | ||
ctxs = [repo[r] for r in revs] | ||||
|
r29363 | for k, reverse in reversed(keyflags): | ||
ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse) | ||||
|
r29001 | return baseset([c.rev() for c in ctxs]) | ||
|
r11275 | |||
|
r43346 | |||
|
r43347 | @predicate(b'subrepo([pattern])') | ||
|
r24446 | def subrepo(repo, subset, x): | ||
|
r27584 | """Changesets that add, modify or remove the given subrepo. If no subrepo | ||
|
r24446 | pattern is named, any subrepo changes are returned. | ||
""" | ||||
# i18n: "subrepo" is a keyword | ||||
|
r43347 | args = getargs(x, 0, 1, _(b'subrepo takes at most one argument')) | ||
|
r28272 | pat = None | ||
|
r24446 | if len(args) != 0: | ||
|
r43347 | pat = getstring(args[0], _(b"subrepo requires a pattern")) | ||
m = matchmod.exact([b'.hgsubstate']) | ||||
|
r24446 | |||
def submatches(names): | ||||
|
r37102 | k, p, m = stringutil.stringmatcher(pat) | ||
|
r24446 | for name in names: | ||
if m(name): | ||||
yield name | ||||
def matches(x): | ||||
c = repo[x] | ||||
s = repo.status(c.p1().node(), c.node(), match=m) | ||||
|
r28272 | if pat is None: | ||
|
r24446 | return s.added or s.modified or s.removed | ||
if s.added: | ||||
|
r25149 | return any(submatches(c.substate.keys())) | ||
|
r24446 | |||
if s.modified: | ||||
subs = set(c.p1().substate.keys()) | ||||
subs.update(c.substate.keys()) | ||||
for path in submatches(subs): | ||||
if c.p1().substate.get(path) != c.substate.get(path): | ||||
return True | ||||
if s.removed: | ||||
|
r25149 | return any(submatches(c.p1().substate.keys())) | ||
|
r24446 | |||
return False | ||||
|
r43347 | return subset.filter(matches, condrepr=(b'<subrepo %r>', pat)) | ||
|
r24446 | |||
|
r43346 | |||
|
r33377 | def _mapbynodefunc(repo, s, f): | ||
"""(repo, smartset, [node] -> [node]) -> smartset | ||||
Helper method to map a smartset to another smartset given a function only | ||||
talking about nodes. Handles converting between rev numbers and nodes, and | ||||
filtering. | ||||
""" | ||||
cl = repo.unfiltered().changelog | ||||
r43961 | torev = cl.index.get_rev | |||
|
r33377 | tonode = cl.node | ||
|
r44937 | result = {torev(n) for n in f(tonode(r) for r in s)} | ||
r43961 | result.discard(None) | |||
|
r33377 | return smartset.baseset(result - repo.changelog.filteredrevs) | ||
|
r43346 | |||
|
r43347 | @predicate(b'successors(set)', safe=True) | ||
|
r33377 | def successors(repo, subset, x): | ||
|
r45189 | """All successors for set, including the given set themselves. | ||
(EXPERIMENTAL)""" | ||||
|
r33377 | s = getset(repo, fullreposet(repo), x) | ||
f = lambda nodes: obsutil.allsuccessors(repo.obsstore, nodes) | ||||
d = _mapbynodefunc(repo, s, f) | ||||
return subset & d | ||||
|
r43346 | |||
|
r30782 | def _substringmatcher(pattern, casesensitive=True): | ||
|
r37102 | kind, pattern, matcher = stringutil.stringmatcher( | ||
|
r43346 | pattern, casesensitive=casesensitive | ||
) | ||||
|
r43347 | if kind == b'literal': | ||
|
r30782 | if not casesensitive: | ||
pattern = encoding.lower(pattern) | ||||
matcher = lambda s: pattern in encoding.lower(s) | ||||
else: | ||||
matcher = lambda s: pattern in s | ||||
|
r16823 | return kind, pattern, matcher | ||
|
r16819 | |||
|
r43346 | |||
|
r43347 | @predicate(b'tag([name])', safe=True) | ||
|
r12715 | def tag(repo, subset, x): | ||
|
r27584 | """The specified tag by name, or all tagged revisions if no name is given. | ||
|
r20824 | |||
|
r30784 | Pattern matching is supported for `name`. See | ||
|
r30799 | :hg:`help revisions.patterns`. | ||
|
r12821 | """ | ||
|
r12815 | # i18n: "tag" is a keyword | ||
|
r43347 | args = getargs(x, 0, 1, _(b"tag takes one or no arguments")) | ||
|
r11280 | cl = repo.changelog | ||
|
r12715 | if args: | ||
|
r43346 | pattern = getstring( | ||
args[0], | ||||
# i18n: "tag" is a keyword | ||||
|
r43347 | _(b'the argument to tag must be a string'), | ||
|
r43346 | ) | ||
|
r37102 | kind, pattern, matcher = stringutil.stringmatcher(pattern) | ||
|
r43347 | if kind == b'literal': | ||
|
r16825 | # avoid resolving all tags | ||
tn = repo._tagscache.tags.get(pattern, None) | ||||
if tn is None: | ||||
|
r43346 | raise error.RepoLookupError( | ||
|
r43347 | _(b"tag '%s' does not exist") % pattern | ||
|
r43346 | ) | ||
|
r32291 | s = {repo[tn].rev()} | ||
|
r16820 | else: | ||
|
r32291 | s = {cl.rev(n) for t, n in repo.tagslist() if matcher(t)} | ||
|
r12715 | else: | ||
|
r43347 | s = {cl.rev(n) for t, n in repo.tagslist() if t != b'tip'} | ||
|
r20367 | return subset & s | ||
|
r11280 | |||
|
r43346 | |||
|
r43347 | @predicate(b'tagged', safe=True) | ||
|
r12821 | def tagged(repo, subset, x): | ||
return tag(repo, subset, x) | ||||
|
r43346 | |||
|
r43347 | @predicate(b'orphan()', safe=True) | ||
|
r33769 | def orphan(repo, subset, x): | ||
|
r33855 | """Non-obsolete changesets with obsolete ancestors. (EXPERIMENTAL) | ||
|
r17291 | """ | ||
|
r33769 | # i18n: "orphan" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"orphan takes no arguments")) | ||
orphan = obsmod.getrevs(repo, b'orphan') | ||||
|
r33772 | return subset & orphan | ||
|
r17171 | |||
|
r45182 | @predicate(b'unstable()', safe=True) | ||
def unstable(repo, subset, x): | ||||
"""Changesets with instabilities. (EXPERIMENTAL) | ||||
""" | ||||
# i18n: "unstable" is a keyword | ||||
getargs(x, 0, 0, b'unstable takes no arguments') | ||||
_unstable = set() | ||||
_unstable.update(obsmod.getrevs(repo, b'orphan')) | ||||
_unstable.update(obsmod.getrevs(repo, b'phasedivergent')) | ||||
_unstable.update(obsmod.getrevs(repo, b'contentdivergent')) | ||||
_unstable = baseset(_unstable) | ||||
_unstable.sort() # set is non-ordered, enforce order | ||||
return subset & _unstable | ||||
|
r43347 | @predicate(b'user(string)', safe=True, weight=10) | ||
|
r13915 | def user(repo, subset, x): | ||
|
r27584 | """User name contains string. The match is case-insensitive. | ||
|
r16823 | |||
|
r30784 | Pattern matching is supported for `string`. See | ||
|
r30799 | :hg:`help revisions.patterns`. | ||
|
r13359 | """ | ||
|
r13915 | return author(repo, subset, x) | ||
|
r13359 | |||
|
r43346 | |||
|
r43347 | @predicate(b'wdir()', safe=True, weight=0) | ||
|
r24419 | def wdir(repo, subset, x): | ||
|
r30701 | """Working directory. (EXPERIMENTAL)""" | ||
|
r24419 | # i18n: "wdir" is a keyword | ||
|
r43347 | getargs(x, 0, 0, _(b"wdir takes no arguments")) | ||
|
r25765 | if node.wdirrev in subset or isinstance(subset, fullreposet): | ||
return baseset([node.wdirrev]) | ||||
|
r24419 | return baseset() | ||
|
r43346 | |||
|
r29935 | def _orderedlist(repo, subset, x): | ||
|
r43347 | s = getstring(x, b"internal error") | ||
|
r15898 | if not s: | ||
|
r22802 | return baseset() | ||
|
r25341 | # remove duplicates here. it's difficult for caller to deduplicate sets | ||
# because different symbols can point to the same rev. | ||||
|
r25344 | cl = repo.changelog | ||
|
r25341 | ls = [] | ||
seen = set() | ||||
|
r43347 | for t in s.split(b'\0'): | ||
|
r25344 | try: | ||
# fast path for integer revision | ||||
r = int(t) | ||||
|
r43347 | if (b'%d' % r) != t or r not in cl: | ||
|
r25344 | raise ValueError | ||
|
r26143 | revs = [r] | ||
|
r25344 | except ValueError: | ||
|
r34013 | revs = stringset(repo, subset, t, defineorder) | ||
|
r26143 | |||
for r in revs: | ||||
if r in seen: | ||||
continue | ||||
|
r43346 | if ( | ||
r in subset | ||||
or r in _virtualrevs | ||||
and isinstance(subset, fullreposet) | ||||
): | ||||
|
r26143 | ls.append(r) | ||
seen.add(r) | ||||
|
r25341 | return baseset(ls) | ||
|
r15898 | |||
|
r43346 | |||
|
r20566 | # for internal use | ||
|
r43347 | @predicate(b'_list', safe=True, takeorder=True) | ||
|
r29935 | def _list(repo, subset, x, order): | ||
if order == followorder: | ||||
# slow path to take the subset order | ||||
return subset & _orderedlist(repo, fullreposet(repo), x) | ||||
else: | ||||
return _orderedlist(repo, subset, x) | ||||
|
r43346 | |||
|
r29935 | def _orderedintlist(repo, subset, x): | ||
|
r43347 | s = getstring(x, b"internal error") | ||
|
r20566 | if not s: | ||
|
r22802 | return baseset() | ||
|
r43347 | ls = [int(r) for r in s.split(b'\0')] | ||
|
r22876 | s = subset | ||
|
r20566 | return baseset([r for r in ls if r in s]) | ||
|
r43346 | |||
|
r20569 | # for internal use | ||
|
r43347 | @predicate(b'_intlist', safe=True, takeorder=True, weight=0) | ||
|
r29935 | def _intlist(repo, subset, x, order): | ||
if order == followorder: | ||||
# slow path to take the subset order | ||||
return subset & _orderedintlist(repo, fullreposet(repo), x) | ||||
else: | ||||
return _orderedintlist(repo, subset, x) | ||||
|
r43346 | |||
|
r29935 | def _orderedhexlist(repo, subset, x): | ||
|
r43347 | s = getstring(x, b"internal error") | ||
|
r20569 | if not s: | ||
|
r22802 | return baseset() | ||
|
r20569 | cl = repo.changelog | ||
|
r43347 | ls = [cl.rev(node.bin(r)) for r in s.split(b'\0')] | ||
|
r22877 | s = subset | ||
|
r20569 | return baseset([r for r in ls if r in s]) | ||
|
r15898 | |||
|
r43346 | |||
|
r29935 | # for internal use | ||
|
r43347 | @predicate(b'_hexlist', safe=True, takeorder=True) | ||
|
r29935 | def _hexlist(repo, subset, x, order): | ||
if order == followorder: | ||||
# slow path to take the subset order | ||||
return subset & _orderedhexlist(repo, fullreposet(repo), x) | ||||
else: | ||||
return _orderedhexlist(repo, subset, x) | ||||
|
r43346 | |||
|
r11275 | methods = { | ||
|
r43347 | b"range": rangeset, | ||
b"rangeall": rangeall, | ||||
b"rangepre": rangepre, | ||||
b"rangepost": rangepost, | ||||
b"dagrange": dagrange, | ||||
b"string": stringset, | ||||
b"symbol": stringset, | ||||
b"and": andset, | ||||
b"andsmally": andsmallyset, | ||||
b"or": orset, | ||||
b"not": notset, | ||||
b"difference": differenceset, | ||||
b"relation": relationset, | ||||
b"relsubscript": relsubscriptset, | ||||
b"subscript": subscriptset, | ||||
b"list": listset, | ||||
b"keyvalue": keyvaluepair, | ||||
b"func": func, | ||||
b"ancestor": ancestorspec, | ||||
b"parent": parentspec, | ||||
b"parentpost": parentpost, | ||||
b"smartset": rawsmartset, | ||||
|
r11275 | } | ||
r45204 | relations = { | |||
b"g": generationsrel, | ||||
b"generations": generationsrel, | ||||
} | ||||
r40967 | subscriptrelations = { | |||
r45203 | b"g": generationssubrel, | |||
b"generations": generationssubrel, | ||||
r40967 | } | |||
|
r43346 | |||
|
r37368 | def lookupfn(repo): | ||
return lambda symbol: scmutil.isrevsymbol(repo, symbol) | ||||
|
r43346 | |||
|
r37692 | def match(ui, spec, lookup=None): | ||
|
r34020 | """Create a matcher for a single revision spec""" | ||
|
r37992 | return matchany(ui, [spec], lookup=lookup) | ||
|
r29955 | |||
|
r43346 | |||
|
r37692 | def matchany(ui, specs, lookup=None, localalias=None): | ||
|
r25927 | """Create a matcher that will include any revisions matching one of the | ||
|
r29955 | given specs | ||
|
r37692 | If lookup function is not None, the parser will first attempt to handle | ||
old-style ranges, which may contain operator characters. | ||||
|
r33336 | If localalias is not None, it is a dict {name: definitionstring}. It takes | ||
precedence over [revsetalias] config section. | ||||
|
r29955 | """ | ||
|
r25927 | if not specs: | ||
|
r43346 | |||
|
r25927 | def mfunc(repo, subset=None): | ||
return baseset() | ||||
|
r43346 | |||
|
r25927 | return mfunc | ||
if not all(specs): | ||||
|
r43347 | raise error.ParseError(_(b"empty query")) | ||
|
r25927 | if len(specs) == 1: | ||
|
r31024 | tree = revsetlang.parse(specs[0], lookup) | ||
|
r25927 | else: | ||
|
r43346 | tree = ( | ||
|
r43347 | b'or', | ||
(b'list',) + tuple(revsetlang.parse(s, lookup) for s in specs), | ||||
|
r43346 | ) | ||
|
r29906 | |||
|
r33336 | aliases = [] | ||
warn = None | ||||
|
r14900 | if ui: | ||
|
r43347 | aliases.extend(ui.configitems(b'revsetalias')) | ||
|
r33336 | warn = ui.warn | ||
if localalias: | ||||
aliases.extend(localalias.items()) | ||||
if aliases: | ||||
tree = revsetlang.expandaliases(tree, aliases, warn=warn) | ||||
|
r31024 | tree = revsetlang.foldconcat(tree) | ||
|
r34013 | tree = revsetlang.analyze(tree) | ||
|
r31024 | tree = revsetlang.optimize(tree) | ||
|
r34020 | return makematcher(tree) | ||
|
r29906 | |||
|
r43346 | |||
|
r34020 | def makematcher(tree): | ||
|
r29906 | """Create a matcher from an evaluatable tree""" | ||
|
r43346 | |||
|
r34021 | def mfunc(repo, subset=None, order=None): | ||
if order is None: | ||||
if subset is None: | ||||
order = defineorder # 'x' | ||||
else: | ||||
order = followorder # 'subset & x' | ||||
|
r24114 | if subset is None: | ||
|
r24115 | subset = fullreposet(repo) | ||
|
r34013 | return getset(repo, subset, tree, order) | ||
|
r43346 | |||
|
r11275 | return mfunc | ||
|
r12821 | |||
|
r43346 | |||
|
r28393 | def loadpredicate(ui, extname, registrarobj): | ||
"""Load revset predicates from specified registrarobj | ||||
""" | ||||
|
r43376 | for name, func in pycompat.iteritems(registrarobj._table): | ||
|
r28393 | symbols[name] = func | ||
if func._safe: | ||||
safesymbols.add(name) | ||||
|
r43346 | |||
|
r28395 | # load built-in predicates explicitly to setup safesymbols | ||
loadpredicate(None, None, predicate) | ||||
|
r12823 | # tell hggettext to extract docstrings from these functions: | ||
i18nfunctions = symbols.values() | ||||