revset.py
3485 lines
| 113.3 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 | ||
|
r20690 | import heapq | ||
|
r25971 | import re | ||
from .i18n import _ | ||||
from . import ( | ||||
|
r26713 | destutil, | ||
|
r25971 | encoding, | ||
error, | ||||
hbisect, | ||||
match as matchmod, | ||||
node, | ||||
obsolete as obsmod, | ||||
parser, | ||||
pathutil, | ||||
phases, | ||||
|
r27584 | registrar, | ||
|
r25971 | repoview, | ||
util, | ||||
) | ||||
|
r11275 | |||
|
r16409 | def _revancestors(repo, revs, followfirst): | ||
"""Like revlog.ancestors(), but supports followfirst.""" | ||||
|
r24306 | if followfirst: | ||
cut = 1 | ||||
else: | ||||
cut = None | ||||
|
r16409 | cl = repo.changelog | ||
|
r20690 | |||
def iterate(): | ||||
|
r22832 | revs.sort(reverse=True) | ||
|
r24939 | irevs = iter(revs) | ||
|
r20691 | h = [] | ||
|
r25143 | inputrev = next(irevs, None) | ||
if inputrev is not None: | ||||
|
r24939 | heapq.heappush(h, -inputrev) | ||
|
r20691 | |||
|
r23956 | seen = set() | ||
|
r20690 | while h: | ||
current = -heapq.heappop(h) | ||||
|
r24940 | if current == inputrev: | ||
|
r25143 | inputrev = next(irevs, None) | ||
if inputrev is not None: | ||||
|
r24940 | heapq.heappush(h, -inputrev) | ||
|
r20690 | if current not in seen: | ||
seen.add(current) | ||||
yield current | ||||
for parent in cl.parentrevs(current)[:cut]: | ||||
if parent != node.nullrev: | ||||
heapq.heappush(h, -parent) | ||||
|
r22795 | return generatorset(iterate(), iterasc=False) | ||
|
r16409 | |||
def _revdescendants(repo, revs, followfirst): | ||||
"""Like revlog.descendants() but supports followfirst.""" | ||||
|
r24306 | if followfirst: | ||
cut = 1 | ||||
else: | ||||
cut = None | ||||
|
r16409 | |||
|
r20692 | def iterate(): | ||
cl = repo.changelog | ||||
|
r25549 | # XXX this should be 'parentset.min()' assuming 'parentset' is a | ||
# smartset (and if it is not, it should.) | ||||
|
r20692 | first = min(revs) | ||
nullrev = node.nullrev | ||||
if first == nullrev: | ||||
# Are there nodes with a null first parent and a non-null | ||||
# second one? Maybe. Do we care? Probably not. | ||||
for i in cl: | ||||
|
r16409 | yield i | ||
|
r20692 | else: | ||
seen = set(revs) | ||||
for i in cl.revs(first + 1): | ||||
for x in cl.parentrevs(i)[:cut]: | ||||
if x != nullrev and x in seen: | ||||
seen.add(i) | ||||
yield i | ||||
break | ||||
|
r22795 | return generatorset(iterate(), iterasc=True) | ||
|
r16409 | |||
|
r26095 | def _reachablerootspure(repo, minroot, roots, heads, includepath): | ||
|
r26002 | """return (heads(::<roots> and ::<heads>)) | ||
If includepath is True, return (<roots>::<heads>).""" | ||||
|
r16862 | if not roots: | ||
|
r26094 | return [] | ||
|
r16862 | parentrevs = repo.changelog.parentrevs | ||
|
r26053 | roots = set(roots) | ||
|
r22487 | visit = list(heads) | ||
|
r16862 | reachable = set() | ||
seen = {} | ||||
|
r25566 | # prefetch all the things! (because python is slow) | ||
reached = reachable.add | ||||
dovisit = visit.append | ||||
nextvisit = visit.pop | ||||
|
r16862 | # open-code the post-order traversal due to the tiny size of | ||
# sys.getrecursionlimit() | ||||
while visit: | ||||
|
r25566 | rev = nextvisit() | ||
|
r16862 | if rev in roots: | ||
|
r25566 | reached(rev) | ||
|
r26002 | if not includepath: | ||
continue | ||||
|
r16862 | parents = parentrevs(rev) | ||
seen[rev] = parents | ||||
for parent in parents: | ||||
if parent >= minroot and parent not in seen: | ||||
|
r25566 | dovisit(parent) | ||
|
r16862 | if not reachable: | ||
|
r22802 | return baseset() | ||
|
r26002 | if not includepath: | ||
return reachable | ||||
|
r16862 | for rev in sorted(seen): | ||
for parent in seen[rev]: | ||||
if parent in reachable: | ||||
|
r25566 | reached(rev) | ||
|
r26091 | return reachable | ||
|
r16862 | |||
|
r26006 | def reachableroots(repo, roots, heads, includepath=False): | ||
"""return (heads(::<roots> and ::<heads>)) | ||||
If includepath is True, return (<roots>::<heads>).""" | ||||
if not roots: | ||||
return baseset() | ||||
|
r26093 | minroot = roots.min() | ||
|
r26053 | roots = list(roots) | ||
|
r26006 | heads = list(heads) | ||
try: | ||||
|
r26094 | revs = repo.changelog.reachableroots(minroot, heads, roots, includepath) | ||
|
r26006 | except AttributeError: | ||
|
r26095 | revs = _reachablerootspure(repo, minroot, roots, heads, includepath) | ||
|
r26094 | revs = baseset(revs) | ||
revs.sort() | ||||
return revs | ||||
|
r26006 | |||
|
r11275 | elements = { | ||
|
r25815 | # token-type: binding-strength, primary, prefix, infix, suffix | ||
"(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None), | ||||
"##": (20, None, None, ("_concat", 20), None), | ||||
"~": (18, None, None, ("ancestor", 18), None), | ||||
"^": (18, None, None, ("parent", 18), ("parentpost", 18)), | ||||
"-": (5, None, ("negate", 19), ("minus", 5), None), | ||||
"::": (17, None, ("dagrangepre", 17), ("dagrange", 17), | ||||
|
r11278 | ("dagrangepost", 17)), | ||
|
r25815 | "..": (17, None, ("dagrangepre", 17), ("dagrange", 17), | ||
|
r11278 | ("dagrangepost", 17)), | ||
|
r25819 | ":": (15, "rangeall", ("rangepre", 15), ("range", 15), ("rangepost", 15)), | ||
|
r25815 | "not": (10, None, ("not", 10), None, None), | ||
"!": (10, None, ("not", 10), None, None), | ||||
"and": (5, None, None, ("and", 5), None), | ||||
"&": (5, None, None, ("and", 5), None), | ||||
"%": (5, None, None, ("only", 5), ("onlypost", 5)), | ||||
"or": (4, None, None, ("or", 4), None), | ||||
"|": (4, None, None, ("or", 4), None), | ||||
"+": (4, None, None, ("or", 4), None), | ||||
"=": (3, None, None, ("keyvalue", 3), None), | ||||
",": (2, None, None, ("list", 2), None), | ||||
")": (0, None, None, None, None), | ||||
"symbol": (0, "symbol", None, None, None), | ||||
"string": (0, "string", None, None, None), | ||||
"end": (0, None, None, None, None), | ||||
|
r11275 | } | ||
keywords = set(['and', 'or', 'not']) | ||||
|
r23842 | # default set of valid characters for the initial letter of symbols | ||
_syminitletters = set(c for c in [chr(i) for i in xrange(256)] | ||||
if c.isalnum() or c in '._@' or ord(c) > 127) | ||||
# default set of valid characters for non-initial letters of symbols | ||||
_symletters = set(c for c in [chr(i) for i in xrange(256)] | ||||
if c.isalnum() or c in '-._/@' or ord(c) > 127) | ||||
def tokenize(program, lookup=None, syminitletters=None, symletters=None): | ||||
|
r17886 | ''' | ||
Parse a revset statement into a stream of tokens | ||||
|
r23842 | ``syminitletters`` is the set of valid characters for the initial | ||
letter of symbols. | ||||
By default, character ``c`` is recognized as valid for initial | ||||
letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``. | ||||
``symletters`` is the set of valid characters for non-initial | ||||
letters of symbols. | ||||
By default, character ``c`` is recognized as valid for non-initial | ||||
letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``. | ||||
|
r17886 | Check that @ is a valid unquoted token character (issue3686): | ||
>>> list(tokenize("@::")) | ||||
[('symbol', '@', 0), ('::', None, 1), ('end', None, 3)] | ||||
''' | ||||
|
r23842 | if syminitletters is None: | ||
syminitletters = _syminitletters | ||||
if symletters is None: | ||||
symletters = _symletters | ||||
|
r17886 | |||
|
r25902 | if program and lookup: | ||
# attempt to parse old-style ranges first to deal with | ||||
# things like old-tag which contain query metacharacters | ||||
parts = program.split(':', 1) | ||||
if all(lookup(sym) for sym in parts if sym): | ||||
if parts[0]: | ||||
yield ('symbol', parts[0], 0) | ||||
if len(parts) > 1: | ||||
s = len(parts[0]) | ||||
yield (':', None, s) | ||||
if parts[1]: | ||||
yield ('symbol', parts[1], s + 1) | ||||
yield ('end', None, len(program)) | ||||
return | ||||
|
r11275 | pos, l = 0, len(program) | ||
while pos < l: | ||||
c = program[pos] | ||||
if c.isspace(): # skip inter-token whitespace | ||||
pass | ||||
|
r11278 | elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully | ||
|
r11289 | yield ('::', None, pos) | ||
|
r11278 | pos += 1 # skip ahead | ||
|
r11275 | elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully | ||
|
r11289 | yield ('..', None, pos) | ||
|
r11275 | pos += 1 # skip ahead | ||
|
r23742 | elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully | ||
yield ('##', None, pos) | ||||
pos += 1 # skip ahead | ||||
|
r25704 | elif c in "():=,-|&+!~^%": # handle simple operators | ||
|
r11289 | yield (c, None, pos) | ||
|
r12408 | elif (c in '"\'' or c == 'r' and | ||
program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings | ||||
if c == 'r': | ||||
pos += 1 | ||||
c = program[pos] | ||||
decode = lambda x: x | ||||
else: | ||||
|
r26232 | decode = parser.unescapestr | ||
|
r11275 | pos += 1 | ||
s = pos | ||||
while pos < l: # find closing quote | ||||
d = program[pos] | ||||
if d == '\\': # skip over escaped characters | ||||
pos += 2 | ||||
continue | ||||
if d == c: | ||||
|
r12408 | yield ('string', decode(program[s:pos]), s) | ||
|
r11275 | break | ||
pos += 1 | ||||
else: | ||||
|
r11383 | raise error.ParseError(_("unterminated string"), s) | ||
|
r16683 | # gather up a symbol/keyword | ||
|
r23842 | elif c in syminitletters: | ||
|
r11275 | s = pos | ||
pos += 1 | ||||
while pos < l: # find end of symbol | ||||
d = program[pos] | ||||
|
r23842 | if d not in symletters: | ||
|
r11275 | break | ||
if d == '.' and program[pos - 1] == '.': # special case for .. | ||||
pos -= 1 | ||||
break | ||||
pos += 1 | ||||
sym = program[s:pos] | ||||
if sym in keywords: # operator keywords | ||||
|
r11289 | yield (sym, None, s) | ||
|
r20780 | elif '-' in sym: | ||
# some jerk gave us foo-bar-baz, try to check if it's a symbol | ||||
if lookup and lookup(sym): | ||||
# looks like a real symbol | ||||
yield ('symbol', sym, s) | ||||
else: | ||||
# looks like an expression | ||||
parts = sym.split('-') | ||||
for p in parts[:-1]: | ||||
if p: # possible consecutive - | ||||
yield ('symbol', p, s) | ||||
s += len(p) | ||||
yield ('-', None, pos) | ||||
s += 1 | ||||
if parts[-1]: # possible trailing - | ||||
yield ('symbol', parts[-1], s) | ||||
|
r11275 | else: | ||
|
r11289 | yield ('symbol', sym, s) | ||
|
r11275 | pos -= 1 | ||
else: | ||||
|
r24708 | raise error.ParseError(_("syntax error in revset '%s'") % | ||
program, pos) | ||||
|
r11275 | pos += 1 | ||
|
r11289 | yield ('end', None, pos) | ||
|
r11275 | |||
# helpers | ||||
def getstring(x, err): | ||||
|
r11406 | if x and (x[0] == 'string' or x[0] == 'symbol'): | ||
|
r11275 | return x[1] | ||
|
r11289 | raise error.ParseError(err) | ||
|
r11275 | |||
def getlist(x): | ||||
if not x: | ||||
return [] | ||||
if x[0] == 'list': | ||||
|
r27987 | return list(x[1:]) | ||
|
r11275 | return [x] | ||
|
r11339 | def getargs(x, min, max, err): | ||
|
r11275 | l = getlist(x) | ||
|
r16161 | if len(l) < min or (max >= 0 and len(l) > max): | ||
|
r11289 | raise error.ParseError(err) | ||
|
r11275 | return l | ||
|
r25767 | def getargsdict(x, funcname, keys): | ||
|
r25705 | return parser.buildargsdict(getlist(x), funcname, keys.split(), | ||
keyvaluenode='keyvalue', keynode='symbol') | ||||
|
r11275 | def getset(repo, subset, x): | ||
if not x: | ||||
|
r11383 | raise error.ParseError(_("missing argument")) | ||
|
r20527 | s = methods[x[0]](repo, subset, *x[1:]) | ||
|
r22884 | if util.safehasattr(s, 'isascending'): | ||
|
r20527 | return s | ||
|
r25630 | if (repo.ui.configbool('devel', 'all-warnings') | ||
or repo.ui.configbool('devel', 'old-revset')): | ||||
# else case should not happen, because all non-func are internal, | ||||
# ignoring for now. | ||||
if x[0] == 'func' and x[1][0] == 'symbol' and x[1][1] in symbols: | ||||
repo.ui.develwarn('revset "%s" use list instead of smartset, ' | ||||
'(upgrade your code)' % x[1][1]) | ||||
|
r20527 | return baseset(s) | ||
|
r11275 | |||
|
r17003 | def _getrevsource(repo, r): | ||
extra = repo[r].extra() | ||||
for label in ('source', 'transplant_source', 'rebase_source'): | ||||
if label in extra: | ||||
try: | ||||
return repo[extra[label]].rev() | ||||
except error.RepoLookupError: | ||||
pass | ||||
return None | ||||
|
r11275 | # operator methods | ||
def stringset(repo, subset, x): | ||||
x = repo[x].rev() | ||||
|
r25265 | if (x in subset | ||
or x == node.nullrev and isinstance(subset, fullreposet)): | ||||
|
r20364 | return baseset([x]) | ||
|
r22802 | return baseset() | ||
|
r11275 | |||
def rangeset(repo, subset, x, y): | ||||
|
r23162 | m = getset(repo, fullreposet(repo), x) | ||
n = getset(repo, fullreposet(repo), y) | ||||
|
r11456 | |||
if not m or not n: | ||||
|
r22802 | return baseset() | ||
|
r22817 | m, n = m.first(), n.last() | ||
|
r11456 | |||
|
r25766 | if m == n: | ||
r = baseset([m]) | ||||
elif n == node.wdirrev: | ||||
r = spanset(repo, m, len(repo)) + baseset([n]) | ||||
elif m == node.wdirrev: | ||||
r = baseset([m]) + spanset(repo, len(repo) - 1, n - 1) | ||||
elif m < n: | ||||
|
r20526 | r = spanset(repo, m, n + 1) | ||
|
r11456 | else: | ||
|
r20526 | r = spanset(repo, m, n - 1) | ||
|
r25548 | # XXX We should combine with subset first: 'subset & baseset(...)'. This is | ||
# necessary to ensure we preserve the order in subset. | ||||
# | ||||
# This has performance implication, carrying the sorting over when possible | ||||
# would be more efficient. | ||||
|
r20526 | return r & subset | ||
|
r11275 | |||
|
r16860 | def dagrange(repo, subset, x, y): | ||
|
r24115 | r = fullreposet(repo) | ||
|
r26002 | xs = reachableroots(repo, getset(repo, r, x), getset(repo, r, y), | ||
includepath=True) | ||||
|
r25548 | # XXX We should combine with subset first: 'subset & baseset(...)'. This is | ||
# necessary to ensure we preserve the order in subset. | ||||
|
r22866 | return xs & subset | ||
|
r16860 | |||
|
r11275 | def andset(repo, subset, x, y): | ||
return getset(repo, getset(repo, subset, x), y) | ||||
|
r28217 | def differenceset(repo, subset, x, y): | ||
return getset(repo, subset, x) - getset(repo, subset, y) | ||||
|
r25309 | def orset(repo, subset, *xs): | ||
|
r25929 | assert xs | ||
if len(xs) == 1: | ||||
return getset(repo, subset, xs[0]) | ||||
p = len(xs) // 2 | ||||
a = orset(repo, subset, *xs[:p]) | ||||
b = orset(repo, subset, *xs[p:]) | ||||
return a + b | ||||
|
r11275 | |||
def notset(repo, subset, x): | ||||
|
r20366 | return subset - getset(repo, subset, x) | ||
|
r11275 | |||
|
r27987 | def listset(repo, subset, *xs): | ||
|
r27517 | raise error.ParseError(_("can't use a list in this context"), | ||
hint=_('see hg help "revsets.x or y"')) | ||||
|
r11275 | |||
|
r25704 | def keyvaluepair(repo, subset, k, v): | ||
raise error.ParseError(_("can't use a key-value pair in this context")) | ||||
|
r11275 | def func(repo, subset, a, b): | ||
if a[0] == 'symbol' and a[1] in symbols: | ||||
return symbols[a[1]](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)] | ||||
raise error.UnknownIdentifier(a[1], syms) | ||||
|
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 | ||||
symbols = {} | ||||
|
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 | |||
|
r27584 | @predicate('_destupdate') | ||
|
r26713 | def _destupdate(repo, subset, x): | ||
# experimental revset for update destination | ||||
args = getargsdict(x, 'limit', 'clean check') | ||||
return subset & baseset([destutil.destupdate(repo, **args)[0]]) | ||||
|
r27584 | @predicate('_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 | |||
|
r27587 | @predicate('adds(pattern)', safe=True) | ||
|
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 | ||||
pat = getstring(x, _("adds requires a pattern")) | ||||
return checkstatus(repo, subset, pat, 1) | ||||
|
r27587 | @predicate('ancestor(*changeset)', safe=True) | ||
|
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 | """ | ||
# i18n: "ancestor" is a keyword | ||||
|
r18536 | l = getlist(x) | ||
|
r24115 | rl = fullreposet(repo) | ||
|
r18536 | anc = None | ||
|
r13915 | |||
|
r18536 | # (getset(repo, rl, i) for i in l) generates a list of lists | ||
for revs in (getset(repo, rl, i) for i in l): | ||||
for r in revs: | ||||
if anc is None: | ||||
|
r20991 | anc = repo[r] | ||
|
r18536 | else: | ||
|
r20991 | anc = anc.ancestor(repo[r]) | ||
if anc is not None and anc.rev() in subset: | ||||
return baseset([anc.rev()]) | ||||
|
r22802 | return baseset() | ||
|
r13915 | |||
|
r16409 | def _ancestors(repo, subset, x, followfirst=False): | ||
|
r24115 | heads = getset(repo, fullreposet(repo), x) | ||
|
r22944 | if not heads: | ||
|
r22802 | return baseset() | ||
|
r22944 | s = _revancestors(repo, heads, followfirst) | ||
|
r23003 | return subset & s | ||
|
r16409 | |||
|
r27587 | @predicate('ancestors(set)', safe=True) | ||
|
r13915 | def ancestors(repo, subset, x): | ||
|
r27584 | """Changesets that are ancestors of a changeset in set. | ||
|
r13915 | """ | ||
|
r16409 | return _ancestors(repo, subset, x) | ||
|
r27587 | @predicate('_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 | |||
|
r14070 | def ancestorspec(repo, subset, x, n): | ||
"""``set~n`` | ||||
|
r16683 | Changesets that are the Nth ancestor (first parents only) of a changeset | ||
in set. | ||||
|
r14070 | """ | ||
try: | ||||
n = int(n[1]) | ||||
|
r14851 | except (TypeError, ValueError): | ||
|
r14070 | raise error.ParseError(_("~ expects a number")) | ||
ps = set() | ||||
cl = repo.changelog | ||||
|
r23163 | for r in getset(repo, fullreposet(repo), x): | ||
|
r14070 | for i in range(n): | ||
r = cl.parentrevs(r)[0] | ||||
ps.add(r) | ||||
|
r22531 | return subset & ps | ||
|
r14070 | |||
|
r27587 | @predicate('author(string)', safe=True) | ||
|
r13915 | def author(repo, subset, x): | ||
|
r27584 | """Alias for ``user(string)``. | ||
|
r13915 | """ | ||
# i18n: "author" is a keyword | ||||
|
r15726 | n = encoding.lower(getstring(x, _("author requires a string"))) | ||
|
r16823 | kind, pattern, matcher = _substringmatcher(n) | ||
|
r28424 | return subset.filter(lambda x: matcher(encoding.lower(repo[x].user())), | ||
condrepr=('<user %r>', n)) | ||||
|
r13915 | |||
|
r27587 | @predicate('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 | ||
|
r15135 | status = getstring(x, _("bisect requires a string")).lower() | ||
|
r16467 | state = set(hbisect.get(repo, status)) | ||
|
r22532 | return subset & state | ||
|
r13915 | |||
|
r15134 | # Backward-compatibility | ||
# - no help entry so that we do not advertise it any more | ||||
|
r27587 | @predicate('bisected', safe=True) | ||
|
r15134 | def bisected(repo, subset, x): | ||
return bisect(repo, subset, x) | ||||
|
r27587 | @predicate('bookmark([name])', safe=True) | ||
|
r13915 | def bookmark(repo, subset, x): | ||
|
r27584 | """The named bookmark or all bookmarks. | ||
|
r16822 | |||
If `name` starts with `re:`, the remainder of the name is treated as | ||||
a regular expression. To match a bookmark that actually starts with `re:`, | ||||
use the prefix `literal:`. | ||||
|
r13915 | """ | ||
# i18n: "bookmark" is a keyword | ||||
args = getargs(x, 0, 1, _('bookmark takes one or no arguments')) | ||||
if args: | ||||
bm = getstring(args[0], | ||||
# i18n: "bookmark" is a keyword | ||||
_('the argument to bookmark must be a string')) | ||||
|
r26481 | kind, pattern, matcher = util.stringmatcher(bm) | ||
|
r22499 | bms = set() | ||
|
r16822 | if kind == 'literal': | ||
|
r22105 | bmrev = repo._bookmarks.get(pattern, None) | ||
|
r16822 | if not bmrev: | ||
|
r23978 | raise error.RepoLookupError(_("bookmark '%s' does not exist") | ||
|
r26538 | % pattern) | ||
|
r22499 | bms.add(repo[bmrev].rev()) | ||
|
r16822 | else: | ||
matchrevs = set() | ||||
|
r18495 | for name, bmrev in repo._bookmarks.iteritems(): | ||
|
r16822 | if matcher(name): | ||
matchrevs.add(bmrev) | ||||
if not matchrevs: | ||||
|
r23978 | raise error.RepoLookupError(_("no bookmarks exist" | ||
" that match '%s'") % pattern) | ||||
|
r16822 | for bmrev in matchrevs: | ||
|
r22499 | bms.add(repo[bmrev].rev()) | ||
else: | ||||
bms = set([repo[r].rev() | ||||
for r in repo._bookmarks.values()]) | ||||
|
r22500 | bms -= set([node.nullrev]) | ||
|
r22530 | return subset & bms | ||
|
r13915 | |||
|
r27587 | @predicate('branch(string or set)', safe=True) | ||
|
r13915 | def branch(repo, subset, x): | ||
|
r27584 | """ | ||
|
r13915 | All changesets belonging to the given branch or the branches of the given | ||
changesets. | ||||
|
r16821 | |||
If `string` starts with `re:`, the remainder of the name is treated as | ||||
a regular expression. To match a branch that actually starts with `re:`, | ||||
use the prefix `literal:`. | ||||
|
r13915 | """ | ||
|
r24374 | getbi = repo.revbranchcache().branchinfo | ||
|
r23787 | |||
|
r13915 | try: | ||
b = getstring(x, '') | ||||
except error.ParseError: | ||||
# not a string, but another revspec, e.g. tip() | ||||
pass | ||||
|
r16821 | else: | ||
|
r26481 | kind, pattern, matcher = util.stringmatcher(b) | ||
|
r16821 | if kind == 'literal': | ||
# note: falls through to the revspec case if no branch with | ||||
|
r26537 | # this name exists and pattern kind is not specified explicitly | ||
|
r16821 | if pattern in repo.branchmap(): | ||
|
r28424 | return subset.filter(lambda r: matcher(getbi(r)[0]), | ||
condrepr=('<branch %r>', b)) | ||||
|
r26537 | if b.startswith('literal:'): | ||
raise error.RepoLookupError(_("branch '%s' does not exist") | ||||
% pattern) | ||||
|
r16821 | else: | ||
|
r28424 | return subset.filter(lambda r: matcher(getbi(r)[0]), | ||
condrepr=('<branch %r>', b)) | ||||
|
r13915 | |||
|
r24115 | s = getset(repo, fullreposet(repo), x) | ||
|
r13915 | b = set() | ||
for r in s: | ||||
|
r24374 | b.add(getbi(r)[0]) | ||
|
r22867 | c = s.__contains__ | ||
|
r28424 | return subset.filter(lambda r: c(r) or getbi(r)[0] in b, | ||
condrepr=lambda: '<branch %r>' % sorted(b)) | ||||
|
r13915 | |||
|
r27587 | @predicate('bumped()', safe=True) | ||
|
r17829 | def bumped(repo, subset, x): | ||
|
r27584 | """Mutable changesets marked as successors of public changesets. | ||
|
r17829 | |||
Only non-public and non-obsolete changesets can be `bumped`. | ||||
""" | ||||
# i18n: "bumped" is a keyword | ||||
getargs(x, 0, 0, _("bumped takes no arguments")) | ||||
bumped = obsmod.getrevs(repo, 'bumped') | ||||
|
r20367 | return subset & bumped | ||
|
r17829 | |||
|
r27587 | @predicate('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: | ||
|
r26587 | raise error.Abort(_("no bundle provided - specify with -R")) | ||
|
r20367 | return subset & bundlerevs | ||
|
r17913 | |||
|
r13915 | def checkstatus(repo, subset, pat, field): | ||
|
r16521 | hasset = matchmod.patkind(pat) == 'set' | ||
|
r20457 | |||
|
r23115 | mcache = [None] | ||
|
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 | ||||
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: | ||
for f in c.files(): | ||||
if m(f): | ||||
break | ||||
else: | ||||
|
r20457 | return False | ||
|
r13915 | files = repo.status(c.p1().node(), c.node())[field] | ||
|
r16521 | if fname is not None: | ||
if fname in files: | ||||
|
r20457 | return True | ||
|
r13915 | else: | ||
for f in files: | ||||
if m(f): | ||||
|
r20457 | return True | ||
|
r28424 | return subset.filter(matches, condrepr=('<status[%r] %r>', field, pat)) | ||
|
r13915 | |||
|
r16396 | def _children(repo, narrow, parentset): | ||
|
r25550 | if not parentset: | ||
return baseset() | ||||
|
r15899 | cs = set() | ||
pr = repo.changelog.parentrevs | ||||
|
r25567 | minrev = parentset.min() | ||
|
r16394 | for r in narrow: | ||
|
r18063 | if r <= minrev: | ||
continue | ||||
|
r15899 | for p in pr(r): | ||
|
r16396 | if p in parentset: | ||
|
r15899 | cs.add(r) | ||
|
r25553 | # XXX using a set to feed the baseset is wrong. Sets are not ordered. | ||
# This does not break because of other fullreposet misbehavior. | ||||
|
r20709 | return baseset(cs) | ||
|
r15899 | |||
|
r27587 | @predicate('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 | |||
|
r27587 | @predicate('closed()', safe=True) | ||
|
r13915 | def closed(repo, subset, x): | ||
|
r27584 | """Changeset is closed. | ||
|
r13915 | """ | ||
# i18n: "closed" is a keyword | ||||
getargs(x, 0, 0, _("closed takes no arguments")) | ||||
|
r28424 | return subset.filter(lambda r: repo[r].closesbranch(), | ||
condrepr='<branch closed>') | ||||
|
r13915 | |||
|
r27584 | @predicate('contains(pattern)') | ||
|
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 | ||||
pat = getstring(x, _("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 | ||||
|
r28424 | return subset.filter(matches, condrepr=('<contains %r>', pat)) | ||
|
r13915 | |||
|
r27587 | @predicate('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 | ||
l = getargs(x, 0, 1, _('converted takes one or no arguments')) | ||||
if l: | ||||
|
r17259 | # i18n: "converted" is a keyword | ||
|
r17002 | rev = getstring(l[0], _('converted requires a revision')) | ||
def _matchvalue(r): | ||||
source = repo[r].extra().get('convert_revision', None) | ||||
return source is not None and (rev is None or source.startswith(rev)) | ||||
|
r28424 | return subset.filter(lambda r: _matchvalue(r), | ||
condrepr=('<converted %r>', rev)) | ||||
|
r17002 | |||
|
r27587 | @predicate('date(interval)', safe=True) | ||
|
r13915 | def date(repo, subset, x): | ||
|
r27584 | """Changesets within the interval, see :hg:`help dates`. | ||
|
r13915 | """ | ||
# i18n: "date" is a keyword | ||||
ds = getstring(x, _("date requires a string")) | ||||
dm = util.matchdate(ds) | ||||
|
r28424 | return subset.filter(lambda x: dm(repo[x].date()[0]), | ||
condrepr=('<date %r>', ds)) | ||||
|
r13915 | |||
|
r27587 | @predicate('desc(string)', safe=True) | ||
|
r14650 | def desc(repo, subset, x): | ||
|
r27584 | """Search commit message for string. The match is case-insensitive. | ||
|
r14650 | """ | ||
# i18n: "desc" is a keyword | ||||
|
r15726 | ds = encoding.lower(getstring(x, _("desc requires a string"))) | ||
|
r20452 | |||
def matches(x): | ||||
c = repo[x] | ||||
return ds in encoding.lower(c.description()) | ||||
|
r28424 | return subset.filter(matches, condrepr=('<desc %r>', ds)) | ||
|
r14650 | |||
|
r16409 | def _descendants(repo, subset, x, followfirst=False): | ||
|
r24115 | roots = getset(repo, fullreposet(repo), x) | ||
|
r22944 | if not roots: | ||
|
r22802 | return baseset() | ||
|
r22944 | s = _revdescendants(repo, roots, followfirst) | ||
|
r20894 | |||
# Both sets need to be ascending in order to lazily return the union | ||||
# in the correct order. | ||||
|
r22944 | base = subset & roots | ||
|
r22860 | desc = subset & s | ||
result = base + desc | ||||
if subset.isascending(): | ||||
result.sort() | ||||
elif subset.isdescending(): | ||||
result.sort(reverse=True) | ||||
else: | ||||
result = subset & result | ||||
|
r22830 | return result | ||
|
r16409 | |||
|
r27587 | @predicate('descendants(set)', safe=True) | ||
|
r13915 | def descendants(repo, subset, x): | ||
|
r27584 | """Changesets which are descendants of changesets in set. | ||
|
r13915 | """ | ||
|
r16409 | return _descendants(repo, subset, x) | ||
|
r27587 | @predicate('_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 | |||
|
r27587 | @predicate('destination([set])', safe=True) | ||
|
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) | ||||
|
r28424 | return subset.filter(dests.__contains__, | ||
condrepr=lambda: '<destination %r>' % sorted(dests)) | ||||
|
r17186 | |||
|
r27587 | @predicate('divergent()', safe=True) | ||
|
r18071 | def divergent(repo, subset, x): | ||
|
r27584 | """ | ||
|
r18071 | Final successors of changesets with an alternative set of final successors. | ||
""" | ||||
# i18n: "divergent" is a keyword | ||||
getargs(x, 0, 0, _("divergent takes no arguments")) | ||||
divergent = obsmod.getrevs(repo, 'divergent') | ||||
|
r22533 | return subset & divergent | ||
|
r18071 | |||
|
r27587 | @predicate('extinct()', safe=True) | ||
|
r17173 | def extinct(repo, subset, x): | ||
|
r27584 | """Obsolete changesets with obsolete descendants only. | ||
|
r17291 | """ | ||
|
r17259 | # i18n: "extinct" is a keyword | ||
|
r17258 | getargs(x, 0, 0, _("extinct takes no arguments")) | ||
|
r17825 | extincts = obsmod.getrevs(repo, 'extinct') | ||
|
r20367 | return subset & extincts | ||
|
r17173 | |||
|
r27587 | @predicate('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. | ||
If `value` starts with `re:`, the remainder of the value is treated as | ||||
a regular expression. To match a value that actually starts with `re:`, | ||||
use the prefix `literal:`. | ||||
""" | ||||
|
r25767 | args = getargsdict(x, 'extra', 'label value') | ||
|
r25706 | if 'label' not in args: | ||
# i18n: "extra" is a keyword | ||||
raise error.ParseError(_('extra takes at least 1 argument')) | ||||
|
r17259 | # i18n: "extra" is a keyword | ||
|
r25706 | label = getstring(args['label'], _('first argument to extra must be ' | ||
'a string')) | ||||
|
r16661 | value = None | ||
|
r25706 | if 'value' in args: | ||
|
r17259 | # i18n: "extra" is a keyword | ||
|
r25706 | value = getstring(args['value'], _('second argument to extra must be ' | ||
'a string')) | ||||
|
r26481 | kind, value, matcher = util.stringmatcher(value) | ||
|
r16661 | |||
def _matchvalue(r): | ||||
extra = repo[r].extra() | ||||
|
r16824 | return label in extra and (value is None or matcher(extra[label])) | ||
|
r16661 | |||
|
r28424 | return subset.filter(lambda r: _matchvalue(r), | ||
condrepr=('<extra[%r] %r>', label, value)) | ||||
|
r15819 | |||
|
r27587 | @predicate('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. | ||||
|
r23719 | |||
If some linkrev points to revisions filtered by the current repoview, we'll | ||||
work around it to return a non-filtered value. | ||||
|
r14342 | """ | ||
|
r17259 | # i18n: "filelog" is a keyword | ||
|
r14342 | pat = getstring(x, _("filelog requires a pattern")) | ||
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 | |||
|
r27587 | @predicate('first(set, [n])', safe=True) | ||
|
r15117 | def first(repo, subset, x): | ||
|
r27584 | """An alias for limit(). | ||
|
r15117 | """ | ||
return limit(repo, subset, x) | ||||
|
r16185 | def _follow(repo, subset, x, name, followfirst=False): | ||
|
r26102 | l = getargs(x, 0, 1, _("%s takes no arguments or a pattern") % name) | ||
|
r16185 | c = repo['.'] | ||
if l: | ||||
|
r26102 | x = getstring(l[0], _("%s expected a pattern") % name) | ||
matcher = matchmod.match(repo.root, repo.getcwd(), [x], | ||||
ctx=repo[None], default='path') | ||||
|
r28008 | files = c.manifest().walk(matcher) | ||
|
r26102 | s = set() | ||
|
r28008 | for fname in files: | ||
fctx = c[fname] | ||||
s = s.union(set(c.rev() for c in fctx.ancestors(followfirst))) | ||||
# include the revision responsible for the most recent version | ||||
s.add(fctx.introrev()) | ||||
|
r16185 | else: | ||
|
r20690 | s = _revancestors(repo, baseset([c.rev()]), followfirst) | ||
|
r16185 | |||
|
r22535 | return subset & s | ||
|
r16185 | |||
|
r27587 | @predicate('follow([pattern])', safe=True) | ||
|
r13915 | def follow(repo, subset, x): | ||
|
r27584 | """ | ||
|
r24366 | An alias for ``::.`` (ancestors of the working directory's first parent). | ||
|
r26102 | If pattern is specified, the histories of files matching given | ||
pattern is followed, including copies. | ||||
|
r14343 | """ | ||
|
r16185 | return _follow(repo, subset, x, 'follow') | ||
|
r14343 | |||
|
r27587 | @predicate('_followfirst', safe=True) | ||
|
r16174 | def _followfirst(repo, subset, x): | ||
|
r26102 | # ``followfirst([pattern])`` | ||
# Like ``follow([pattern])`` but follows only the first parent of | ||||
# every revisions or files revisions. | ||||
|
r16185 | return _follow(repo, subset, x, '_followfirst', followfirst=True) | ||
|
r14343 | |||
|
r27587 | @predicate('all()', safe=True) | ||
|
r13915 | def getall(repo, subset, x): | ||
|
r27584 | """All changesets, the same as ``0:tip``. | ||
|
r13915 | """ | ||
# i18n: "all" is a keyword | ||||
getargs(x, 0, 0, _("all takes no arguments")) | ||||
|
r24202 | return subset & spanset(repo) # drop "null" if any | ||
|
r13915 | |||
|
r27584 | @predicate('grep(regex)') | ||
|
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 | ||||
gr = re.compile(getstring(x, _("grep requires a string"))) | ||||
|
r25660 | except re.error as e: | ||
|
r13915 | raise error.ParseError(_('invalid match pattern: %s') % e) | ||
|
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 | ||||
|
r28424 | return subset.filter(matches, condrepr=('<grep %r>', gr.pattern)) | ||
|
r13915 | |||
|
r27587 | @predicate('_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 | |||
|
r28271 | l = getargs(x, 1, -1, "_matchfiles requires at least one argument") | ||
|
r16161 | pats, inc, exc = [], [], [] | ||
|
r16411 | rev, default = None, None | ||
|
r16161 | for arg in l: | ||
|
r28271 | s = getstring(arg, "_matchfiles requires string arguments") | ||
|
r16161 | prefix, value = s[:2], s[2:] | ||
if prefix == 'p:': | ||||
pats.append(value) | ||||
elif prefix == 'i:': | ||||
inc.append(value) | ||||
elif prefix == 'x:': | ||||
exc.append(value) | ||||
|
r16181 | elif prefix == 'r:': | ||
if rev is not None: | ||||
|
r28271 | raise error.ParseError('_matchfiles expected at most one ' | ||
'revision') | ||||
|
r23950 | if value != '': # empty means working directory; leave rev as None | ||
rev = value | ||||
|
r16411 | elif prefix == 'd:': | ||
if default is not None: | ||||
|
r28271 | raise error.ParseError('_matchfiles expected at most one ' | ||
'default mode') | ||||
|
r16411 | default = value | ||
|
r16161 | else: | ||
|
r28271 | raise error.ParseError('invalid _matchfiles prefix: %s' % prefix) | ||
|
r16411 | if not default: | ||
default = 'glob' | ||||
|
r20458 | |||
|
r23061 | m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc, | ||
exclude=exc, ctx=repo[rev], default=default) | ||||
|
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 | ||
|
r20458 | def matches(x): | ||
|
r27028 | if x == wdirrev: | ||
files = repo[x].files() | ||||
else: | ||||
|
r27440 | files = getfiles(x) | ||
|
r27028 | for f in files: | ||
|
r16161 | if m(f): | ||
|
r20458 | return True | ||
return False | ||||
|
r28424 | return subset.filter(matches, | ||
condrepr=('<matchfiles patterns=%r, include=%r ' | ||||
'exclude=%r, default=%r, rev=%r>', | ||||
pats, inc, exc, default, rev)) | ||||
|
r16161 | |||
|
r27587 | @predicate('file(pattern)', safe=True) | ||
|
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 | ||||
pat = getstring(x, _("file requires a pattern")) | ||||
|
r16161 | return _matchfiles(repo, subset, ('string', 'p:' + pat)) | ||
|
r13915 | |||
|
r27587 | @predicate('head()', safe=True) | ||
|
r13915 | def head(repo, subset, x): | ||
|
r27584 | """Changeset is a named branch head. | ||
|
r13915 | """ | ||
# i18n: "head" is a keyword | ||||
getargs(x, 0, 0, _("head takes no arguments")) | ||||
hs = set() | ||||
|
r25620 | cl = repo.changelog | ||
|
r13915 | for b, ls in repo.branchmap().iteritems(): | ||
|
r25620 | hs.update(cl.rev(h) for h in ls) | ||
|
r25553 | # XXX using a set to feed the baseset is wrong. Sets are not ordered. | ||
# This does not break because of other fullreposet misbehavior. | ||||
|
r25548 | # XXX We should combine with subset first: 'subset & baseset(...)'. This is | ||
# necessary to ensure we preserve the order in subset. | ||||
|
r25634 | return baseset(hs) & subset | ||
|
r13915 | |||
|
r27587 | @predicate('heads(set)', safe=True) | ||
|
r13915 | def heads(repo, subset, x): | ||
|
r27584 | """Members of set with no children in set. | ||
|
r13915 | """ | ||
s = getset(repo, subset, x) | ||||
|
r20366 | ps = parents(repo, subset, x) | ||
return s - ps | ||||
|
r13915 | |||
|
r27587 | @predicate('hidden()', safe=True) | ||
|
r17390 | def hidden(repo, subset, x): | ||
|
r27584 | """Hidden changesets. | ||
|
r17390 | """ | ||
# i18n: "hidden" is a keyword | ||||
getargs(x, 0, 0, _("hidden takes no arguments")) | ||||
|
r18382 | hiddenrevs = repoview.filterrevs(repo, 'visible') | ||
|
r20367 | return subset & hiddenrevs | ||
|
r17390 | |||
|
r27587 | @predicate('keyword(string)', safe=True) | ||
|
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. | ||
|
r13915 | """ | ||
# i18n: "keyword" is a keyword | ||||
|
r15726 | kw = encoding.lower(getstring(x, _("keyword requires a string"))) | ||
|
r20447 | |||
def matches(r): | ||||
|
r13915 | c = repo[r] | ||
|
r25551 | return any(kw in encoding.lower(t) | ||
for t in c.files() + [c.user(), c.description()]) | ||||
|
r20447 | |||
|
r28424 | return subset.filter(matches, condrepr=('<keyword %r>', kw)) | ||
|
r13915 | |||
|
r27587 | @predicate('limit(set[, n[, offset]])', safe=True) | ||
|
r13915 | def limit(repo, subset, x): | ||
|
r27584 | """First n members of set, defaulting to 1, starting from offset. | ||
|
r13915 | """ | ||
|
r26638 | args = getargsdict(x, 'limit', 'set n offset') | ||
|
r26637 | if 'set' not in args: | ||
# i18n: "limit" is a keyword | ||||
|
r26638 | raise error.ParseError(_("limit requires one to three arguments")) | ||
|
r13915 | try: | ||
|
r26638 | lim, ofs = 1, 0 | ||
|
r26637 | if 'n' in args: | ||
|
r15116 | # i18n: "limit" is a keyword | ||
|
r26637 | lim = int(getstring(args['n'], _("limit requires a number"))) | ||
|
r26638 | if 'offset' in args: | ||
# i18n: "limit" is a keyword | ||||
ofs = int(getstring(args['offset'], _("limit requires a number"))) | ||||
if ofs < 0: | ||||
raise error.ParseError(_("negative offset")) | ||||
|
r14851 | except (TypeError, ValueError): | ||
|
r13915 | # i18n: "limit" is a keyword | ||
raise error.ParseError(_("limit expects a number")) | ||||
|
r26637 | os = getset(repo, fullreposet(repo), args['set']) | ||
|
r22804 | result = [] | ||
|
r20446 | it = iter(os) | ||
|
r26638 | for x in xrange(ofs): | ||
y = next(it, None) | ||||
if y is None: | ||||
break | ||||
|
r20446 | for x in xrange(lim): | ||
|
r25144 | y = next(it, None) | ||
if y is None: | ||||
|
r20446 | break | ||
|
r26636 | elif y in subset: | ||
|
r25144 | result.append(y) | ||
|
r28426 | return baseset(result, datarepr=('<limit n=%d, offset=%d, %r, %r>', | ||
lim, ofs, subset, os)) | ||||
|
r13915 | |||
|
r27587 | @predicate('last(set, [n])', safe=True) | ||
|
r14061 | def last(repo, subset, x): | ||
|
r27584 | """Last n members of set, defaulting to 1. | ||
|
r14061 | """ | ||
# i18n: "last" is a keyword | ||||
|
r15116 | l = getargs(x, 1, 2, _("last requires one or two arguments")) | ||
|
r14061 | try: | ||
|
r15116 | lim = 1 | ||
if len(l) == 2: | ||||
# i18n: "last" is a keyword | ||||
lim = int(getstring(l[1], _("last requires a number"))) | ||||
|
r14851 | except (TypeError, ValueError): | ||
|
r14061 | # i18n: "last" is a keyword | ||
raise error.ParseError(_("last expects a number")) | ||||
|
r24115 | os = getset(repo, fullreposet(repo), l[0]) | ||
|
r20534 | os.reverse() | ||
|
r22805 | result = [] | ||
|
r20534 | it = iter(os) | ||
for x in xrange(lim): | ||||
|
r25145 | y = next(it, None) | ||
if y is None: | ||||
|
r20534 | break | ||
|
r26636 | elif y in subset: | ||
|
r25145 | result.append(y) | ||
|
r28426 | return baseset(result, datarepr=('<last n=%d, %r, %r>', lim, subset, os)) | ||
|
r14061 | |||
|
r27587 | @predicate('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: | ||
|
r28427 | return baseset([m], datarepr=('<max %r, %r>', subset, os)) | ||
|
r26305 | except ValueError: | ||
# os.max() throws a ValueError when the collection is empty. | ||||
# Same as python's max(). | ||||
pass | ||||
|
r28427 | return baseset(datarepr=('<max %r, %r>', subset, os)) | ||
|
r13915 | |||
|
r27587 | @predicate('merge()', safe=True) | ||
|
r13915 | def merge(repo, subset, x): | ||
|
r27584 | """Changeset is a merge changeset. | ||
|
r13915 | """ | ||
# i18n: "merge" is a keyword | ||||
getargs(x, 0, 0, _("merge takes no arguments")) | ||||
cl = repo.changelog | ||||
|
r28424 | return subset.filter(lambda r: cl.parentrevs(r)[1] != -1, | ||
condrepr='<merge>') | ||||
|
r13915 | |||
|
r27587 | @predicate('branchpoint()', safe=True) | ||
|
r17753 | def branchpoint(repo, subset, x): | ||
|
r27584 | """Changesets with more than one child. | ||
|
r17753 | """ | ||
# i18n: "branchpoint" is a keyword | ||||
getargs(x, 0, 0, _("branchpoint takes no arguments")) | ||||
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) | ||
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 | ||||
|
r28424 | return subset.filter(lambda r: parentscount[r - baserev] > 1, | ||
condrepr='<branchpoint>') | ||||
|
r17753 | |||
|
r27587 | @predicate('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: | ||
|
r28427 | return baseset([m], datarepr=('<min %r, %r>', subset, os)) | ||
|
r26305 | except ValueError: | ||
# os.min() throws a ValueError when the collection is empty. | ||||
# Same as python's min(). | ||||
pass | ||||
|
r28427 | return baseset(datarepr=('<min %r, %r>', subset, os)) | ||
|
r13915 | |||
|
r27587 | @predicate('modifies(pattern)', safe=True) | ||
|
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 | ||||
pat = getstring(x, _("modifies requires a pattern")) | ||||
return checkstatus(repo, subset, pat, 0) | ||||
|
r27584 | @predicate('named(namespace)') | ||
|
r23836 | def named(repo, subset, x): | ||
|
r27584 | """The changesets in a given namespace. | ||
|
r23836 | |||
If `namespace` starts with `re:`, the remainder of the string is treated as | ||||
a regular expression. To match a namespace that actually starts with `re:`, | ||||
use the prefix `literal:`. | ||||
""" | ||||
# i18n: "named" is a keyword | ||||
args = getargs(x, 1, 1, _('named requires a namespace argument')) | ||||
ns = getstring(args[0], | ||||
# i18n: "named" is a keyword | ||||
_('the argument to named must be a string')) | ||||
|
r26481 | kind, pattern, matcher = util.stringmatcher(ns) | ||
|
r23836 | namespaces = set() | ||
if kind == 'literal': | ||||
if pattern not in repo.names: | ||||
|
r23978 | raise error.RepoLookupError(_("namespace '%s' does not exist") | ||
% ns) | ||||
|
r23836 | namespaces.add(repo.names[pattern]) | ||
else: | ||||
for name, ns in repo.names.iteritems(): | ||||
if matcher(name): | ||||
namespaces.add(ns) | ||||
if not namespaces: | ||||
|
r23978 | raise error.RepoLookupError(_("no namespace exists" | ||
" that match '%s'") % pattern) | ||||
|
r23836 | |||
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 | |||
names -= set([node.nullrev]) | ||||
return subset & names | ||||
|
r27587 | @predicate('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 | ||
|
r12736 | l = getargs(x, 1, 1, _("id requires one argument")) | ||
|
r12815 | # i18n: "id" is a keyword | ||
|
r12736 | n = getstring(l[0], _("id requires a string")) | ||
|
r12716 | if len(n) == 40: | ||
|
r24904 | try: | ||
rn = repo.changelog.rev(node.bin(n)) | ||||
except (LookupError, TypeError): | ||||
rn = None | ||||
|
r12716 | else: | ||
|
r16735 | rn = None | ||
pm = repo.changelog._partialmatch(n) | ||||
if pm is not None: | ||||
rn = repo.changelog.rev(pm) | ||||
|
r23005 | if rn is None: | ||
return baseset() | ||||
result = baseset([rn]) | ||||
return result & subset | ||||
|
r12716 | |||
|
r27587 | @predicate('obsolete()', safe=True) | ||
|
r17170 | def obsolete(repo, subset, x): | ||
|
r27584 | """Mutable changeset with a newer version.""" | ||
|
r17259 | # i18n: "obsolete" is a keyword | ||
|
r17170 | getargs(x, 0, 0, _("obsolete takes no arguments")) | ||
|
r17825 | obsoletes = obsmod.getrevs(repo, 'obsolete') | ||
|
r20367 | return subset & obsoletes | ||
|
r17170 | |||
|
r27587 | @predicate('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 | ||||
args = getargs(x, 1, 2, _('only takes one or two arguments')) | ||||
|
r24115 | include = getset(repo, fullreposet(repo), args[0]) | ||
|
r23466 | if len(args) == 1: | ||
if not include: | ||||
return baseset() | ||||
descendants = set(_revdescendants(repo, include, False)) | ||||
exclude = [rev for rev in cl.headrevs() | ||||
if not rev in descendants and not rev in include] | ||||
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 | ||
# some optimisations from the fact this is a baseset. | ||||
|
r23466 | return subset & results | ||
|
r27587 | @predicate('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 | ||||
|
r22944 | o = set([_firstsrc(r) for r in dests]) | ||
|
r22498 | o -= set([None]) | ||
|
r25554 | # XXX we should turn this into a baseset instead of a set, smartset may do | ||
# some optimisations from the fact this is a baseset. | ||||
|
r22536 | return subset & o | ||
|
r17185 | |||
|
r27587 | @predicate('outgoing([path])', safe=True) | ||
|
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, | ||||
) | ||||
|
r13915 | # i18n: "outgoing" is a keyword | ||
|
r14717 | l = getargs(x, 0, 1, _("outgoing takes one or no arguments")) | ||
|
r13915 | # i18n: "outgoing" is a keyword | ||
dest = l and getstring(l[0], _("outgoing requires a repository path")) or '' | ||||
dest = repo.ui.expandpath(dest or 'default-push', dest or 'default') | ||||
dest, branches = hg.parseurl(dest) | ||||
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 | ||||
|
r15837 | o = set([cl.rev(r) for r in outgoing.missing]) | ||
|
r22529 | return subset & o | ||
|
r12716 | |||
|
r27587 | @predicate('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): | ||
|
r11275 | ps.add(cl.parentrevs(r)[0]) | ||
|
r22495 | ps -= set([node.nullrev]) | ||
|
r25554 | # XXX we should turn this into a baseset instead of a set, smartset may do | ||
# some optimisations from the fact this is a baseset. | ||||
|
r20367 | return subset & ps | ||
|
r11275 | |||
|
r27587 | @predicate('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): | ||
|
r11275 | ps.add(cl.parentrevs(r)[1]) | ||
|
r22495 | ps -= set([node.nullrev]) | ||
|
r25554 | # XXX we should turn this into a baseset instead of a set, smartset may do | ||
# some optimisations from the fact this is a baseset. | ||||
|
r20367 | return subset & ps | ||
|
r11275 | |||
|
r27587 | @predicate('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: | ||
|
r22496 | ps = set(p.rev() for p in repo[x].parents()) | ||
else: | ||||
ps = set() | ||||
cl = repo.changelog | ||||
|
r25716 | up = ps.update | ||
parentrevs = cl.parentrevs | ||||
|
r24115 | for r in getset(repo, fullreposet(repo), x): | ||
|
r25765 | if r == node.wdirrev: | ||
|
r25716 | up(p.rev() for p in repo[r].parents()) | ||
|
r25689 | else: | ||
|
r25716 | up(parentrevs(r)) | ||
|
r22497 | ps -= set([node.nullrev]) | ||
|
r22712 | return subset & ps | ||
|
r11275 | |||
|
r25621 | def _phase(repo, subset, target): | ||
"""helper to select all rev in phase <target>""" | ||||
|
r25622 | repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded | ||
if repo._phasecache._phasesets: | ||||
s = repo._phasecache._phasesets[target] - repo.changelog.filteredrevs | ||||
s = baseset(s) | ||||
s.sort() # set are non ordered, so we enforce ascending | ||||
return subset & s | ||||
else: | ||||
phase = repo._phasecache.phase | ||||
condition = lambda r: phase(repo, r) == target | ||||
|
r28424 | return subset.filter(condition, condrepr=('<phase %r>', target), | ||
cache=False) | ||||
|
r25621 | |||
|
r27587 | @predicate('draft()', safe=True) | ||
|
r25621 | def draft(repo, subset, x): | ||
|
r27584 | """Changeset in draft phase.""" | ||
|
r25621 | # i18n: "draft" is a keyword | ||
getargs(x, 0, 0, _("draft takes no arguments")) | ||||
target = phases.draft | ||||
return _phase(repo, subset, target) | ||||
|
r27587 | @predicate('secret()', safe=True) | ||
|
r25621 | def secret(repo, subset, x): | ||
|
r27584 | """Changeset in secret phase.""" | ||
|
r25621 | # i18n: "secret" is a keyword | ||
getargs(x, 0, 0, _("secret takes no arguments")) | ||||
target = phases.secret | ||||
return _phase(repo, subset, target) | ||||
|
r14070 | def parentspec(repo, subset, x, n): | ||
"""``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): | ||
|
r14070 | raise error.ParseError(_("^ expects a number 0, 1, or 2")) | ||
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: | ||||
ps.add(cl.parentrevs(r)[0]) | ||||
elif n == 2: | ||||
parents = cl.parentrevs(r) | ||||
if len(parents) > 1: | ||||
ps.add(parents[1]) | ||||
|
r20367 | return subset & ps | ||
|
r11275 | |||
|
r27587 | @predicate('present(set)', safe=True) | ||
|
r11944 | def present(repo, subset, x): | ||
|
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: | ||
return getset(repo, subset, x) | ||||
except error.RepoLookupError: | ||||
|
r22802 | return baseset() | ||
|
r11944 | |||
|
r25224 | # for internal use | ||
|
r27587 | @predicate('_notpublic', safe=True) | ||
|
r25191 | def _notpublic(repo, subset, x): | ||
|
r25225 | getargs(x, 0, 0, "_notpublic takes no arguments") | ||
|
r25612 | repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded | ||
|
r25191 | if repo._phasecache._phasesets: | ||
s = set() | ||||
for u in repo._phasecache._phasesets[1:]: | ||||
s.update(u) | ||||
|
r25619 | s = baseset(s - repo.changelog.filteredrevs) | ||
s.sort() | ||||
|
r25191 | return subset & s | ||
else: | ||||
phase = repo._phasecache.phase | ||||
target = phases.public | ||||
condition = lambda r: phase(repo, r) != target | ||||
|
r28424 | return subset.filter(condition, condrepr=('<phase %r>', target), | ||
cache=False) | ||||
|
r25191 | |||
|
r27587 | @predicate('public()', safe=True) | ||
|
r15819 | def public(repo, subset, x): | ||
|
r27584 | """Changeset in public phase.""" | ||
|
r17259 | # i18n: "public" is a keyword | ||
|
r15819 | getargs(x, 0, 0, _("public takes no arguments")) | ||
|
r23019 | phase = repo._phasecache.phase | ||
target = phases.public | ||||
condition = lambda r: phase(repo, r) == target | ||||
|
r28424 | return subset.filter(condition, condrepr=('<phase %r>', target), | ||
cache=False) | ||||
|
r15819 | |||
|
r27587 | @predicate('remote([id [,path]])', safe=True) | ||
|
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. | ||||
""" | ||||
|
r25971 | from . import hg # avoid start-up nasties | ||
|
r15936 | # i18n: "remote" is a keyword | ||
|
r27293 | l = getargs(x, 0, 2, _("remote takes zero, one, or two arguments")) | ||
|
r15936 | |||
q = '.' | ||||
if len(l) > 0: | ||||
# i18n: "remote" is a keyword | ||||
q = getstring(l[0], _("remote requires a string id")) | ||||
if q == '.': | ||||
q = repo['.'].branch() | ||||
dest = '' | ||||
if len(l) > 1: | ||||
# i18n: "remote" is a keyword | ||||
dest = getstring(l[1], _("remote requires a repository path")) | ||||
dest = repo.ui.expandpath(dest or 'default') | ||||
dest, branches = hg.parseurl(dest) | ||||
revs, checkout = hg.addbranchrevs(repo, repo, branches, []) | ||||
if revs: | ||||
revs = [repo.lookup(rev) for rev in revs] | ||||
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 | |||
|
r27587 | @predicate('removes(pattern)', safe=True) | ||
|
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 | ||
|
r12736 | pat = getstring(x, _("removes requires a pattern")) | ||
|
r11275 | return checkstatus(repo, subset, pat, 2) | ||
|
r27587 | @predicate('rev(number)', safe=True) | ||
|
r13915 | def rev(repo, subset, x): | ||
|
r27584 | """Revision with the given numeric identifier. | ||
|
r12821 | """ | ||
|
r13915 | # i18n: "rev" is a keyword | ||
l = getargs(x, 1, 1, _("rev requires one argument")) | ||||
try: | ||||
# i18n: "rev" is a keyword | ||||
l = int(getstring(l[0], _("rev requires a number"))) | ||||
|
r14851 | except (TypeError, ValueError): | ||
|
r13915 | # i18n: "rev" is a keyword | ||
raise error.ParseError(_("rev expects a number")) | ||||
|
r24031 | if l not in repo.changelog and l != node.nullrev: | ||
|
r23062 | return baseset() | ||
|
r22537 | return subset & baseset([l]) | ||
|
r11275 | |||
|
r27587 | @predicate('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 | ||
|
r16402 | l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments")) | ||
|
r23166 | revs = getset(repo, fullreposet(repo), l[0]) | ||
|
r16402 | |||
fieldlist = ['metadata'] | ||||
if len(l) > 1: | ||||
fieldlist = getstring(l[1], | ||||
|
r17259 | # i18n: "matching" is a keyword | ||
|
r16402 | _("matching requires a string " | ||
"as its second argument")).split() | ||||
|
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: | ||||
if field == 'metadata': | ||||
fields += ['user', 'description', 'date'] | ||||
|
r17102 | elif field == 'diff': | ||
# 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 | ||||
fields += ['files', 'diff'] | ||||
|
r16402 | else: | ||
if field == 'author': | ||||
field = 'user' | ||||
fields.append(field) | ||||
fields = set(fields) | ||||
|
r16444 | if 'summary' in fields and 'description' in fields: | ||
# If a revision matches its description it also matches its summary | ||||
fields.discard('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 | ||||
|
r16453 | fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary', | ||
|
r17102 | 'files', 'description', 'substate', 'diff'] | ||
|
r16446 | def fieldkeyfunc(f): | ||
try: | ||||
return fieldorder.index(f) | ||||
except ValueError: | ||||
# assume an unknown field is very costly | ||||
return len(fieldorder) | ||||
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 = { | ||||
'user': lambda r: repo[r].user(), | ||||
'branch': lambda r: repo[r].branch(), | ||||
'date': lambda r: repo[r].date(), | ||||
'description': lambda r: repo[r].description(), | ||||
'files': lambda r: repo[r].files(), | ||||
'parents': lambda r: repo[r].parents(), | ||||
'phase': lambda r: repo[r].phase(), | ||||
'substate': lambda r: repo[r].substate, | ||||
'summary': lambda r: repo[r].description().splitlines()[0], | ||||
|
r17102 | 'diff': lambda r: list(repo[r].diff(git=True),) | ||
|
r16402 | } | ||
for info in fields: | ||||
getfield = _funcs.get(info, None) | ||||
if getfield is None: | ||||
raise error.ParseError( | ||||
|
r17259 | # i18n: "matching" is a keyword | ||
|
r16402 | _("unexpected field name passed to matching: %s") % info) | ||
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 | ||||
|
r28424 | return subset.filter(matches, condrepr=('<matching%r %r>', fields, revs)) | ||
|
r16402 | |||
|
r27587 | @predicate('reverse(set)', safe=True) | ||
|
r11275 | def reverse(repo, subset, x): | ||
|
r27584 | """Reverse order of set. | ||
|
r12821 | """ | ||
|
r11275 | l = getset(repo, subset, x) | ||
l.reverse() | ||||
return l | ||||
|
r27587 | @predicate('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 | ||
def filter(r): | ||||
for p in parents(r): | ||||
if 0 <= p and p in s: | ||||
return False | ||||
return True | ||||
|
r28424 | return subset & s.filter(filter, condrepr='<roots>') | ||
|
r11944 | |||
|
r27587 | @predicate('sort(set[, [-]key...])', safe=True) | ||
|
r11275 | def sort(repo, subset, x): | ||
|
r27584 | """Sort set by keys. The default sort order is ascending, specify a key | ||
|
r12821 | 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 | ||||
""" | ||||
|
r12815 | # i18n: "sort" is a keyword | ||
|
r12736 | l = getargs(x, 1, 2, _("sort requires one or two arguments")) | ||
|
r11275 | keys = "rev" | ||
if len(l) == 2: | ||||
|
r17259 | # i18n: "sort" is a keyword | ||
|
r11383 | keys = getstring(l[1], _("sort spec must be a string")) | ||
|
r11275 | |||
s = l[0] | ||||
keys = keys.split() | ||||
l = [] | ||||
def invert(s): | ||||
return "".join(chr(255 - ord(c)) for c in s) | ||||
|
r20719 | revs = getset(repo, subset, s) | ||
if keys == ["rev"]: | ||||
revs.sort() | ||||
return revs | ||||
elif keys == ["-rev"]: | ||||
revs.sort(reverse=True) | ||||
return revs | ||||
for r in revs: | ||||
|
r11275 | c = repo[r] | ||
e = [] | ||||
for k in keys: | ||||
if k == 'rev': | ||||
e.append(r) | ||||
elif k == '-rev': | ||||
e.append(-r) | ||||
elif k == 'branch': | ||||
e.append(c.branch()) | ||||
elif k == '-branch': | ||||
e.append(invert(c.branch())) | ||||
elif k == 'desc': | ||||
e.append(c.description()) | ||||
elif k == '-desc': | ||||
e.append(invert(c.description())) | ||||
elif k in 'user author': | ||||
e.append(c.user()) | ||||
elif k in '-user -author': | ||||
e.append(invert(c.user())) | ||||
elif k == 'date': | ||||
e.append(c.date()[0]) | ||||
elif k == '-date': | ||||
e.append(-c.date()[0]) | ||||
else: | ||||
|
r11383 | raise error.ParseError(_("unknown sort key %r") % k) | ||
|
r11275 | e.append(r) | ||
l.append(e) | ||||
l.sort() | ||||
|
r20364 | return baseset([e[-1] for e in l]) | ||
|
r11275 | |||
|
r27584 | @predicate('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 | ||||
args = getargs(x, 0, 1, _('subrepo takes at most one argument')) | ||||
|
r28272 | pat = None | ||
|
r24446 | if len(args) != 0: | ||
pat = getstring(args[0], _("subrepo requires a pattern")) | ||||
m = matchmod.exact(repo.root, repo.root, ['.hgsubstate']) | ||||
def submatches(names): | ||||
|
r26481 | k, p, m = util.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 | ||||
|
r28424 | return subset.filter(matches, condrepr=('<subrepo %r>', pat)) | ||
|
r24446 | |||
|
r16823 | def _substringmatcher(pattern): | ||
|
r26481 | kind, pattern, matcher = util.stringmatcher(pattern) | ||
|
r16823 | if kind == 'literal': | ||
matcher = lambda s: pattern in s | ||||
return kind, pattern, matcher | ||||
|
r16819 | |||
|
r27587 | @predicate('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 | |||
If `name` starts with `re:`, the remainder of the name is treated as | ||||
a regular expression. To match a tag that actually starts with `re:`, | ||||
use the prefix `literal:`. | ||||
|
r12821 | """ | ||
|
r12815 | # i18n: "tag" is a keyword | ||
|
r12715 | args = getargs(x, 0, 1, _("tag takes one or no arguments")) | ||
|
r11280 | cl = repo.changelog | ||
|
r12715 | if args: | ||
|
r16820 | pattern = getstring(args[0], | ||
# i18n: "tag" is a keyword | ||||
_('the argument to tag must be a string')) | ||||
|
r26481 | kind, pattern, matcher = util.stringmatcher(pattern) | ||
|
r16820 | if kind == 'literal': | ||
|
r16825 | # avoid resolving all tags | ||
tn = repo._tagscache.tags.get(pattern, None) | ||||
if tn is None: | ||||
|
r23978 | raise error.RepoLookupError(_("tag '%s' does not exist") | ||
% pattern) | ||||
|
r16825 | s = set([repo[tn].rev()]) | ||
|
r16820 | else: | ||
s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)]) | ||||
|
r12715 | else: | ||
s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip']) | ||||
|
r20367 | return subset & s | ||
|
r11280 | |||
|
r27587 | @predicate('tagged', safe=True) | ||
|
r12821 | def tagged(repo, subset, x): | ||
return tag(repo, subset, x) | ||||
|
r27587 | @predicate('unstable()', safe=True) | ||
|
r17171 | def unstable(repo, subset, x): | ||
|
r27584 | """Non-obsolete changesets with obsolete ancestors. | ||
|
r17291 | """ | ||
|
r17259 | # i18n: "unstable" is a keyword | ||
|
r17258 | getargs(x, 0, 0, _("unstable takes no arguments")) | ||
|
r17825 | unstables = obsmod.getrevs(repo, 'unstable') | ||
|
r20367 | return subset & unstables | ||
|
r17171 | |||
|
r27587 | @predicate('user(string)', safe=True) | ||
|
r13915 | def user(repo, subset, x): | ||
|
r27584 | """User name contains string. The match is case-insensitive. | ||
|
r16823 | |||
If `string` starts with `re:`, the remainder of the string is treated as | ||||
a regular expression. To match a user that actually contains `re:`, use | ||||
the prefix `literal:`. | ||||
|
r13359 | """ | ||
|
r13915 | return author(repo, subset, x) | ||
|
r13359 | |||
|
r24777 | # experimental | ||
|
r27587 | @predicate('wdir', safe=True) | ||
|
r24419 | def wdir(repo, subset, x): | ||
# i18n: "wdir" is a keyword | ||||
getargs(x, 0, 0, _("wdir takes no arguments")) | ||||
|
r25765 | if node.wdirrev in subset or isinstance(subset, fullreposet): | ||
return baseset([node.wdirrev]) | ||||
|
r24419 | return baseset() | ||
|
r15898 | # for internal use | ||
|
r27587 | @predicate('_list', safe=True) | ||
|
r15898 | def _list(repo, subset, x): | ||
s = getstring(x, "internal error") | ||||
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() | ||||
for t in s.split('\0'): | ||||
|
r25344 | try: | ||
# fast path for integer revision | ||||
r = int(t) | ||||
if str(r) != t or r not in cl: | ||||
raise ValueError | ||||
|
r26143 | revs = [r] | ||
|
r25344 | except ValueError: | ||
|
r26143 | revs = stringset(repo, subset, t) | ||
for r in revs: | ||||
if r in seen: | ||||
continue | ||||
if (r in subset | ||||
or r == node.nullrev and isinstance(subset, fullreposet)): | ||||
ls.append(r) | ||||
seen.add(r) | ||||
|
r25341 | return baseset(ls) | ||
|
r15898 | |||
|
r20566 | # for internal use | ||
|
r27587 | @predicate('_intlist', safe=True) | ||
|
r20566 | def _intlist(repo, subset, x): | ||
s = getstring(x, "internal error") | ||||
if not s: | ||||
|
r22802 | return baseset() | ||
|
r20566 | ls = [int(r) for r in s.split('\0')] | ||
|
r22876 | s = subset | ||
|
r20566 | return baseset([r for r in ls if r in s]) | ||
|
r20569 | # for internal use | ||
|
r27587 | @predicate('_hexlist', safe=True) | ||
|
r20569 | def _hexlist(repo, subset, x): | ||
s = getstring(x, "internal error") | ||||
if not s: | ||||
|
r22802 | return baseset() | ||
|
r20569 | cl = repo.changelog | ||
ls = [cl.rev(node.bin(r)) for r in s.split('\0')] | ||||
|
r22877 | s = subset | ||
|
r20569 | return baseset([r for r in ls if r in s]) | ||
|
r15898 | |||
|
r11275 | methods = { | ||
"range": rangeset, | ||||
|
r16860 | "dagrange": dagrange, | ||
|
r11275 | "string": stringset, | ||
|
r24932 | "symbol": stringset, | ||
|
r11275 | "and": andset, | ||
"or": orset, | ||||
"not": notset, | ||||
|
r28217 | "difference": differenceset, | ||
|
r11275 | "list": listset, | ||
|
r25704 | "keyvalue": keyvaluepair, | ||
|
r11275 | "func": func, | ||
|
r14070 | "ancestor": ancestorspec, | ||
"parent": parentspec, | ||||
"parentpost": p1, | ||||
|
r11275 | } | ||
|
r11279 | def optimize(x, small): | ||
|
r13031 | if x is None: | ||
|
r11279 | return 0, x | ||
|
r11275 | smallbonus = 1 | ||
if small: | ||||
smallbonus = .5 | ||||
op = x[0] | ||||
|
r11283 | if op == 'minus': | ||
|
r11279 | return optimize(('and', x[1], ('not', x[2])), small) | ||
|
r23765 | elif op == 'only': | ||
return optimize(('func', ('symbol', 'only'), | ||||
('list', x[1], x[2])), small) | ||||
|
r25094 | elif op == 'onlypost': | ||
return optimize(('func', ('symbol', 'only'), x[1]), small) | ||||
|
r11279 | elif op == 'dagrangepre': | ||
return optimize(('func', ('symbol', 'ancestors'), x[1]), small) | ||||
elif op == 'dagrangepost': | ||||
return optimize(('func', ('symbol', 'descendants'), x[1]), small) | ||||
|
r25819 | elif op == 'rangeall': | ||
return optimize(('range', ('string', '0'), ('string', 'tip')), small) | ||||
|
r11279 | elif op == 'rangepre': | ||
return optimize(('range', ('string', '0'), x[1]), small) | ||||
elif op == 'rangepost': | ||||
return optimize(('range', x[1], ('string', 'tip')), small) | ||||
|
r11467 | elif op == 'negate': | ||
return optimize(('string', | ||||
'-' + getstring(x[1], _("can't negate that"))), small) | ||||
|
r11279 | elif op in 'string symbol negate': | ||
return smallbonus, x # single revisions are small | ||||
|
r16859 | elif op == 'and': | ||
|
r11279 | wa, ta = optimize(x[1], True) | ||
wb, tb = optimize(x[2], True) | ||||
|
r20499 | |||
# (::x and not ::y)/(not ::y and ::x) have a fast path | ||||
|
r21893 | def isonly(revs, bases): | ||
|
r20499 | return ( | ||
|
r25995 | revs is not None | ||
and revs[0] == 'func' | ||||
|
r20499 | and getstring(revs[1], _('not a symbol')) == 'ancestors' | ||
|
r25995 | and bases is not None | ||
|
r20499 | and bases[0] == 'not' | ||
and bases[1][0] == 'func' | ||||
and getstring(bases[1][1], _('not a symbol')) == 'ancestors') | ||||
|
r11279 | w = min(wa, wb) | ||
|
r21893 | if isonly(ta, tb): | ||
return w, ('func', ('symbol', 'only'), ('list', ta[2], tb[1][2])) | ||||
if isonly(tb, ta): | ||||
return w, ('func', ('symbol', 'only'), ('list', tb[2], ta[1][2])) | ||||
|
r20499 | |||
|
r28217 | if tb is not None and tb[0] == 'not': | ||
return wa, ('difference', ta, tb[1]) | ||||
|
r11279 | if wa > wb: | ||
return w, (op, tb, ta) | ||||
return w, (op, ta, tb) | ||||
elif op == 'or': | ||||
|
r25343 | # fast path for machine-generated expression, that is likely to have | ||
# lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()' | ||||
ws, ts, ss = [], [], [] | ||||
def flushss(): | ||||
if not ss: | ||||
return | ||||
if len(ss) == 1: | ||||
w, t = ss[0] | ||||
else: | ||||
s = '\0'.join(t[1] for w, t in ss) | ||||
y = ('func', ('symbol', '_list'), ('string', s)) | ||||
w, t = optimize(y, False) | ||||
ws.append(w) | ||||
ts.append(t) | ||||
del ss[:] | ||||
for y in x[1:]: | ||||
w, t = optimize(y, False) | ||||
|
r25996 | if t is not None and (t[0] == 'string' or t[0] == 'symbol'): | ||
|
r25343 | ss.append((w, t)) | ||
continue | ||||
flushss() | ||||
ws.append(w) | ||||
ts.append(t) | ||||
flushss() | ||||
if len(ts) == 1: | ||||
return ws[0], ts[0] # 'or' operation is fully optimized out | ||||
|
r25307 | # we can't reorder trees by weight because it would change the order. | ||
# ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a") | ||||
|
r25309 | # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0])) | ||
|
r25343 | return max(ws), (op,) + tuple(ts) | ||
|
r11275 | elif op == 'not': | ||
|
r25191 | # Optimize not public() to _notpublic() because we have a fast version | ||
if x[1] == ('func', ('symbol', 'public'), None): | ||||
|
r27637 | newsym = ('func', ('symbol', '_notpublic'), None) | ||
|
r25191 | o = optimize(newsym, not small) | ||
return o[0], o[1] | ||||
else: | ||||
o = optimize(x[1], not small) | ||||
return o[0], (op, o[1]) | ||||
|
r14070 | elif op == 'parentpost': | ||
o = optimize(x[1], small) | ||||
return o[0], (op, o[1]) | ||||
|
r11275 | elif op == 'group': | ||
|
r11279 | return optimize(x[1], small) | ||
|
r27987 | elif op in 'dagrange range parent ancestorspec': | ||
|
r14842 | if op == 'parent': | ||
# x^:y means (x^) : y, not x ^ (:y) | ||||
post = ('parentpost', x[1]) | ||||
if x[2][0] == 'dagrangepre': | ||||
return optimize(('dagrange', post, x[2][1]), small) | ||||
elif x[2][0] == 'rangepre': | ||||
return optimize(('range', post, x[2][1]), small) | ||||
|
r11279 | wa, ta = optimize(x[1], small) | ||
wb, tb = optimize(x[2], small) | ||||
return wa + wb, (op, ta, tb) | ||||
|
r27987 | elif op == 'list': | ||
ws, ts = zip(*(optimize(y, small) for y in x[1:])) | ||||
return sum(ws), (op,) + ts | ||||
|
r11275 | elif op == 'func': | ||
|
r11383 | f = getstring(x[1], _("not a symbol")) | ||
|
r11279 | wa, ta = optimize(x[2], small) | ||
|
r14650 | if f in ("author branch closed date desc file grep keyword " | ||
"outgoing user"): | ||||
|
r11279 | w = 10 # slow | ||
|
r12351 | elif f in "modifies adds removes": | ||
|
r11279 | w = 30 # slower | ||
|
r11275 | elif f == "contains": | ||
|
r11279 | w = 100 # very slow | ||
|
r11275 | elif f == "ancestor": | ||
|
r11279 | w = 1 * smallbonus | ||
|
r22451 | elif f in "reverse limit first _intlist": | ||
|
r11279 | w = 0 | ||
|
r11275 | elif f in "sort": | ||
|
r11279 | w = 10 # assume most sorts look at changelog | ||
|
r11275 | else: | ||
|
r11279 | w = 1 | ||
return w + wa, (op, x[1], ta) | ||||
return 1, x | ||||
|
r11275 | |||
|
r23845 | # the set of valid characters for the initial letter of symbols in | ||
# alias declarations and definitions | ||||
_aliassyminitletters = set(c for c in [chr(i) for i in xrange(256)] | ||||
if c.isalnum() or c in '._@$' or ord(c) > 127) | ||||
def _tokenizealias(program, lookup=None): | ||||
"""Parse alias declaration/definition into a stream of tokens | ||||
This allows symbol names to use also ``$`` as an initial letter | ||||
(for backward compatibility), and callers of this function should | ||||
examine whether ``$`` is used also for unexpected symbols or not. | ||||
""" | ||||
return tokenize(program, lookup=lookup, | ||||
syminitletters=_aliassyminitletters) | ||||
|
r28874 | def _parsealias(spec): | ||
"""Parse alias declaration/definition ``spec`` | ||||
>>> _parsealias('foo($1)') | ||||
|
r28871 | ('func', ('symbol', 'foo'), ('symbol', '$1')) | ||
|
r28874 | >>> _parsealias('foo bar') | ||
|
r28871 | Traceback (most recent call last): | ||
... | ||||
ParseError: ('invalid token', 4) | ||||
|
r23845 | """ | ||
|
r25654 | p = parser.parser(elements) | ||
|
r28874 | tree, pos = p.parse(_tokenizealias(spec)) | ||
if pos != len(spec): | ||||
|
r23993 | raise error.ParseError(_('invalid token'), pos) | ||
|
r28873 | return parser.simplifyinfixops(tree, ('list', 'or')) | ||
|
r23993 | |||
|
r28870 | class _aliasrules(parser.basealiasrules): | ||
"""Parsing and expansion rule set of revset aliases""" | ||||
_section = _('revset alias') | ||||
|
r28875 | _parse = staticmethod(_parsealias) | ||
|
r28870 | _getlist = staticmethod(getlist) | ||
|
r14098 | class revsetalias(object): | ||
|
r23725 | # whether own `error` information is already shown or not. | ||
# this avoids showing same warning multiple times at each `findaliases`. | ||||
warned = False | ||||
|
r14723 | def __init__(self, name, value): | ||
|
r14098 | '''Aliases like: | ||
h = heads(default) | ||||
b($1) = ancestors($1) - ancestors(default) | ||||
''' | ||||
|
r28871 | r = _aliasrules._builddecl(name) | ||
self.name, self.tree, self.args, self.error = r | ||||
|
r23846 | if self.error: | ||
self.error = _('failed to parse the declaration of revset alias' | ||||
' "%s": %s') % (self.name, self.error) | ||||
return | ||||
|
r23725 | try: | ||
|
r28873 | self.replacement = _aliasrules._builddefn(value, self.args) | ||
|
r25660 | except error.ParseError as inst: | ||
|
r23844 | self.error = _('failed to parse the definition of revset alias' | ||
|
r28720 | ' "%s": %s') % (self.name, | ||
parser.parseerrordetail(inst)) | ||||
|
r14098 | |||
|
r16096 | def _getalias(aliases, tree): | ||
"""If tree looks like an unexpanded alias, return it. Return None | ||||
otherwise. | ||||
""" | ||||
|
r28705 | if isinstance(tree, tuple): | ||
if tree[0] == 'symbol': | ||||
|
r16096 | name = tree[1] | ||
alias = aliases.get(name) | ||||
if alias and alias.args is None and alias.tree == tree: | ||||
return alias | ||||
|
r28705 | if tree[0] == 'func': | ||
if tree[1][0] == 'symbol': | ||||
|
r16096 | name = tree[1][1] | ||
alias = aliases.get(name) | ||||
if alias and alias.args is not None and alias.tree == tree[:2]: | ||||
return alias | ||||
return None | ||||
|
r14098 | |||
|
r16096 | def _expandargs(tree, args): | ||
|
r16771 | """Replace _aliasarg instances with the substitution value of the | ||
same name in args, recursively. | ||||
|
r16096 | """ | ||
|
r28705 | if not isinstance(tree, tuple): | ||
|
r16096 | return tree | ||
|
r28691 | if tree[0] == '_aliasarg': | ||
sym = tree[1] | ||||
return args[sym] | ||||
|
r16096 | return tuple(_expandargs(t, args) for t in tree) | ||
|
r16838 | def _expandaliases(aliases, tree, expanding, cache): | ||
|
r16096 | """Expand aliases in tree, recursively. | ||
'aliases' is a dictionary mapping user defined aliases to | ||||
revsetalias objects. | ||||
""" | ||||
if not isinstance(tree, tuple): | ||||
# Do not expand raw strings | ||||
|
r14098 | return tree | ||
|
r16096 | alias = _getalias(aliases, tree) | ||
if alias is not None: | ||||
|
r23725 | if alias.error: | ||
|
r26587 | raise error.Abort(alias.error) | ||
|
r16096 | if alias in expanding: | ||
raise error.ParseError(_('infinite expansion of revset alias "%s" ' | ||||
'detected') % alias.name) | ||||
expanding.append(alias) | ||||
|
r16838 | if alias.name not in cache: | ||
cache[alias.name] = _expandaliases(aliases, alias.replacement, | ||||
expanding, cache) | ||||
result = cache[alias.name] | ||||
|
r16772 | expanding.pop() | ||
|
r16096 | if alias.args is not None: | ||
l = getlist(tree[2]) | ||||
if len(l) != len(alias.args): | ||||
raise error.ParseError( | ||||
|
r26778 | _('invalid number of arguments: %d') % len(l)) | ||
|
r16838 | l = [_expandaliases(aliases, a, [], cache) for a in l] | ||
|
r16096 | result = _expandargs(result, dict(zip(alias.args, l))) | ||
else: | ||||
|
r16838 | result = tuple(_expandaliases(aliases, t, expanding, cache) | ||
|
r16096 | for t in tree) | ||
return result | ||||
|
r14098 | |||
|
r23725 | def findaliases(ui, tree, showwarning=None): | ||
|
r16096 | aliases = {} | ||
|
r14098 | for k, v in ui.configitems('revsetalias'): | ||
alias = revsetalias(k, v) | ||||
|
r16096 | aliases[alias.name] = alias | ||
|
r23725 | tree = _expandaliases(aliases, tree, [], {}) | ||
if showwarning: | ||||
# warn about problematic (but not referred) aliases | ||||
for name, alias in sorted(aliases.iteritems()): | ||||
if alias.error and not alias.warned: | ||||
|
r23844 | showwarning(_('warning: %s\n') % (alias.error)) | ||
|
r23725 | alias.warned = True | ||
return tree | ||||
|
r14098 | |||
|
r23742 | def foldconcat(tree): | ||
"""Fold elements to be concatenated by `##` | ||||
""" | ||||
if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'): | ||||
return tree | ||||
if tree[0] == '_concat': | ||||
pending = [tree] | ||||
l = [] | ||||
while pending: | ||||
e = pending.pop() | ||||
if e[0] == '_concat': | ||||
pending.extend(reversed(e[1:])) | ||||
elif e[0] in ('string', 'symbol'): | ||||
l.append(e[1]) | ||||
else: | ||||
msg = _("\"##\" can't concatenate \"%s\" element") % (e[0]) | ||||
raise error.ParseError(msg) | ||||
return ('string', ''.join(l)) | ||||
else: | ||||
return tuple(foldconcat(t) for t in tree) | ||||
|
r14098 | |||
|
r20779 | def parse(spec, lookup=None): | ||
|
r25654 | p = parser.parser(elements) | ||
tree, pos = p.parse(tokenize(spec, lookup=lookup)) | ||||
|
r25251 | if pos != len(spec): | ||
raise error.ParseError(_("invalid token"), pos) | ||||
|
r27987 | return parser.simplifyinfixops(tree, ('list', 'or')) | ||
|
r20779 | |||
|
r24518 | def posttreebuilthook(tree, repo): | ||
# hook for extensions to execute code on the optimized tree | ||||
pass | ||||
|
r20779 | def match(ui, spec, repo=None): | ||
|
r11385 | if not spec: | ||
raise error.ParseError(_("empty query")) | ||||
|
r20779 | lookup = None | ||
if repo: | ||||
lookup = repo.__contains__ | ||||
|
r25251 | tree = parse(spec, lookup) | ||
|
r25926 | return _makematcher(ui, tree, repo) | ||
|
r25927 | def matchany(ui, specs, repo=None): | ||
"""Create a matcher that will include any revisions matching one of the | ||||
given specs""" | ||||
if not specs: | ||||
def mfunc(repo, subset=None): | ||||
return baseset() | ||||
return mfunc | ||||
if not all(specs): | ||||
raise error.ParseError(_("empty query")) | ||||
lookup = None | ||||
if repo: | ||||
lookup = repo.__contains__ | ||||
if len(specs) == 1: | ||||
tree = parse(specs[0], lookup) | ||||
else: | ||||
tree = ('or',) + tuple(parse(s, lookup) for s in specs) | ||||
return _makematcher(ui, tree, repo) | ||||
|
r25926 | def _makematcher(ui, tree, repo): | ||
|
r14900 | if ui: | ||
|
r23725 | tree = findaliases(ui, tree, showwarning=ui.warn) | ||
|
r23742 | tree = foldconcat(tree) | ||
|
r11279 | weight, tree = optimize(tree, True) | ||
|
r24518 | posttreebuilthook(tree, repo) | ||
|
r24114 | def mfunc(repo, subset=None): | ||
if subset is None: | ||||
|
r24115 | subset = fullreposet(repo) | ||
|
r22885 | if util.safehasattr(subset, 'isascending'): | ||
|
r22686 | result = getset(repo, subset, tree) | ||
else: | ||||
result = getset(repo, baseset(subset), tree) | ||||
return result | ||||
|
r11275 | return mfunc | ||
|
r12821 | |||
|
r14901 | def formatspec(expr, *args): | ||
''' | ||||
This is a convenience function for using revsets internally, and | ||||
escapes arguments appropriately. Aliases are intentionally ignored | ||||
so that intended expression behavior isn't accidentally subverted. | ||||
Supported arguments: | ||||
|
r15266 | %r = revset expression, parenthesized | ||
|
r14901 | %d = int(arg), no quoting | ||
%s = string(arg), escaped and single-quoted | ||||
%b = arg.branch(), escaped and single-quoted | ||||
%n = hex(arg), single-quoted | ||||
%% = a literal '%' | ||||
|
r15266 | Prefixing the type with 'l' specifies a parenthesized list of that type. | ||
|
r15140 | |||
|
r15268 | >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()")) | ||
'(10 or 11):: and ((this()) or (that()))' | ||||
|
r14901 | >>> formatspec('%d:: and not %d::', 10, 20) | ||
'10:: and not 20::' | ||||
|
r15325 | >>> formatspec('%ld or %ld', [], [1]) | ||
|
r15898 | "_list('') or 1" | ||
|
r14901 | >>> formatspec('keyword(%s)', 'foo\\xe9') | ||
"keyword('foo\\\\xe9')" | ||||
>>> b = lambda: 'default' | ||||
>>> b.branch = b | ||||
>>> formatspec('branch(%b)', b) | ||||
"branch('default')" | ||||
|
r15140 | >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd']) | ||
|
r15898 | "root(_list('a\\x00b\\x00c\\x00d'))" | ||
|
r14901 | ''' | ||
def quote(s): | ||||
return repr(str(s)) | ||||
|
r15140 | def argtype(c, arg): | ||
if c == 'd': | ||||
return str(int(arg)) | ||||
elif c == 's': | ||||
return quote(arg) | ||||
|
r15266 | elif c == 'r': | ||
parse(arg) # make sure syntax errors are confined | ||||
return '(%s)' % arg | ||||
|
r15140 | elif c == 'n': | ||
|
r16417 | return quote(node.hex(arg)) | ||
|
r15140 | elif c == 'b': | ||
return quote(arg.branch()) | ||||
|
r15595 | def listexp(s, t): | ||
l = len(s) | ||||
if l == 0: | ||||
|
r15898 | return "_list('')" | ||
elif l == 1: | ||||
|
r15595 | return argtype(t, s[0]) | ||
|
r15898 | elif t == 'd': | ||
|
r20566 | return "_intlist('%s')" % "\0".join(str(int(a)) for a in s) | ||
|
r15898 | elif t == 's': | ||
return "_list('%s')" % "\0".join(s) | ||||
elif t == 'n': | ||||
|
r20569 | return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s) | ||
|
r15898 | elif t == 'b': | ||
return "_list('%s')" % "\0".join(a.branch() for a in s) | ||||
|
r15791 | m = l // 2 | ||
|
r15595 | return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t)) | ||
|
r14901 | ret = '' | ||
pos = 0 | ||||
arg = 0 | ||||
while pos < len(expr): | ||||
c = expr[pos] | ||||
if c == '%': | ||||
pos += 1 | ||||
d = expr[pos] | ||||
if d == '%': | ||||
ret += d | ||||
|
r15268 | elif d in 'dsnbr': | ||
|
r15140 | ret += argtype(d, args[arg]) | ||
|
r14901 | arg += 1 | ||
|
r15140 | elif d == 'l': | ||
# a list of some type | ||||
pos += 1 | ||||
d = expr[pos] | ||||
|
r15596 | ret += listexp(list(args[arg]), d) | ||
|
r14901 | arg += 1 | ||
else: | ||||
|
r26587 | raise error.Abort('unexpected revspec format character %s' % d) | ||
|
r14901 | else: | ||
ret += c | ||||
pos += 1 | ||||
return ret | ||||
|
r16218 | def prettyformat(tree): | ||
|
r25253 | return parser.prettyformat(tree, ('string', 'symbol')) | ||
|
r16218 | |||
|
r19719 | def depth(tree): | ||
if isinstance(tree, tuple): | ||||
return max(map(depth, tree)) + 1 | ||||
else: | ||||
return 0 | ||||
|
r19720 | def funcsused(tree): | ||
if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'): | ||||
return set() | ||||
else: | ||||
funcs = set() | ||||
for s in tree[1:]: | ||||
funcs |= funcsused(s) | ||||
if tree[0] == 'func': | ||||
funcs.add(tree[1][1]) | ||||
return funcs | ||||
|
r28423 | def _formatsetrepr(r): | ||
"""Format an optional printable representation of a set | ||||
======== ================================= | ||||
type(r) example | ||||
======== ================================= | ||||
tuple ('<not %r>', other) | ||||
str '<branch closed>' | ||||
callable lambda: '<branch %r>' % sorted(b) | ||||
object other | ||||
======== ================================= | ||||
""" | ||||
if r is None: | ||||
return '' | ||||
elif isinstance(r, tuple): | ||||
return r[0] % r[1:] | ||||
elif isinstance(r, str): | ||||
return r | ||||
elif callable(r): | ||||
return r() | ||||
else: | ||||
return repr(r) | ||||
|
r22692 | class abstractsmartset(object): | ||
def __nonzero__(self): | ||||
"""True if the smartset is not empty""" | ||||
raise NotImplementedError() | ||||
def __contains__(self, rev): | ||||
"""provide fast membership testing""" | ||||
raise NotImplementedError() | ||||
def __iter__(self): | ||||
"""iterate the set in the order it is supposed to be iterated""" | ||||
raise NotImplementedError() | ||||
|
r22716 | # Attributes containing a function to perform a fast iteration in a given | ||
# direction. A smartset can have none, one, or both defined. | ||||
# | ||||
# Default value is None instead of a function returning None to avoid | ||||
# initializing an iterator just for testing if a fast method exists. | ||||
fastasc = None | ||||
fastdesc = None | ||||
|
r22692 | def isascending(self): | ||
"""True if the set will iterate in ascending order""" | ||||
raise NotImplementedError() | ||||
def isdescending(self): | ||||
"""True if the set will iterate in descending order""" | ||||
raise NotImplementedError() | ||||
|
r26099 | @util.cachefunc | ||
|
r22692 | def min(self): | ||
"""return the minimum element in the set""" | ||||
|
r22722 | if self.fastasc is not None: | ||
for r in self.fastasc(): | ||||
return r | ||||
raise ValueError('arg is an empty sequence') | ||||
return min(self) | ||||
|
r22692 | |||
|
r26099 | @util.cachefunc | ||
|
r22692 | def max(self): | ||
"""return the maximum element in the set""" | ||||
|
r22722 | if self.fastdesc is not None: | ||
for r in self.fastdesc(): | ||||
return r | ||||
raise ValueError('arg is an empty sequence') | ||||
return max(self) | ||||
|
r22692 | |||
|
r22808 | def first(self): | ||
"""return the first element in the set (user iteration perspective) | ||||
Return None if the set is empty""" | ||||
raise NotImplementedError() | ||||
def last(self): | ||||
"""return the last element in the set (user iteration perspective) | ||||
Return None if the set is empty""" | ||||
raise NotImplementedError() | ||||
|
r22995 | def __len__(self): | ||
"""return the length of the smartsets | ||||
This can be expensive on smartset that could be lazy otherwise.""" | ||||
raise NotImplementedError() | ||||
|
r22692 | def reverse(self): | ||
"""reverse the expected iteration order""" | ||||
raise NotImplementedError() | ||||
def sort(self, reverse=True): | ||||
"""get the set to iterate in an ascending or descending order""" | ||||
raise NotImplementedError() | ||||
def __and__(self, other): | ||||
"""Returns a new object with the intersection of the two collections. | ||||
This is part of the mandatory API for smartset.""" | ||||
|
r24459 | if isinstance(other, fullreposet): | ||
return self | ||||
|
r28423 | return self.filter(other.__contains__, condrepr=other, cache=False) | ||
|
r22692 | |||
def __add__(self, other): | ||||
"""Returns a new object with the union of the two collections. | ||||
This is part of the mandatory API for smartset.""" | ||||
|
r22861 | return addset(self, other) | ||
|
r22692 | |||
def __sub__(self, other): | ||||
"""Returns a new object with the substraction of the two collections. | ||||
This is part of the mandatory API for smartset.""" | ||||
|
r22730 | c = other.__contains__ | ||
|
r28423 | return self.filter(lambda r: not c(r), condrepr=('<not %r>', other), | ||
cache=False) | ||||
def filter(self, condition, condrepr=None, cache=True): | ||||
|
r22692 | """Returns this smartset filtered by condition as a new smartset. | ||
`condition` is a callable which takes a revision number and returns a | ||||
|
r28423 | boolean. Optional `condrepr` provides a printable representation of | ||
the given `condition`. | ||||
|
r22692 | |||
This is part of the mandatory API for smartset.""" | ||||
|
r22864 | # builtin cannot be cached. but do not needs to | ||
if cache and util.safehasattr(condition, 'func_code'): | ||||
condition = util.cachefunc(condition) | ||||
|
r28423 | return filteredset(self, condition, condrepr) | ||
|
r22692 | |||
|
r22825 | class baseset(abstractsmartset): | ||
|
r20416 | """Basic data structure that represents a revset and contains the basic | ||
operation that it should be able to perform. | ||||
|
r20727 | |||
Every method in this class should be implemented by any smartset class. | ||||
|
r20416 | """ | ||
|
r28425 | def __init__(self, data=(), datarepr=None): | ||
""" | ||||
datarepr: a tuple of (format, obj, ...), a function or an object that | ||||
provides a printable representation of the given data. | ||||
""" | ||||
|
r28786 | self._ascending = None | ||
|
r22825 | if not isinstance(data, list): | ||
|
r26060 | if isinstance(data, set): | ||
self._set = data | ||||
|
r28786 | # set has no order we pick one for stability purpose | ||
self._ascending = True | ||||
|
r22825 | data = list(data) | ||
self._list = data | ||||
|
r28425 | self._datarepr = datarepr | ||
|
r20365 | |||
|
r22826 | @util.propertycache | ||
|
r22879 | def _set(self): | ||
return set(self._list) | ||||
@util.propertycache | ||||
|
r22826 | def _asclist(self): | ||
asclist = self._list[:] | ||||
asclist.sort() | ||||
return asclist | ||||
|
r22827 | def __iter__(self): | ||
if self._ascending is None: | ||||
return iter(self._list) | ||||
elif self._ascending: | ||||
return iter(self._asclist) | ||||
else: | ||||
return reversed(self._asclist) | ||||
|
r22826 | def fastasc(self): | ||
return iter(self._asclist) | ||||
def fastdesc(self): | ||||
return reversed(self._asclist) | ||||
|
r22503 | @util.propertycache | ||
def __contains__(self): | ||||
|
r22880 | return self._set.__contains__ | ||
|
r22503 | |||
|
r22691 | def __nonzero__(self): | ||
|
r22825 | return bool(self._list) | ||
def sort(self, reverse=False): | ||||
|
r22829 | self._ascending = not bool(reverse) | ||
|
r22825 | |||
def reverse(self): | ||||
|
r22829 | if self._ascending is None: | ||
self._list.reverse() | ||||
else: | ||||
self._ascending = not self._ascending | ||||
|
r22825 | |||
def __len__(self): | ||||
return len(self._list) | ||||
|
r22691 | |||
|
r20725 | def isascending(self): | ||
|
r20727 | """Returns True if the collection is ascending order, False if not. | ||
This is part of the mandatory API for smartset.""" | ||||
|
r22863 | if len(self) <= 1: | ||
return True | ||||
|
r22828 | return self._ascending is not None and self._ascending | ||
|
r20725 | |||
def isdescending(self): | ||||
|
r20727 | """Returns True if the collection is descending order, False if not. | ||
This is part of the mandatory API for smartset.""" | ||||
|
r22863 | if len(self) <= 1: | ||
return True | ||||
|
r22828 | return self._ascending is not None and not self._ascending | ||
|
r20725 | |||
|
r22812 | def first(self): | ||
if self: | ||||
|
r22829 | if self._ascending is None: | ||
return self._list[0] | ||||
elif self._ascending: | ||||
return self._asclist[0] | ||||
else: | ||||
return self._asclist[-1] | ||||
|
r22812 | return None | ||
def last(self): | ||||
if self: | ||||
|
r22829 | if self._ascending is None: | ||
return self._list[-1] | ||||
elif self._ascending: | ||||
return self._asclist[-1] | ||||
else: | ||||
return self._asclist[0] | ||||
|
r22812 | return None | ||
|
r24457 | def __repr__(self): | ||
d = {None: '', False: '-', True: '+'}[self._ascending] | ||||
|
r28425 | s = _formatsetrepr(self._datarepr) | ||
if not s: | ||||
|
r28785 | l = self._list | ||
# if _list has been built from a set, it might have a different | ||||
# order from one python implementation to another. | ||||
# We fallback to the sorted version for a stable output. | ||||
if self._ascending is not None: | ||||
l = self._asclist | ||||
s = repr(l) | ||||
|
r28425 | return '<%s%s %s>' % (type(self).__name__, d, s) | ||
|
r24457 | |||
|
r22726 | class filteredset(abstractsmartset): | ||
|
r20427 | """Duck type for baseset class which iterates lazily over the revisions in | ||
the subset and contains a function which tests for membership in the | ||||
revset | ||||
""" | ||||
|
r28423 | def __init__(self, subset, condition=lambda x: True, condrepr=None): | ||
|
r20738 | """ | ||
condition: a function that decide whether a revision in the subset | ||||
belongs to the revset or not. | ||||
|
r28423 | condrepr: a tuple of (format, obj, ...), a function or an object that | ||
provides a printable representation of the given condition. | ||||
|
r20738 | """ | ||
|
r20427 | self._subset = subset | ||
self._condition = condition | ||||
|
r28423 | self._condrepr = condrepr | ||
|
r20427 | |||
def __contains__(self, x): | ||||
|
r26212 | return x in self._subset and self._condition(x) | ||
|
r20427 | |||
def __iter__(self): | ||||
|
r22719 | return self._iterfilter(self._subset) | ||
def _iterfilter(self, it): | ||||
|
r20427 | cond = self._condition | ||
|
r22719 | for x in it: | ||
|
r20427 | if cond(x): | ||
yield x | ||||
|
r22720 | @property | ||
def fastasc(self): | ||||
it = self._subset.fastasc | ||||
if it is None: | ||||
return None | ||||
return lambda: self._iterfilter(it()) | ||||
@property | ||||
def fastdesc(self): | ||||
it = self._subset.fastdesc | ||||
if it is None: | ||||
return None | ||||
return lambda: self._iterfilter(it()) | ||||
|
r20552 | def __nonzero__(self): | ||
|
r26307 | fast = self.fastasc | ||
if fast is None: | ||||
fast = self.fastdesc | ||||
if fast is not None: | ||||
|
r26306 | it = fast() | ||
|
r26307 | else: | ||
it = self | ||||
|
r26306 | |||
for r in it: | ||||
|
r20552 | return True | ||
return False | ||||
|
r20429 | def __len__(self): | ||
# Basic implementation to be changed in future patches. | ||||
|
r28718 | # until this gets improved, we use generator expression | ||
# here, since list compr is free to call __len__ again | ||||
# causing infinite recursion | ||||
l = baseset(r for r in self) | ||||
|
r20429 | return len(l) | ||
def sort(self, reverse=False): | ||||
|
r22862 | self._subset.sort(reverse=reverse) | ||
|
r20429 | |||
def reverse(self): | ||||
self._subset.reverse() | ||||
|
r20725 | def isascending(self): | ||
|
r22862 | return self._subset.isascending() | ||
|
r20725 | |||
def isdescending(self): | ||||
|
r22862 | return self._subset.isdescending() | ||
|
r20725 | |||
|
r22813 | def first(self): | ||
for x in self: | ||||
return x | ||||
return None | ||||
def last(self): | ||||
it = None | ||||
|
r25648 | if self.isascending(): | ||
|
r22862 | it = self.fastdesc | ||
|
r25648 | elif self.isdescending(): | ||
it = self.fastasc | ||||
if it is not None: | ||||
for x in it(): | ||||
return x | ||||
return None #empty case | ||||
else: | ||||
x = None | ||||
for x in self: | ||||
pass | ||||
|
r22813 | return x | ||
|
r24457 | def __repr__(self): | ||
|
r28423 | xs = [repr(self._subset)] | ||
s = _formatsetrepr(self._condrepr) | ||||
if s: | ||||
xs.append(s) | ||||
return '<%s %s>' % (type(self).__name__, ', '.join(xs)) | ||||
|
r24457 | |||
|
r25131 | def _iterordered(ascending, iter1, iter2): | ||
"""produce an ordered iteration from two iterators with the same order | ||||
The ascending is used to indicated the iteration direction. | ||||
""" | ||||
choice = max | ||||
if ascending: | ||||
choice = min | ||||
val1 = None | ||||
val2 = None | ||||
try: | ||||
# Consume both iterators in an ordered way until one is empty | ||||
while True: | ||||
if val1 is None: | ||||
val1 = iter1.next() | ||||
if val2 is None: | ||||
val2 = iter2.next() | ||||
next = choice(val1, val2) | ||||
yield next | ||||
if val1 == next: | ||||
val1 = None | ||||
if val2 == next: | ||||
val2 = None | ||||
except StopIteration: | ||||
# Flush any remaining values and consume the other one | ||||
it = iter2 | ||||
if val1 is not None: | ||||
yield val1 | ||||
it = iter1 | ||||
elif val2 is not None: | ||||
# might have been equality and both are empty | ||||
yield val2 | ||||
for val in it: | ||||
yield val | ||||
|
r22793 | class addset(abstractsmartset): | ||
|
r20708 | """Represent the addition of two sets | ||
Wrapper structure for lazily adding two structures without losing much | ||||
|
r20694 | performance on the __contains__ method | ||
|
r20708 | |||
|
r20712 | If the ascending attribute is set, that means the two structures are | ||
ordered in either an ascending or descending way. Therefore, we can add | ||||
|
r21024 | them maintaining the order by iterating over both at the same time | ||
|
r25024 | |||
>>> xs = baseset([0, 3, 2]) | ||||
>>> ys = baseset([5, 2, 4]) | ||||
>>> rs = addset(xs, ys) | ||||
>>> bool(rs), 0 in rs, 1 in rs, 5 in rs, rs.first(), rs.last() | ||||
(True, True, False, True, 0, 4) | ||||
>>> rs = addset(xs, baseset([])) | ||||
>>> bool(rs), 0 in rs, 1 in rs, rs.first(), rs.last() | ||||
(True, True, False, 0, 2) | ||||
>>> rs = addset(baseset([]), baseset([])) | ||||
>>> bool(rs), 0 in rs, rs.first(), rs.last() | ||||
(False, False, None, None) | ||||
iterate unsorted: | ||||
>>> rs = addset(xs, ys) | ||||
|
r28717 | >>> # (use generator because pypy could call len()) | ||
>>> list(x for x in rs) # without _genlist | ||||
|
r25024 | [0, 3, 2, 5, 4] | ||
>>> assert not rs._genlist | ||||
>>> len(rs) | ||||
5 | ||||
>>> [x for x in rs] # with _genlist | ||||
[0, 3, 2, 5, 4] | ||||
>>> assert rs._genlist | ||||
iterate ascending: | ||||
>>> rs = addset(xs, ys, ascending=True) | ||||
|
r28717 | >>> # (use generator because pypy could call len()) | ||
>>> list(x for x in rs), list(x for x in rs.fastasc()) # without _asclist | ||||
|
r25024 | ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5]) | ||
>>> assert not rs._asclist | ||||
|
r25115 | >>> len(rs) | ||
5 | ||||
>>> [x for x in rs], [x for x in rs.fastasc()] | ||||
([0, 2, 3, 4, 5], [0, 2, 3, 4, 5]) | ||||
|
r25024 | >>> assert rs._asclist | ||
iterate descending: | ||||
>>> rs = addset(xs, ys, ascending=False) | ||||
|
r28717 | >>> # (use generator because pypy could call len()) | ||
>>> list(x for x in rs), list(x for x in rs.fastdesc()) # without _asclist | ||||
|
r25024 | ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0]) | ||
>>> assert not rs._asclist | ||||
|
r25115 | >>> len(rs) | ||
5 | ||||
>>> [x for x in rs], [x for x in rs.fastdesc()] | ||||
([5, 4, 3, 2, 0], [5, 4, 3, 2, 0]) | ||||
|
r25024 | >>> assert rs._asclist | ||
iterate ascending without fastasc: | ||||
>>> rs = addset(xs, generatorset(ys), ascending=True) | ||||
>>> assert rs.fastasc is None | ||||
|
r25115 | >>> [x for x in rs] | ||
[0, 2, 3, 4, 5] | ||||
|
r25024 | |||
iterate descending without fastdesc: | ||||
>>> rs = addset(generatorset(xs), ys, ascending=False) | ||||
>>> assert rs.fastdesc is None | ||||
|
r25115 | >>> [x for x in rs] | ||
[5, 4, 3, 2, 0] | ||||
|
r20694 | """ | ||
|
r20712 | def __init__(self, revs1, revs2, ascending=None): | ||
|
r20694 | self._r1 = revs1 | ||
self._r2 = revs2 | ||||
self._iter = None | ||||
|
r20712 | self._ascending = ascending | ||
|
r20720 | self._genlist = None | ||
|
r22859 | self._asclist = None | ||
|
r20720 | |||
|
r20845 | def __len__(self): | ||
return len(self._list) | ||||
|
r22743 | def __nonzero__(self): | ||
|
r23100 | return bool(self._r1) or bool(self._r2) | ||
|
r22743 | |||
|
r20720 | @util.propertycache | ||
def _list(self): | ||||
if not self._genlist: | ||||
|
r25115 | self._genlist = baseset(iter(self)) | ||
|
r20720 | return self._genlist | ||
|
r20694 | |||
|
r25115 | def __iter__(self): | ||
|
r20722 | """Iterate over both collections without repeating elements | ||
If the ascending attribute is not set, iterate over the first one and | ||||
then over the second one checking for membership on the first one so we | ||||
dont yield any duplicates. | ||||
If the ascending attribute is set, iterate over both collections at the | ||||
same time, yielding only one value at a time in the given order. | ||||
""" | ||||
|
r22799 | if self._ascending is None: | ||
|
r25115 | if self._genlist: | ||
return iter(self._genlist) | ||||
def arbitraryordergen(): | ||||
|
r22799 | for r in self._r1: | ||
yield r | ||||
|
r22881 | inr1 = self._r1.__contains__ | ||
|
r22799 | for r in self._r2: | ||
|
r22881 | if not inr1(r): | ||
|
r20694 | yield r | ||
|
r25115 | return arbitraryordergen() | ||
# try to use our own fast iterator if it exists | ||||
|
r22859 | self._trysetasclist() | ||
if self._ascending: | ||||
|
r25130 | attr = 'fastasc' | ||
|
r22859 | else: | ||
|
r25130 | attr = 'fastdesc' | ||
it = getattr(self, attr) | ||||
|
r25115 | if it is not None: | ||
return it() | ||||
# maybe half of the component supports fast | ||||
# get iterator for _r1 | ||||
iter1 = getattr(self._r1, attr) | ||||
if iter1 is None: | ||||
# let's avoid side effect (not sure it matters) | ||||
iter1 = iter(sorted(self._r1, reverse=not self._ascending)) | ||||
else: | ||||
iter1 = iter1() | ||||
# get iterator for _r2 | ||||
iter2 = getattr(self._r2, attr) | ||||
if iter2 is None: | ||||
# let's avoid side effect (not sure it matters) | ||||
iter2 = iter(sorted(self._r2, reverse=not self._ascending)) | ||||
else: | ||||
iter2 = iter2() | ||||
|
r25131 | return _iterordered(self._ascending, iter1, iter2) | ||
|
r22859 | |||
def _trysetasclist(self): | ||||
|
r23139 | """populate the _asclist attribute if possible and necessary""" | ||
|
r22859 | if self._genlist is not None and self._asclist is None: | ||
self._asclist = sorted(self._genlist) | ||||
|
r20694 | |||
|
r22742 | @property | ||
def fastasc(self): | ||||
|
r22859 | self._trysetasclist() | ||
if self._asclist is not None: | ||||
return self._asclist.__iter__ | ||||
|
r22742 | iter1 = self._r1.fastasc | ||
iter2 = self._r2.fastasc | ||||
if None in (iter1, iter2): | ||||
return None | ||||
|
r25131 | return lambda: _iterordered(True, iter1(), iter2()) | ||
|
r22742 | |||
@property | ||||
def fastdesc(self): | ||||
|
r22859 | self._trysetasclist() | ||
if self._asclist is not None: | ||||
return self._asclist.__reversed__ | ||||
|
r22742 | iter1 = self._r1.fastdesc | ||
iter2 = self._r2.fastdesc | ||||
if None in (iter1, iter2): | ||||
return None | ||||
|
r25131 | return lambda: _iterordered(False, iter1(), iter2()) | ||
|
r22741 | |||
|
r20694 | def __contains__(self, x): | ||
return x in self._r1 or x in self._r2 | ||||
|
r20724 | def sort(self, reverse=False): | ||
"""Sort the added set | ||||
For this we use the cached list with all the generated values and if we | ||||
know they are ascending or descending we can sort them in a smart way. | ||||
""" | ||||
|
r22859 | self._ascending = not reverse | ||
|
r20724 | |||
|
r20733 | def isascending(self): | ||
return self._ascending is not None and self._ascending | ||||
def isdescending(self): | ||||
return self._ascending is not None and not self._ascending | ||||
|
r20723 | def reverse(self): | ||
|
r22859 | if self._ascending is None: | ||
self._list.reverse() | ||||
else: | ||||
|
r20723 | self._ascending = not self._ascending | ||
|
r22810 | def first(self): | ||
|
r23127 | for x in self: | ||
return x | ||||
|
r22810 | return None | ||
def last(self): | ||||
|
r23127 | self.reverse() | ||
val = self.first() | ||||
self.reverse() | ||||
return val | ||||
|
r22810 | |||
|
r24457 | def __repr__(self): | ||
d = {None: '', False: '-', True: '+'}[self._ascending] | ||||
return '<%s%s %r, %r>' % (type(self).__name__, d, self._r1, self._r2) | ||||
|
r22795 | class generatorset(abstractsmartset): | ||
|
r20705 | """Wrap a generator for lazy iteration | ||
Wrapper structure for generators that provides lazy membership and can | ||||
|
r20540 | be iterated more than once. | ||
When asked for membership it generates values until either it finds the | ||||
requested one or has gone through all the elements in the generator | ||||
""" | ||||
|
r22755 | def __init__(self, gen, iterasc=None): | ||
|
r20739 | """ | ||
gen: a generator producing the values for the generatorset. | ||||
""" | ||||
|
r20536 | self._gen = gen | ||
|
r22798 | self._asclist = None | ||
|
r20536 | self._cache = {} | ||
|
r22796 | self._genlist = [] | ||
|
r20703 | self._finished = False | ||
|
r22800 | self._ascending = True | ||
|
r22755 | if iterasc is not None: | ||
if iterasc: | ||||
|
r22797 | self.fastasc = self._iterator | ||
|
r22757 | self.__contains__ = self._asccontains | ||
|
r22755 | else: | ||
|
r22797 | self.fastdesc = self._iterator | ||
|
r22757 | self.__contains__ = self._desccontains | ||
|
r20540 | |||
|
r22739 | def __nonzero__(self): | ||
|
r24936 | # Do not use 'for r in self' because it will enforce the iteration | ||
# order (default ascending), possibly unrolling a whole descending | ||||
# iterator. | ||||
if self._genlist: | ||||
return True | ||||
for r in self._consumegen(): | ||||
|
r22739 | return True | ||
return False | ||||
|
r20536 | def __contains__(self, x): | ||
if x in self._cache: | ||||
return self._cache[x] | ||||
|
r20828 | # Use new values only, as existing values would be cached. | ||
for l in self._consumegen(): | ||||
|
r20634 | if l == x: | ||
return True | ||||
|
r20536 | |||
self._cache[x] = False | ||||
return False | ||||
|
r22757 | def _asccontains(self, x): | ||
"""version of contains optimised for ascending generator""" | ||||
if x in self._cache: | ||||
return self._cache[x] | ||||
# Use new values only, as existing values would be cached. | ||||
for l in self._consumegen(): | ||||
if l == x: | ||||
return True | ||||
if l > x: | ||||
break | ||||
self._cache[x] = False | ||||
return False | ||||
def _desccontains(self, x): | ||||
"""version of contains optimised for descending generator""" | ||||
if x in self._cache: | ||||
return self._cache[x] | ||||
# Use new values only, as existing values would be cached. | ||||
for l in self._consumegen(): | ||||
if l == x: | ||||
return True | ||||
if l < x: | ||||
break | ||||
self._cache[x] = False | ||||
return False | ||||
|
r20536 | def __iter__(self): | ||
|
r22800 | if self._ascending: | ||
it = self.fastasc | ||||
else: | ||||
it = self.fastdesc | ||||
if it is not None: | ||||
return it() | ||||
# we need to consume the iterator | ||||
for x in self._consumegen(): | ||||
pass | ||||
# recall the same code | ||||
return iter(self) | ||||
|
r22797 | |||
def _iterator(self): | ||||
|
r20833 | if self._finished: | ||
|
r22670 | return iter(self._genlist) | ||
|
r20833 | |||
|
r22494 | # We have to use this complex iteration strategy to allow multiple | ||
# iterations at the same time. We need to be able to catch revision | ||||
|
r23139 | # removed from _consumegen and added to genlist in another instance. | ||
|
r22494 | # | ||
# Getting rid of it would provide an about 15% speed up on this | ||||
# iteration. | ||||
|
r20833 | genlist = self._genlist | ||
|
r22669 | nextrev = self._consumegen().next | ||
_len = len # cache global lookup | ||||
|
r22670 | def gen(): | ||
i = 0 | ||||
while True: | ||||
if i < _len(genlist): | ||||
yield genlist[i] | ||||
else: | ||||
yield nextrev() | ||||
i += 1 | ||||
return gen() | ||||
|
r20828 | |||
def _consumegen(self): | ||||
|
r22528 | cache = self._cache | ||
genlist = self._genlist.append | ||||
|
r20634 | for item in self._gen: | ||
|
r22528 | cache[item] = True | ||
genlist(item) | ||||
|
r20634 | yield item | ||
|
r22798 | if not self._finished: | ||
self._finished = True | ||||
asc = self._genlist[:] | ||||
asc.sort() | ||||
self._asclist = asc | ||||
self.fastasc = asc.__iter__ | ||||
self.fastdesc = asc.__reversed__ | ||||
|
r20703 | |||
|
r22996 | def __len__(self): | ||
for x in self._consumegen(): | ||||
pass | ||||
return len(self._genlist) | ||||
|
r20703 | def sort(self, reverse=False): | ||
|
r22800 | self._ascending = not reverse | ||
def reverse(self): | ||||
self._ascending = not self._ascending | ||||
|
r20703 | |||
|
r22801 | def isascending(self): | ||
return self._ascending | ||||
def isdescending(self): | ||||
return not self._ascending | ||||
|
r22811 | def first(self): | ||
if self._ascending: | ||||
it = self.fastasc | ||||
else: | ||||
it = self.fastdesc | ||||
if it is None: | ||||
# we need to consume all and try again | ||||
for x in self._consumegen(): | ||||
pass | ||||
return self.first() | ||||
|
r25146 | return next(it(), None) | ||
|
r22811 | |||
def last(self): | ||||
if self._ascending: | ||||
it = self.fastdesc | ||||
else: | ||||
it = self.fastasc | ||||
if it is None: | ||||
# we need to consume all and try again | ||||
for x in self._consumegen(): | ||||
pass | ||||
return self.first() | ||||
|
r25146 | return next(it(), None) | ||
|
r22811 | |||
|
r24457 | def __repr__(self): | ||
d = {False: '-', True: '+'}[self._ascending] | ||||
return '<%s%s>' % (type(self).__name__, d) | ||||
|
r24116 | class spanset(abstractsmartset): | ||
|
r20482 | """Duck type for baseset class which represents a range of revisions and | ||
can work lazily and without having all the range in memory | ||||
|
r20737 | |||
Note that spanset(x, y) behave almost like xrange(x, y) except for two | ||||
notable points: | ||||
- when x < y it will be automatically descending, | ||||
- revision filtered with this repoview will be skipped. | ||||
|
r20482 | """ | ||
|
r20525 | def __init__(self, repo, start=0, end=None): | ||
|
r20737 | """ | ||
start: first revision included the set | ||||
(default to 0) | ||||
end: first revision excluded (last+1) | ||||
(default to len(repo) | ||||
Spanset will be descending if `end` < `start`. | ||||
""" | ||||
|
r22717 | if end is None: | ||
end = len(repo) | ||||
self._ascending = start <= end | ||||
if not self._ascending: | ||||
start, end = end + 1, start +1 | ||||
|
r20482 | self._start = start | ||
|
r22717 | self._end = end | ||
|
r20525 | self._hiddenrevs = repo.changelog.filteredrevs | ||
|
r20521 | |||
|
r22717 | def sort(self, reverse=False): | ||
self._ascending = not reverse | ||||
def reverse(self): | ||||
self._ascending = not self._ascending | ||||
def _iterfilter(self, iterrange): | ||||
s = self._hiddenrevs | ||||
for r in iterrange: | ||||
if r not in s: | ||||
yield r | ||||
|
r20482 | def __iter__(self): | ||
|
r22717 | if self._ascending: | ||
return self.fastasc() | ||||
|
r20482 | else: | ||
|
r22717 | return self.fastdesc() | ||
def fastasc(self): | ||||
iterrange = xrange(self._start, self._end) | ||||
|
r20521 | if self._hiddenrevs: | ||
|
r22717 | return self._iterfilter(iterrange) | ||
return iter(iterrange) | ||||
def fastdesc(self): | ||||
iterrange = xrange(self._end - 1, self._start - 1, -1) | ||||
if self._hiddenrevs: | ||||
return self._iterfilter(iterrange) | ||||
return iter(iterrange) | ||||
|
r20482 | |||
|
r21201 | def __contains__(self, rev): | ||
|
r22526 | hidden = self._hiddenrevs | ||
|
r22718 | return ((self._start <= rev < self._end) | ||
|
r22526 | and not (hidden and rev in hidden)) | ||
|
r20482 | |||
|
r20716 | def __nonzero__(self): | ||
for r in self: | ||||
return True | ||||
return False | ||||
|
r20484 | def __len__(self): | ||
|
r20521 | if not self._hiddenrevs: | ||
return abs(self._end - self._start) | ||||
else: | ||||
count = 0 | ||||
|
r21205 | start = self._start | ||
end = self._end | ||||
|
r20521 | for rev in self._hiddenrevs: | ||
|
r21284 | if (end < rev <= start) or (start <= rev < end): | ||
|
r20521 | count += 1 | ||
return abs(self._end - self._start) - count | ||||
|
r20484 | |||
|
r20725 | def isascending(self): | ||
|
r23826 | return self._ascending | ||
|
r20725 | |||
def isdescending(self): | ||||
|
r23826 | return not self._ascending | ||
|
r20725 | |||
|
r22809 | def first(self): | ||
if self._ascending: | ||||
it = self.fastasc | ||||
else: | ||||
it = self.fastdesc | ||||
for x in it(): | ||||
return x | ||||
return None | ||||
def last(self): | ||||
if self._ascending: | ||||
it = self.fastdesc | ||||
else: | ||||
it = self.fastasc | ||||
for x in it(): | ||||
return x | ||||
return None | ||||
|
r24457 | def __repr__(self): | ||
d = {False: '-', True: '+'}[self._ascending] | ||||
return '<%s%s %d:%d>' % (type(self).__name__, d, | ||||
self._start, self._end - 1) | ||||
|
r24116 | class fullreposet(spanset): | ||
|
r22508 | """a set containing all revisions in the repo | ||
|
r24204 | This class exists to host special optimization and magic to handle virtual | ||
revisions such as "null". | ||||
|
r22508 | """ | ||
def __init__(self, repo): | ||||
super(fullreposet, self).__init__(repo) | ||||
|
r22510 | def __and__(self, other): | ||
|
r23139 | """As self contains the whole repo, all of the other set should also be | ||
in self. Therefore `self & other = other`. | ||||
|
r22510 | |||
This boldly assumes the other contains valid revs only. | ||||
""" | ||||
# other not a smartset, make is so | ||||
|
r22883 | if not util.safehasattr(other, 'isascending'): | ||
|
r22510 | # filter out hidden revision | ||
# (this boldly assumes all smartset are pure) | ||||
# | ||||
# `other` was used with "&", let's assume this is a set like | ||||
# object. | ||||
other = baseset(other - self._hiddenrevs) | ||||
|
r25547 | # XXX As fullreposet is also used as bootstrap, this is wrong. | ||
# | ||||
# With a giveme312() revset returning [3,1,2], this makes | ||||
# 'hg log -r "giveme312()"' -> 1, 2, 3 (wrong) | ||||
# We cannot just drop it because other usage still need to sort it: | ||||
# 'hg log -r "all() and giveme312()"' -> 1, 2, 3 (right) | ||||
# | ||||
# There is also some faulty revset implementations that rely on it | ||||
# (eg: children as of its state in e8075329c5fb) | ||||
# | ||||
# When we fix the two points above we can move this into the if clause | ||||
|
r23827 | other.sort(reverse=self.isdescending()) | ||
|
r22510 | return other | ||
|
r24458 | def prettyformatset(revs): | ||
lines = [] | ||||
rs = repr(revs) | ||||
p = 0 | ||||
while p < len(rs): | ||||
q = rs.find('<', p + 1) | ||||
if q < 0: | ||||
q = len(rs) | ||||
l = rs.count('<', 0, p) - rs.count('>', 0, p) | ||||
assert l >= 0 | ||||
lines.append((l, rs[p:q].rstrip())) | ||||
p = q | ||||
return '\n'.join(' ' * l + s for l, s in lines) | ||||
|
r28393 | def loadpredicate(ui, extname, registrarobj): | ||
"""Load revset predicates from specified registrarobj | ||||
""" | ||||
for name, func in registrarobj._table.iteritems(): | ||||
symbols[name] = func | ||||
if func._safe: | ||||
safesymbols.add(name) | ||||
|
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() | ||||