revset.py
2025 lines
| 62.7 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. | ||||
|
r16834 | import re | ||
|
r15819 | import parser, util, error, discovery, hbisect, phases | ||
|
r16417 | import node | ||
|
r12085 | import match as matchmod | ||
|
r13593 | from i18n import _ | ||
|
r15726 | import encoding | ||
|
r17469 | import obsolete as obsmod | ||
|
r18251 | import repoview | ||
|
r11275 | |||
|
r16409 | def _revancestors(repo, revs, followfirst): | ||
"""Like revlog.ancestors(), but supports followfirst.""" | ||||
cut = followfirst and 1 or None | ||||
cl = repo.changelog | ||||
|
r16834 | visit = util.deque(revs) | ||
|
r16417 | seen = set([node.nullrev]) | ||
|
r16409 | while visit: | ||
|
r16803 | for parent in cl.parentrevs(visit.popleft())[:cut]: | ||
|
r16409 | if parent not in seen: | ||
visit.append(parent) | ||||
seen.add(parent) | ||||
yield parent | ||||
def _revdescendants(repo, revs, followfirst): | ||||
"""Like revlog.descendants() but supports followfirst.""" | ||||
cut = followfirst and 1 or None | ||||
cl = repo.changelog | ||||
first = min(revs) | ||||
|
r16417 | nullrev = node.nullrev | ||
if first == nullrev: | ||||
|
r16409 | # Are there nodes with a null first parent and a non-null | ||
# second one? Maybe. Do we care? Probably not. | ||||
for i in cl: | ||||
yield i | ||||
return | ||||
seen = set(revs) | ||||
|
r17804 | for i in cl.revs(first + 1): | ||
|
r16409 | for x in cl.parentrevs(i)[:cut]: | ||
|
r16417 | if x != nullrev and x in seen: | ||
|
r16409 | seen.add(i) | ||
yield i | ||||
break | ||||
|
r16862 | def _revsbetween(repo, roots, heads): | ||
"""Return all paths between roots and heads, inclusive of both endpoint | ||||
sets.""" | ||||
if not roots: | ||||
return [] | ||||
parentrevs = repo.changelog.parentrevs | ||||
visit = heads[:] | ||||
reachable = set() | ||||
seen = {} | ||||
minroot = min(roots) | ||||
roots = set(roots) | ||||
# open-code the post-order traversal due to the tiny size of | ||||
# sys.getrecursionlimit() | ||||
while visit: | ||||
rev = visit.pop() | ||||
if rev in roots: | ||||
reachable.add(rev) | ||||
parents = parentrevs(rev) | ||||
seen[rev] = parents | ||||
for parent in parents: | ||||
if parent >= minroot and parent not in seen: | ||||
visit.append(parent) | ||||
if not reachable: | ||||
return [] | ||||
for rev in sorted(seen): | ||||
for parent in seen[rev]: | ||||
if parent in reachable: | ||||
reachable.add(rev) | ||||
return sorted(reachable) | ||||
|
r11275 | elements = { | ||
"(": (20, ("group", 1, ")"), ("func", 1, ")")), | ||||
|
r14070 | "~": (18, None, ("ancestor", 18)), | ||
"^": (18, None, ("parent", 18), ("parentpost", 18)), | ||||
|
r12616 | "-": (5, ("negate", 19), ("minus", 5)), | ||
|
r11278 | "::": (17, ("dagrangepre", 17), ("dagrange", 17), | ||
("dagrangepost", 17)), | ||||
"..": (17, ("dagrangepre", 17), ("dagrange", 17), | ||||
("dagrangepost", 17)), | ||||
":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)), | ||||
|
r11275 | "not": (10, ("not", 10)), | ||
"!": (10, ("not", 10)), | ||||
"and": (5, None, ("and", 5)), | ||||
"&": (5, None, ("and", 5)), | ||||
"or": (4, None, ("or", 4)), | ||||
"|": (4, None, ("or", 4)), | ||||
"+": (4, None, ("or", 4)), | ||||
",": (2, None, ("list", 2)), | ||||
")": (0, None, None), | ||||
"symbol": (0, ("symbol",), None), | ||||
"string": (0, ("string",), None), | ||||
"end": (0, None, None), | ||||
} | ||||
keywords = set(['and', 'or', 'not']) | ||||
def tokenize(program): | ||||
|
r17886 | ''' | ||
Parse a revset statement into a stream of tokens | ||||
Check that @ is a valid unquoted token character (issue3686): | ||||
>>> list(tokenize("@::")) | ||||
[('symbol', '@', 0), ('::', None, 1), ('end', None, 3)] | ||||
''' | ||||
|
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 | ||
|
r14070 | 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: | ||||
decode = lambda x: x.decode('string-escape') | ||||
|
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 | ||
|
r17886 | elif c.isalnum() or c in '._@' or ord(c) > 127: | ||
|
r11275 | s = pos | ||
pos += 1 | ||||
while pos < l: # find end of symbol | ||||
d = program[pos] | ||||
|
r17886 | if not (d.isalnum() or d in "._/@" or ord(d) > 127): | ||
|
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) | ||
|
r11275 | else: | ||
|
r11289 | yield ('symbol', sym, s) | ||
|
r11275 | pos -= 1 | ||
else: | ||||
|
r11383 | raise error.ParseError(_("syntax error"), 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': | ||||
return getlist(x[1]) + [x[2]] | ||||
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 | ||
def getset(repo, subset, x): | ||||
if not x: | ||||
|
r11383 | raise error.ParseError(_("missing argument")) | ||
|
r11275 | return methods[x[0]](repo, subset, *x[1:]) | ||
|
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() | ||||
|
r11282 | if x == -1 and len(subset) == len(repo): | ||
return [-1] | ||||
|
r13938 | if len(subset) == len(repo) or x in subset: | ||
|
r11275 | return [x] | ||
return [] | ||||
def symbolset(repo, subset, x): | ||||
if x in symbols: | ||||
|
r11383 | raise error.ParseError(_("can't use %s here") % x) | ||
|
r11275 | return stringset(repo, subset, x) | ||
def rangeset(repo, subset, x, y): | ||||
|
r18473 | cl = repo.changelog | ||
m = getset(repo, cl, x) | ||||
n = getset(repo, cl, y) | ||||
|
r11456 | |||
if not m or not n: | ||||
return [] | ||||
m, n = m[0], n[-1] | ||||
|
r11275 | if m < n: | ||
|
r11456 | r = range(m, n + 1) | ||
else: | ||||
r = range(m, n - 1, -1) | ||||
s = set(subset) | ||||
return [x for x in r if x in s] | ||||
|
r11275 | |||
|
r16860 | def dagrange(repo, subset, x, y): | ||
|
r18991 | r = list(repo) | ||
xs = _revsbetween(repo, getset(repo, r, x), getset(repo, r, y)) | ||||
s = set(subset) | ||||
return [r for r in xs if r in s] | ||||
|
r16860 | |||
|
r11275 | def andset(repo, subset, x, y): | ||
return getset(repo, getset(repo, subset, x), y) | ||||
def orset(repo, subset, x, y): | ||||
|
r13932 | xl = getset(repo, subset, x) | ||
s = set(xl) | ||||
yl = getset(repo, [r for r in subset if r not in s], y) | ||||
return xl + yl | ||||
|
r11275 | |||
def notset(repo, subset, x): | ||||
s = set(getset(repo, subset, x)) | ||||
return [r for r in subset if r not in s] | ||||
def listset(repo, subset, a, b): | ||||
|
r11383 | raise error.ParseError(_("can't use a list 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) | ||||
|
r11383 | raise error.ParseError(_("not a function: %s") % a[1]) | ||
|
r11275 | |||
# functions | ||||
|
r13915 | def adds(repo, subset, x): | ||
"""``adds(pattern)`` | ||||
Changesets that add a file matching pattern. | ||||
""" | ||||
# i18n: "adds" is a keyword | ||||
pat = getstring(x, _("adds requires a pattern")) | ||||
return checkstatus(repo, subset, pat, 1) | ||||
def ancestor(repo, subset, x): | ||||
|
r18536 | """``ancestor(*changeset)`` | ||
Greatest common ancestor of the changesets. | ||||
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) | ||
rl = list(repo) | ||||
anc = None | ||||
|
r13915 | |||
|
r18536 | # (getset(repo, rl, i) for i in l) generates a list of lists | ||
rev = repo.changelog.rev | ||||
ancestor = repo.changelog.ancestor | ||||
node = repo.changelog.node | ||||
for revs in (getset(repo, rl, i) for i in l): | ||||
for r in revs: | ||||
if anc is None: | ||||
anc = r | ||||
else: | ||||
anc = rev(ancestor(node(anc), node(r))) | ||||
if anc is not None and anc in subset: | ||||
return [anc] | ||||
return [] | ||||
|
r13915 | |||
|
r16409 | def _ancestors(repo, subset, x, followfirst=False): | ||
|
r17675 | args = getset(repo, list(repo), x) | ||
|
r16409 | if not args: | ||
return [] | ||||
s = set(_revancestors(repo, args, followfirst)) | set(args) | ||||
return [r for r in subset if r in s] | ||||
|
r13915 | def ancestors(repo, subset, x): | ||
"""``ancestors(set)`` | ||||
Changesets that are ancestors of a changeset in set. | ||||
""" | ||||
|
r16409 | return _ancestors(repo, subset, x) | ||
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 | ||||
|
r18473 | for r in getset(repo, cl, x): | ||
|
r14070 | for i in range(n): | ||
r = cl.parentrevs(r)[0] | ||||
ps.add(r) | ||||
return [r for r in subset if r in ps] | ||||
|
r13915 | def author(repo, subset, x): | ||
"""``author(string)`` | ||||
Alias for ``user(string)``. | ||||
""" | ||||
# i18n: "author" is a keyword | ||||
|
r15726 | n = encoding.lower(getstring(x, _("author requires a string"))) | ||
|
r16823 | kind, pattern, matcher = _substringmatcher(n) | ||
return [r for r in subset if matcher(encoding.lower(repo[r].user()))] | ||||
|
r13915 | |||
|
r15134 | def bisect(repo, subset, x): | ||
"""``bisect(string)`` | ||||
|
r15153 | 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)) | ||
return [r for r in subset if r in state] | ||||
|
r13915 | |||
|
r15134 | # Backward-compatibility | ||
# - no help entry so that we do not advertise it any more | ||||
def bisected(repo, subset, x): | ||||
return bisect(repo, subset, x) | ||||
|
r13915 | def bookmark(repo, subset, x): | ||
"""``bookmark([name])`` | ||||
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')) | ||||
|
r16822 | kind, pattern, matcher = _stringmatcher(bm) | ||
if kind == 'literal': | ||||
|
r18495 | bmrev = repo._bookmarks.get(bm, None) | ||
|
r16822 | if not bmrev: | ||
raise util.Abort(_("bookmark '%s' does not exist") % bm) | ||||
bmrev = repo[bmrev].rev() | ||||
return [r for r in subset if r == bmrev] | ||||
else: | ||||
matchrevs = set() | ||||
|
r18495 | for name, bmrev in repo._bookmarks.iteritems(): | ||
|
r16822 | if matcher(name): | ||
matchrevs.add(bmrev) | ||||
if not matchrevs: | ||||
raise util.Abort(_("no bookmarks exist that match '%s'") | ||||
% pattern) | ||||
bmrevs = set() | ||||
for bmrev in matchrevs: | ||||
bmrevs.add(repo[bmrev].rev()) | ||||
return [r for r in subset if r in bmrevs] | ||||
|
r13915 | bms = set([repo[r].rev() | ||
|
r18495 | for r in repo._bookmarks.values()]) | ||
|
r13915 | return [r for r in subset if r in bms] | ||
def branch(repo, subset, x): | ||||
"""``branch(string or set)`` | ||||
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 | """ | ||
try: | ||||
b = getstring(x, '') | ||||
except error.ParseError: | ||||
# not a string, but another revspec, e.g. tip() | ||||
pass | ||||
|
r16821 | else: | ||
kind, pattern, matcher = _stringmatcher(b) | ||||
if kind == 'literal': | ||||
# note: falls through to the revspec case if no branch with | ||||
# this name exists | ||||
if pattern in repo.branchmap(): | ||||
return [r for r in subset if matcher(repo[r].branch())] | ||||
else: | ||||
return [r for r in subset if matcher(repo[r].branch())] | ||||
|
r13915 | |||
|
r17675 | s = getset(repo, list(repo), x) | ||
|
r13915 | b = set() | ||
for r in s: | ||||
b.add(repo[r].branch()) | ||||
s = set(s) | ||||
return [r for r in subset if r in s or repo[r].branch() in b] | ||||
|
r17829 | def bumped(repo, subset, x): | ||
"""``bumped()`` | ||||
Mutable changesets marked as successors of public changesets. | ||||
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') | ||||
return [r for r in subset if r in bumped] | ||||
|
r17913 | def bundle(repo, subset, x): | ||
"""``bundle()`` | ||||
Changesets in the bundle. | ||||
Bundle must be specified by the -R option.""" | ||||
try: | ||||
|
r18411 | bundlerevs = repo.changelog.bundlerevs | ||
|
r17913 | except AttributeError: | ||
raise util.Abort(_("no bundle provided - specify with -R")) | ||||
|
r18411 | return [r for r in subset if r in bundlerevs] | ||
|
r17913 | |||
|
r13915 | def checkstatus(repo, subset, pat, field): | ||
|
r15964 | m = None | ||
|
r13915 | s = [] | ||
|
r16521 | hasset = matchmod.patkind(pat) == 'set' | ||
fname = None | ||||
|
r13915 | for r in subset: | ||
c = repo[r] | ||||
|
r16521 | if not m or hasset: | ||
m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c) | ||||
if not m.anypats() and len(m.files()) == 1: | ||||
fname = m.files()[0] | ||||
if fname is not None: | ||||
if fname not in c.files(): | ||||
|
r13915 | continue | ||
else: | ||||
for f in c.files(): | ||||
if m(f): | ||||
break | ||||
else: | ||||
continue | ||||
files = repo.status(c.p1().node(), c.node())[field] | ||||
|
r16521 | if fname is not None: | ||
if fname in files: | ||||
|
r13915 | s.append(r) | ||
else: | ||||
for f in files: | ||||
if m(f): | ||||
s.append(r) | ||||
break | ||||
return s | ||||
|
r16396 | def _children(repo, narrow, parentset): | ||
|
r15899 | cs = set() | ||
|
r18063 | if not parentset: | ||
return cs | ||||
|
r15899 | pr = repo.changelog.parentrevs | ||
|
r18063 | minrev = min(parentset) | ||
|
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) | ||
return cs | ||||
|
r13915 | def children(repo, subset, x): | ||
"""``children(set)`` | ||||
Child changesets of changesets in set. | ||||
""" | ||||
|
r17675 | s = set(getset(repo, list(repo), x)) | ||
|
r15899 | cs = _children(repo, subset, s) | ||
|
r13915 | return [r for r in subset if r in cs] | ||
def closed(repo, subset, x): | ||||
"""``closed()`` | ||||
Changeset is closed. | ||||
""" | ||||
# i18n: "closed" is a keyword | ||||
getargs(x, 0, 0, _("closed takes no arguments")) | ||||
|
r16720 | return [r for r in subset if repo[r].closesbranch()] | ||
|
r13915 | |||
def contains(repo, subset, x): | ||||
"""``contains(pattern)`` | ||||
|
r14357 | Revision contains a file matching pattern. See :hg:`help patterns` | ||
for information about file patterns. | ||||
|
r13915 | """ | ||
# i18n: "contains" is a keyword | ||||
pat = getstring(x, _("contains requires a pattern")) | ||||
|
r15964 | m = None | ||
|
r13915 | s = [] | ||
|
r15964 | if not matchmod.patkind(pat): | ||
|
r13915 | for r in subset: | ||
if pat in repo[r]: | ||||
s.append(r) | ||||
else: | ||||
for r in subset: | ||||
|
r15964 | c = repo[r] | ||
if not m or matchmod.patkind(pat) == 'set': | ||||
m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c) | ||||
for f in c.manifest(): | ||||
|
r13915 | if m(f): | ||
s.append(r) | ||||
break | ||||
return s | ||||
|
r17002 | def converted(repo, subset, x): | ||
"""``converted([id])`` | ||||
Changesets converted from the given identifier in the old repository if | ||||
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)) | ||||
return [r for r in subset if _matchvalue(r)] | ||||
|
r13915 | def date(repo, subset, x): | ||
"""``date(interval)`` | ||||
Changesets within the interval, see :hg:`help dates`. | ||||
""" | ||||
# i18n: "date" is a keyword | ||||
ds = getstring(x, _("date requires a string")) | ||||
dm = util.matchdate(ds) | ||||
return [r for r in subset if dm(repo[r].date()[0])] | ||||
|
r14650 | def desc(repo, subset, x): | ||
"""``desc(string)`` | ||||
Search commit message for string. The match is case-insensitive. | ||||
""" | ||||
# i18n: "desc" is a keyword | ||||
|
r15726 | ds = encoding.lower(getstring(x, _("desc requires a string"))) | ||
|
r14650 | l = [] | ||
for r in subset: | ||||
c = repo[r] | ||||
|
r15726 | if ds in encoding.lower(c.description()): | ||
|
r14650 | l.append(r) | ||
return l | ||||
|
r16409 | def _descendants(repo, subset, x, followfirst=False): | ||
|
r17675 | args = getset(repo, list(repo), x) | ||
|
r16409 | if not args: | ||
return [] | ||||
s = set(_revdescendants(repo, args, followfirst)) | set(args) | ||||
return [r for r in subset if r in s] | ||||
|
r13915 | def descendants(repo, subset, x): | ||
"""``descendants(set)`` | ||||
Changesets which are descendants of changesets in set. | ||||
""" | ||||
|
r16409 | return _descendants(repo, subset, x) | ||
def _firstdescendants(repo, subset, x): | ||||
# ``_firstdescendants(set)`` | ||||
# Like ``descendants(set)`` but follows only the first parents. | ||||
return _descendants(repo, subset, x, followfirst=True) | ||||
|
r13915 | |||
|
r17186 | def destination(repo, subset, x): | ||
"""``destination([set])`` | ||||
Changesets that were created by a graft, transplant or rebase operation, | ||||
with the given revisions specified as the source. Omitting the optional set | ||||
is the same as passing all(). | ||||
""" | ||||
if x is not None: | ||||
|
r17675 | args = set(getset(repo, list(repo), x)) | ||
|
r17186 | else: | ||
|
r17675 | args = set(getall(repo, list(repo), x)) | ||
|
r17186 | |||
dests = set() | ||||
# subset contains all of the possible destinations that can be returned, so | ||||
# iterate over them and see if their source(s) were provided in the args. | ||||
# Even if the immediate src of r is not in the args, src's source (or | ||||
# 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. | ||||
if src in args or src in dests: | ||||
dests.update(lineage) | ||||
break | ||||
r = src | ||||
src = _getrevsource(repo, r) | ||||
return [r for r in subset if r in dests] | ||||
|
r18071 | def divergent(repo, subset, x): | ||
"""``divergent()`` | ||||
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') | ||||
return [r for r in subset if r in divergent] | ||||
|
r15819 | def draft(repo, subset, x): | ||
"""``draft()`` | ||||
Changeset in draft phase.""" | ||||
|
r17259 | # i18n: "draft" is a keyword | ||
|
r15819 | getargs(x, 0, 0, _("draft takes no arguments")) | ||
|
r16657 | pc = repo._phasecache | ||
return [r for r in subset if pc.phase(repo, r) == phases.draft] | ||||
|
r15819 | |||
|
r17173 | def extinct(repo, subset, x): | ||
"""``extinct()`` | ||||
|
r17291 | Obsolete changesets with obsolete descendants only. | ||
""" | ||||
|
r17259 | # i18n: "extinct" is a keyword | ||
|
r17258 | getargs(x, 0, 0, _("extinct takes no arguments")) | ||
|
r17825 | extincts = obsmod.getrevs(repo, 'extinct') | ||
|
r17469 | return [r for r in subset if r in extincts] | ||
|
r17173 | |||
|
r16661 | def extra(repo, subset, x): | ||
"""``extra(label, [value])`` | ||||
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:`. | ||||
""" | ||||
|
r16661 | |||
|
r17259 | # i18n: "extra" is a keyword | ||
|
r16661 | l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments')) | ||
|
r17259 | # i18n: "extra" is a keyword | ||
|
r16661 | label = getstring(l[0], _('first argument to extra must be a string')) | ||
value = None | ||||
if len(l) > 1: | ||||
|
r17259 | # i18n: "extra" is a keyword | ||
|
r16661 | value = getstring(l[1], _('second argument to extra must be a string')) | ||
|
r16824 | kind, value, matcher = _stringmatcher(value) | ||
|
r16661 | |||
def _matchvalue(r): | ||||
extra = repo[r].extra() | ||||
|
r16824 | return label in extra and (value is None or matcher(extra[label])) | ||
|
r16661 | |||
return [r for r in subset if _matchvalue(r)] | ||||
|
r15819 | |||
|
r14342 | def filelog(repo, subset, x): | ||
"""``filelog(pattern)`` | ||||
Changesets connected to the specified filelog. | ||||
|
r17244 | |||
|
r17265 | For performance reasons, ``filelog()`` does not show every changeset | ||
that affects the requested file(s). See :hg:`help log` for details. For | ||||
a slower, more accurate result, use ``file()``. | ||||
|
r14342 | """ | ||
|
r17259 | # i18n: "filelog" is a keyword | ||
|
r14342 | pat = getstring(x, _("filelog requires a pattern")) | ||
|
r15964 | m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath', | ||
ctx=repo[None]) | ||||
|
r14342 | s = set() | ||
|
r15964 | if not matchmod.patkind(pat): | ||
|
r14342 | for f in m.files(): | ||
fl = repo.file(f) | ||||
for fr in fl: | ||||
s.add(fl.linkrev(fr)) | ||||
else: | ||||
for f in repo[None]: | ||||
if m(f): | ||||
fl = repo.file(f) | ||||
for fr in fl: | ||||
s.add(fl.linkrev(fr)) | ||||
return [r for r in subset if r in s] | ||||
|
r15117 | def first(repo, subset, x): | ||
"""``first(set, [n])`` | ||||
An alias for limit(). | ||||
""" | ||||
return limit(repo, subset, x) | ||||
|
r16185 | def _follow(repo, subset, x, name, followfirst=False): | ||
l = getargs(x, 0, 1, _("%s takes no arguments or a filename") % name) | ||||
c = repo['.'] | ||||
if l: | ||||
x = getstring(l[0], _("%s expected a filename") % name) | ||||
if x in c: | ||||
cx = c[x] | ||||
s = set(ctx.rev() for ctx in cx.ancestors(followfirst=followfirst)) | ||||
# include the revision responsible for the most recent version | ||||
s.add(cx.linkrev()) | ||||
else: | ||||
return [] | ||||
else: | ||||
|
r16409 | s = set(_revancestors(repo, [c.rev()], followfirst)) | set([c.rev()]) | ||
|
r16185 | |||
return [r for r in subset if r in s] | ||||
|
r13915 | def follow(repo, subset, x): | ||
|
r14343 | """``follow([file])`` | ||
An alias for ``::.`` (ancestors of the working copy's first parent). | ||||
If a filename is specified, the history of the given file is followed, | ||||
including copies. | ||||
""" | ||||
|
r16185 | return _follow(repo, subset, x, 'follow') | ||
|
r14343 | |||
|
r16174 | def _followfirst(repo, subset, x): | ||
# ``followfirst([file])`` | ||||
# Like ``follow([file])`` but follows only the first parent of | ||||
# every revision or file revision. | ||||
|
r16185 | return _follow(repo, subset, x, '_followfirst', followfirst=True) | ||
|
r14343 | |||
|
r13915 | def getall(repo, subset, x): | ||
"""``all()`` | ||||
All changesets, the same as ``0:tip``. | ||||
""" | ||||
# i18n: "all" is a keyword | ||||
getargs(x, 0, 0, _("all takes no arguments")) | ||||
return subset | ||||
def grep(repo, subset, x): | ||||
"""``grep(regex)`` | ||||
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"))) | ||||
except re.error, e: | ||||
raise error.ParseError(_('invalid match pattern: %s') % e) | ||||
l = [] | ||||
for r in subset: | ||||
c = repo[r] | ||||
for e in c.files() + [c.user(), c.description()]: | ||||
if gr.search(e): | ||||
l.append(r) | ||||
break | ||||
return l | ||||
|
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 | |||
# i18n: "_matchfiles" is a keyword | ||||
l = getargs(x, 1, -1, _("_matchfiles requires at least one argument")) | ||||
pats, inc, exc = [], [], [] | ||||
hasset = False | ||||
|
r16411 | rev, default = None, None | ||
|
r16161 | for arg in l: | ||
|
r17259 | # i18n: "_matchfiles" is a keyword | ||
|
r16161 | s = getstring(arg, _("_matchfiles requires string arguments")) | ||
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: | ||||
|
r17259 | # i18n: "_matchfiles" is a keyword | ||
|
r16181 | raise error.ParseError(_('_matchfiles expected at most one ' | ||
'revision')) | ||||
rev = value | ||||
|
r16411 | elif prefix == 'd:': | ||
if default is not None: | ||||
|
r17259 | # i18n: "_matchfiles" is a keyword | ||
|
r16411 | raise error.ParseError(_('_matchfiles expected at most one ' | ||
'default mode')) | ||||
default = value | ||||
|
r16161 | else: | ||
|
r17259 | # i18n: "_matchfiles" is a keyword | ||
|
r16161 | raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix) | ||
if not hasset and matchmod.patkind(value) == 'set': | ||||
hasset = True | ||||
|
r16411 | if not default: | ||
default = 'glob' | ||||
|
r16161 | m = None | ||
s = [] | ||||
for r in subset: | ||||
c = repo[r] | ||||
|
r16181 | if not m or (hasset and rev is None): | ||
ctx = c | ||||
if rev is not None: | ||||
ctx = repo[rev or None] | ||||
|
r16161 | m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc, | ||
|
r16411 | exclude=exc, ctx=ctx, default=default) | ||
|
r16161 | for f in c.files(): | ||
if m(f): | ||||
s.append(r) | ||||
break | ||||
return s | ||||
|
r13915 | def hasfile(repo, subset, x): | ||
"""``file(pattern)`` | ||||
Changesets affecting files matched by pattern. | ||||
|
r17244 | |||
|
r17265 | For a faster but less accurate result, consider using ``filelog()`` | ||
instead. | ||||
|
r13915 | """ | ||
# i18n: "file" is a keyword | ||||
pat = getstring(x, _("file requires a pattern")) | ||||
|
r16161 | return _matchfiles(repo, subset, ('string', 'p:' + pat)) | ||
|
r13915 | |||
def head(repo, subset, x): | ||||
"""``head()`` | ||||
Changeset is a named branch head. | ||||
""" | ||||
# i18n: "head" is a keyword | ||||
getargs(x, 0, 0, _("head takes no arguments")) | ||||
hs = set() | ||||
for b, ls in repo.branchmap().iteritems(): | ||||
hs.update(repo[h].rev() for h in ls) | ||||
return [r for r in subset if r in hs] | ||||
def heads(repo, subset, x): | ||||
"""``heads(set)`` | ||||
Members of set with no children in set. | ||||
""" | ||||
s = getset(repo, subset, x) | ||||
ps = set(parents(repo, subset, x)) | ||||
return [r for r in s if r not in ps] | ||||
|
r17390 | def hidden(repo, subset, x): | ||
"""``hidden()`` | ||||
Hidden changesets. | ||||
""" | ||||
# i18n: "hidden" is a keyword | ||||
getargs(x, 0, 0, _("hidden takes no arguments")) | ||||
|
r18382 | hiddenrevs = repoview.filterrevs(repo, 'visible') | ||
|
r18251 | return [r for r in subset if r in hiddenrevs] | ||
|
r17390 | |||
|
r13915 | def keyword(repo, subset, x): | ||
"""``keyword(string)`` | ||||
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"))) | ||
|
r13915 | l = [] | ||
for r in subset: | ||||
c = repo[r] | ||||
|
r19706 | if util.any(kw in encoding.lower(t) | ||
for t in c.files() + [c.user(), c.description()]): | ||||
|
r13915 | l.append(r) | ||
return l | ||||
def limit(repo, subset, x): | ||||
|
r15116 | """``limit(set, [n])`` | ||
First n members of set, defaulting to 1. | ||||
|
r13915 | """ | ||
# i18n: "limit" is a keyword | ||||
|
r15116 | l = getargs(x, 1, 2, _("limit requires one or two arguments")) | ||
|
r13915 | try: | ||
|
r15116 | lim = 1 | ||
if len(l) == 2: | ||||
# i18n: "limit" is a keyword | ||||
lim = int(getstring(l[1], _("limit requires a number"))) | ||||
|
r14851 | except (TypeError, ValueError): | ||
|
r13915 | # i18n: "limit" is a keyword | ||
raise error.ParseError(_("limit expects a number")) | ||||
|
r14153 | ss = set(subset) | ||
|
r17675 | os = getset(repo, list(repo), l[0])[:lim] | ||
|
r14153 | return [r for r in os if r in ss] | ||
|
r13915 | |||
|
r14061 | def last(repo, subset, x): | ||
|
r15116 | """``last(set, [n])`` | ||
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")) | ||||
|
r14153 | ss = set(subset) | ||
|
r17675 | os = getset(repo, list(repo), l[0])[-lim:] | ||
|
r14153 | return [r for r in os if r in ss] | ||
|
r14061 | |||
|
r13915 | def maxrev(repo, subset, x): | ||
"""``max(set)`` | ||||
Changeset with highest revision number in set. | ||||
""" | ||||
|
r17675 | os = getset(repo, list(repo), x) | ||
|
r14153 | if os: | ||
m = max(os) | ||||
|
r13915 | if m in subset: | ||
return [m] | ||||
return [] | ||||
def merge(repo, subset, x): | ||||
"""``merge()`` | ||||
Changeset is a merge changeset. | ||||
""" | ||||
# i18n: "merge" is a keyword | ||||
getargs(x, 0, 0, _("merge takes no arguments")) | ||||
cl = repo.changelog | ||||
return [r for r in subset if cl.parentrevs(r)[1] != -1] | ||||
|
r17753 | def branchpoint(repo, subset, x): | ||
"""``branchpoint()`` | ||||
Changesets with more than one child. | ||||
""" | ||||
# i18n: "branchpoint" is a keyword | ||||
getargs(x, 0, 0, _("branchpoint takes no arguments")) | ||||
cl = repo.changelog | ||||
if not subset: | ||||
return [] | ||||
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 | ||||
|
r17786 | return [r for r in subset if (parentscount[r - baserev] > 1)] | ||
|
r17753 | |||
|
r13915 | def minrev(repo, subset, x): | ||
"""``min(set)`` | ||||
Changeset with lowest revision number in set. | ||||
""" | ||||
|
r17675 | os = getset(repo, list(repo), x) | ||
|
r14153 | if os: | ||
m = min(os) | ||||
|
r13915 | if m in subset: | ||
return [m] | ||||
return [] | ||||
def modifies(repo, subset, x): | ||||
"""``modifies(pattern)`` | ||||
Changesets modifying files matched by pattern. | ||||
""" | ||||
# i18n: "modifies" is a keyword | ||||
pat = getstring(x, _("modifies requires a pattern")) | ||||
return checkstatus(repo, subset, pat, 0) | ||||
|
r16417 | def node_(repo, subset, x): | ||
|
r12821 | """``id(string)`` | ||
|
r12859 | 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: | ||
rn = repo[n].rev() | ||||
else: | ||||
|
r16735 | rn = None | ||
pm = repo.changelog._partialmatch(n) | ||||
if pm is not None: | ||||
rn = repo.changelog.rev(pm) | ||||
|
r12716 | return [r for r in subset if r == rn] | ||
|
r17170 | def obsolete(repo, subset, x): | ||
"""``obsolete()`` | ||||
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') | ||
|
r17469 | return [r for r in subset if r in obsoletes] | ||
|
r17170 | |||
|
r17185 | def origin(repo, subset, x): | ||
"""``origin([set])`` | ||||
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: | ||||
|
r17675 | args = set(getset(repo, list(repo), x)) | ||
|
r17185 | else: | ||
|
r17675 | args = set(getall(repo, list(repo), x)) | ||
|
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 | ||||
o = set([_firstsrc(r) for r in args]) | ||||
return [r for r in subset if r in o] | ||||
|
r13915 | def outgoing(repo, subset, x): | ||
"""``outgoing([path])`` | ||||
Changesets not found in the specified destination repository, or the | ||||
default push location. | ||||
|
r12821 | """ | ||
|
r13915 | import hg # avoid start-up nasties | ||
# 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]) | ||
|
r13915 | return [r for r in subset if r in o] | ||
|
r12716 | |||
|
r11275 | def p1(repo, subset, x): | ||
|
r12928 | """``p1([set])`` | ||
First parent of changesets in set, or the working directory. | ||||
|
r12821 | """ | ||
|
r12928 | if x is None: | ||
|
r13878 | p = repo[x].p1().rev() | ||
|
r12935 | return [r for r in subset if r == p] | ||
|
r12928 | |||
|
r11275 | ps = set() | ||
cl = repo.changelog | ||||
|
r17675 | for r in getset(repo, list(repo), x): | ||
|
r11275 | ps.add(cl.parentrevs(r)[0]) | ||
return [r for r in subset if r in ps] | ||||
def p2(repo, subset, x): | ||||
|
r12928 | """``p2([set])`` | ||
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() | ||
return [r for r in subset if r == p] | ||||
|
r12928 | except IndexError: | ||
return [] | ||||
|
r11275 | ps = set() | ||
cl = repo.changelog | ||||
|
r17675 | for r in getset(repo, list(repo), x): | ||
|
r11275 | ps.add(cl.parentrevs(r)[1]) | ||
return [r for r in subset if r in ps] | ||||
def parents(repo, subset, x): | ||||
|
r12929 | """``parents([set])`` | ||
The set of all parents for all changesets in set, or the working directory. | ||||
|
r12821 | """ | ||
|
r12929 | if x is None: | ||
|
r12935 | ps = tuple(p.rev() for p in repo[x].parents()) | ||
return [r for r in subset if r in ps] | ||||
|
r12929 | |||
|
r11275 | ps = set() | ||
cl = repo.changelog | ||||
|
r17675 | for r in getset(repo, list(repo), x): | ||
|
r11275 | ps.update(cl.parentrevs(r)) | ||
return [r for r in subset if r in ps] | ||||
|
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 | ||
|
r18473 | for r in getset(repo, cl, 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]) | ||||
return [r for r in subset if r in ps] | ||||
|
r11275 | |||
|
r11944 | def present(repo, subset, x): | ||
|
r12821 | """``present(set)`` | ||
An empty set, if any revision in set isn't found; otherwise, | ||||
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: | ||||
return [] | ||||
|
r15819 | def public(repo, subset, x): | ||
"""``public()`` | ||||
Changeset in public phase.""" | ||||
|
r17259 | # i18n: "public" is a keyword | ||
|
r15819 | getargs(x, 0, 0, _("public takes no arguments")) | ||
|
r16657 | pc = repo._phasecache | ||
return [r for r in subset if pc.phase(repo, r) == phases.public] | ||||
|
r15819 | |||
|
r15936 | def remote(repo, subset, x): | ||
|
r16007 | """``remote([id [,path]])`` | ||
|
r15936 | Local revision that corresponds to the given identifier in a | ||
remote repository, if present. Here, the '.' identifier is a | ||||
synonym for the current local branch. | ||||
""" | ||||
import hg # avoid start-up nasties | ||||
# i18n: "remote" is a keyword | ||||
|
r16007 | l = getargs(x, 0, 2, _("remote takes one, two or no 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: | ||
return [r] | ||||
|
r15936 | return [] | ||
|
r11275 | def removes(repo, subset, x): | ||
|
r12821 | """``removes(pattern)`` | ||
Changesets which remove files matching pattern. | ||||
""" | ||||
|
r12815 | # i18n: "removes" is a keyword | ||
|
r12736 | pat = getstring(x, _("removes requires a pattern")) | ||
|
r11275 | return checkstatus(repo, subset, pat, 2) | ||
|
r13915 | def rev(repo, subset, x): | ||
"""``rev(number)`` | ||||
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")) | ||||
return [r for r in subset if r == l] | ||||
|
r11275 | |||
|
r16402 | def matching(repo, subset, x): | ||
"""``matching(revision [, field])`` | ||||
Changesets in which a given set of fields match the set of fields in the | ||||
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")) | ||
|
r17804 | revs = getset(repo, repo.changelog, 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 | |||
|
r16640 | matches = set() | ||
|
r16402 | for rev in revs: | ||
target = getinfo(rev) | ||||
|
r16445 | for r in subset: | ||
match = True | ||||
for n, f in enumerate(getfieldfuncs): | ||||
if target[n] != f(r): | ||||
match = False | ||||
break | ||||
if match: | ||||
|
r16640 | matches.add(r) | ||
return [r for r in subset if r in matches] | ||||
|
r16402 | |||
|
r11275 | def reverse(repo, subset, x): | ||
|
r12821 | """``reverse(set)`` | ||
Reverse order of set. | ||||
""" | ||||
|
r11275 | l = getset(repo, subset, x) | ||
|
r17100 | if not isinstance(l, list): | ||
l = list(l) | ||||
|
r11275 | l.reverse() | ||
return l | ||||
|
r13915 | def roots(repo, subset, x): | ||
"""``roots(set)`` | ||||
|
r16394 | Changesets in set with no parent changeset in set. | ||
|
r12821 | """ | ||
|
r17804 | s = set(getset(repo, repo.changelog, x)) | ||
|
r17961 | subset = [r for r in subset if r in s] | ||
|
r16394 | cs = _children(repo, subset, s) | ||
|
r16395 | return [r for r in subset if r not in cs] | ||
|
r11944 | |||
|
r15819 | def secret(repo, subset, x): | ||
"""``secret()`` | ||||
Changeset in secret phase.""" | ||||
|
r17259 | # i18n: "secret" is a keyword | ||
|
r15819 | getargs(x, 0, 0, _("secret takes no arguments")) | ||
|
r16657 | pc = repo._phasecache | ||
return [r for r in subset if pc.phase(repo, r) == phases.secret] | ||||
|
r15819 | |||
|
r11275 | def sort(repo, subset, x): | ||
|
r12821 | """``sort(set[, [-]key...])`` | ||
Sort set by keys. The default sort order is ascending, specify a key | ||||
as ``-key`` to sort in descending order. | ||||
The keys can be: | ||||
- ``rev`` for the revision number, | ||||
- ``branch`` for the branch name, | ||||
- ``desc`` for the commit message (description), | ||||
- ``user`` for user name (``author`` can be used as an alias), | ||||
- ``date`` for the commit date | ||||
""" | ||||
|
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) | ||||
for r in getset(repo, subset, s): | ||||
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() | ||||
return [e[-1] for e in l] | ||||
|
r16819 | def _stringmatcher(pattern): | ||
""" | ||||
accepts a string, possibly starting with 're:' or 'literal:' prefix. | ||||
returns the matcher name, pattern, and matcher function. | ||||
missing or unknown prefixes are treated as literal matches. | ||||
helper for tests: | ||||
>>> def test(pattern, *tests): | ||||
... kind, pattern, matcher = _stringmatcher(pattern) | ||||
... return (kind, pattern, [bool(matcher(t)) for t in tests]) | ||||
exact matching (no prefix): | ||||
>>> test('abcdefg', 'abc', 'def', 'abcdefg') | ||||
('literal', 'abcdefg', [False, False, True]) | ||||
regex matching ('re:' prefix) | ||||
>>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar') | ||||
('re', 'a.+b', [False, False, True]) | ||||
force exact matches ('literal:' prefix) | ||||
>>> test('literal:re:foobar', 'foobar', 're:foobar') | ||||
('literal', 're:foobar', [False, True]) | ||||
unknown prefixes are ignored and treated as literals | ||||
>>> test('foo:bar', 'foo', 'bar', 'foo:bar') | ||||
('literal', 'foo:bar', [False, False, True]) | ||||
""" | ||||
if pattern.startswith('re:'): | ||||
pattern = pattern[3:] | ||||
try: | ||||
regex = re.compile(pattern) | ||||
except re.error, e: | ||||
raise error.ParseError(_('invalid regular expression: %s') | ||||
% e) | ||||
return 're', pattern, regex.search | ||||
elif pattern.startswith('literal:'): | ||||
pattern = pattern[8:] | ||||
return 'literal', pattern, pattern.__eq__ | ||||
|
r16823 | def _substringmatcher(pattern): | ||
kind, pattern, matcher = _stringmatcher(pattern) | ||||
if kind == 'literal': | ||||
matcher = lambda s: pattern in s | ||||
return kind, pattern, matcher | ||||
|
r16819 | |||
|
r12715 | def tag(repo, subset, x): | ||
|
r14356 | """``tag([name])`` | ||
|
r12821 | The specified tag by name, or all tagged revisions if no name is given. | ||
""" | ||||
|
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')) | ||||
kind, pattern, matcher = _stringmatcher(pattern) | ||||
if kind == 'literal': | ||||
|
r16825 | # avoid resolving all tags | ||
tn = repo._tagscache.tags.get(pattern, None) | ||||
if tn is None: | ||||
|
r16820 | raise util.Abort(_("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']) | ||||
|
r11280 | return [r for r in subset if r in s] | ||
|
r12821 | def tagged(repo, subset, x): | ||
return tag(repo, subset, x) | ||||
|
r17171 | def unstable(repo, subset, x): | ||
"""``unstable()`` | ||||
|
r17291 | Non-obsolete changesets with obsolete ancestors. | ||
""" | ||||
|
r17259 | # i18n: "unstable" is a keyword | ||
|
r17258 | getargs(x, 0, 0, _("unstable takes no arguments")) | ||
|
r17825 | unstables = obsmod.getrevs(repo, 'unstable') | ||
|
r17469 | return [r for r in subset if r in unstables] | ||
|
r17171 | |||
|
r13915 | def user(repo, subset, x): | ||
"""``user(string)`` | ||||
|
r14357 | 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 | |||
|
r15898 | # for internal use | ||
def _list(repo, subset, x): | ||||
s = getstring(x, "internal error") | ||||
if not s: | ||||
return [] | ||||
if not isinstance(subset, set): | ||||
subset = set(subset) | ||||
ls = [repo[r].rev() for r in s.split('\0')] | ||||
return [r for r in ls if r in subset] | ||||
|
r11275 | symbols = { | ||
|
r11284 | "adds": adds, | ||
"all": getall, | ||||
|
r11275 | "ancestor": ancestor, | ||
"ancestors": ancestors, | ||||
|
r16409 | "_firstancestors": _firstancestors, | ||
|
r11284 | "author": author, | ||
|
r15134 | "bisect": bisect, | ||
|
r13602 | "bisected": bisected, | ||
|
r13359 | "bookmark": bookmark, | ||
|
r11275 | "branch": branch, | ||
|
r17753 | "branchpoint": branchpoint, | ||
|
r17829 | "bumped": bumped, | ||
|
r17913 | "bundle": bundle, | ||
|
r11284 | "children": children, | ||
"closed": closed, | ||||
"contains": contains, | ||||
|
r17002 | "converted": converted, | ||
|
r11284 | "date": date, | ||
|
r14650 | "desc": desc, | ||
|
r11284 | "descendants": descendants, | ||
|
r16409 | "_firstdescendants": _firstdescendants, | ||
|
r17186 | "destination": destination, | ||
|
r18071 | "divergent": divergent, | ||
|
r15819 | "draft": draft, | ||
|
r17173 | "extinct": extinct, | ||
|
r16661 | "extra": extra, | ||
|
r11284 | "file": hasfile, | ||
|
r14342 | "filelog": filelog, | ||
|
r15117 | "first": first, | ||
|
r11284 | "follow": follow, | ||
|
r16174 | "_followfirst": _followfirst, | ||
|
r11284 | "grep": grep, | ||
"head": head, | ||||
"heads": heads, | ||||
|
r17390 | "hidden": hidden, | ||
|
r16417 | "id": node_, | ||
|
r11275 | "keyword": keyword, | ||
|
r14061 | "last": last, | ||
|
r11284 | "limit": limit, | ||
|
r16161 | "_matchfiles": _matchfiles, | ||
|
r11284 | "max": maxrev, | ||
|
r14649 | "merge": merge, | ||
|
r11708 | "min": minrev, | ||
|
r11284 | "modifies": modifies, | ||
|
r17170 | "obsolete": obsolete, | ||
|
r17185 | "origin": origin, | ||
|
r11284 | "outgoing": outgoing, | ||
|
r11275 | "p1": p1, | ||
"p2": p2, | ||||
"parents": parents, | ||||
|
r11944 | "present": present, | ||
|
r15819 | "public": public, | ||
|
r15936 | "remote": remote, | ||
|
r11284 | "removes": removes, | ||
|
r14649 | "rev": rev, | ||
|
r11284 | "reverse": reverse, | ||
|
r11275 | "roots": roots, | ||
|
r11284 | "sort": sort, | ||
|
r15819 | "secret": secret, | ||
|
r16402 | "matching": matching, | ||
|
r12715 | "tag": tag, | ||
|
r12821 | "tagged": tagged, | ||
"user": user, | ||||
|
r17171 | "unstable": unstable, | ||
|
r15898 | "_list": _list, | ||
|
r11275 | } | ||
|
r19721 | # 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([ | ||||
"adds", | ||||
"all", | ||||
"ancestor", | ||||
"ancestors", | ||||
"_firstancestors", | ||||
"author", | ||||
"bisect", | ||||
"bisected", | ||||
"bookmark", | ||||
"branch", | ||||
"branchpoint", | ||||
"bumped", | ||||
"bundle", | ||||
"children", | ||||
"closed", | ||||
"converted", | ||||
"date", | ||||
"desc", | ||||
"descendants", | ||||
"_firstdescendants", | ||||
"destination", | ||||
"divergent", | ||||
"draft", | ||||
"extinct", | ||||
"extra", | ||||
"file", | ||||
"filelog", | ||||
"first", | ||||
"follow", | ||||
"_followfirst", | ||||
"head", | ||||
"heads", | ||||
"hidden", | ||||
"id", | ||||
"keyword", | ||||
"last", | ||||
"limit", | ||||
"_matchfiles", | ||||
"max", | ||||
"merge", | ||||
"min", | ||||
"modifies", | ||||
"obsolete", | ||||
"origin", | ||||
"outgoing", | ||||
"p1", | ||||
"p2", | ||||
"parents", | ||||
"present", | ||||
"public", | ||||
"remote", | ||||
"removes", | ||||
"rev", | ||||
"reverse", | ||||
"roots", | ||||
"sort", | ||||
"secret", | ||||
"matching", | ||||
"tag", | ||||
"tagged", | ||||
"user", | ||||
"unstable", | ||||
"_list", | ||||
]) | ||||
|
r11275 | methods = { | ||
"range": rangeset, | ||||
|
r16860 | "dagrange": dagrange, | ||
|
r11275 | "string": stringset, | ||
"symbol": symbolset, | ||||
"and": andset, | ||||
"or": orset, | ||||
"not": notset, | ||||
"list": listset, | ||||
"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) | ||
elif op == 'dagrangepre': | ||||
return optimize(('func', ('symbol', 'ancestors'), x[1]), small) | ||||
elif op == 'dagrangepost': | ||||
return optimize(('func', ('symbol', 'descendants'), x[1]), small) | ||||
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) | ||||
w = min(wa, wb) | ||||
if wa > wb: | ||||
return w, (op, tb, ta) | ||||
return w, (op, ta, tb) | ||||
elif op == 'or': | ||||
wa, ta = optimize(x[1], False) | ||||
wb, tb = optimize(x[2], False) | ||||
if wb < wa: | ||||
wb, wa = wa, wb | ||||
return max(wa, wb), (op, ta, tb) | ||||
|
r11275 | elif op == 'not': | ||
|
r11279 | 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) | ||
|
r16860 | elif op in 'dagrange range list 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) | ||||
|
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 | ||
|
r15117 | elif f in "reverse limit first": | ||
|
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 | |||
|
r16771 | _aliasarg = ('func', ('symbol', '_aliasarg')) | ||
def _getaliasarg(tree): | ||||
"""If tree matches ('func', ('symbol', '_aliasarg'), ('string', X)) | ||||
return X, None otherwise. | ||||
""" | ||||
if (len(tree) == 3 and tree[:2] == _aliasarg | ||||
and tree[2][0] == 'string'): | ||||
return tree[2][1] | ||||
return None | ||||
def _checkaliasarg(tree, known=None): | ||||
"""Check tree contains no _aliasarg construct or only ones which | ||||
value is in known. Used to avoid alias placeholders injection. | ||||
""" | ||||
if isinstance(tree, tuple): | ||||
arg = _getaliasarg(tree) | ||||
if arg is not None and (not known or arg not in known): | ||||
raise error.ParseError(_("not a function: %s") % '_aliasarg') | ||||
for t in tree: | ||||
_checkaliasarg(t, known) | ||||
|
r14098 | class revsetalias(object): | ||
funcre = re.compile('^([^(]+)\(([^)]+)\)$') | ||||
|
r14723 | args = None | ||
|
r14098 | |||
|
r14723 | def __init__(self, name, value): | ||
|
r14098 | '''Aliases like: | ||
h = heads(default) | ||||
b($1) = ancestors($1) - ancestors(default) | ||||
''' | ||||
|
r16096 | m = self.funcre.search(name) | ||
if m: | ||||
self.name = m.group(1) | ||||
self.tree = ('func', ('symbol', m.group(1))) | ||||
self.args = [x.strip() for x in m.group(2).split(',')] | ||||
for arg in self.args: | ||||
|
r16771 | # _aliasarg() is an unknown symbol only used separate | ||
# alias argument placeholders from regular strings. | ||||
value = value.replace(arg, '_aliasarg(%r)' % (arg,)) | ||||
|
r16096 | else: | ||
self.name = name | ||||
self.tree = ('symbol', name) | ||||
self.replacement, pos = parse(value) | ||||
if pos != len(value): | ||||
raise error.ParseError(_('invalid token'), pos) | ||||
|
r16771 | # Check for placeholder injection | ||
_checkaliasarg(self.replacement, self.args) | ||||
|
r14098 | |||
|
r16096 | def _getalias(aliases, tree): | ||
"""If tree looks like an unexpanded alias, return it. Return None | ||||
otherwise. | ||||
""" | ||||
if isinstance(tree, tuple) and tree: | ||||
if tree[0] == 'symbol' and len(tree) == 2: | ||||
name = tree[1] | ||||
alias = aliases.get(name) | ||||
if alias and alias.args is None and alias.tree == tree: | ||||
return alias | ||||
if tree[0] == 'func' and len(tree) > 1: | ||||
if tree[1][0] == 'symbol' and len(tree[1]) == 2: | ||||
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 | """ | ||
|
r16771 | if not tree or not isinstance(tree, tuple): | ||
|
r16096 | return tree | ||
|
r16771 | arg = _getaliasarg(tree) | ||
if arg is not None: | ||||
return args[arg] | ||||
|
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: | ||||
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( | ||||
_('invalid number of arguments: %s') % 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 | |||
def findaliases(ui, tree): | ||||
|
r16771 | _checkaliasarg(tree) | ||
|
r16096 | aliases = {} | ||
|
r14098 | for k, v in ui.configitems('revsetalias'): | ||
alias = revsetalias(k, v) | ||||
|
r16096 | aliases[alias.name] = alias | ||
|
r16838 | return _expandaliases(aliases, tree, [], {}) | ||
|
r14098 | |||
|
r11275 | parse = parser.parser(tokenize, elements).parse | ||
|
r14098 | def match(ui, spec): | ||
|
r11385 | if not spec: | ||
raise error.ParseError(_("empty query")) | ||||
|
r14496 | tree, pos = parse(spec) | ||
if (pos != len(spec)): | ||||
|
r14701 | raise error.ParseError(_("invalid token"), pos) | ||
|
r14900 | if ui: | ||
tree = findaliases(ui, tree) | ||||
|
r11279 | weight, tree = optimize(tree, True) | ||
|
r11275 | def mfunc(repo, subset): | ||
return getset(repo, subset, tree) | ||||
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': | ||
return "_list('%s')" % "\0".join(str(int(a)) for a in s) | ||||
elif t == 's': | ||||
return "_list('%s')" % "\0".join(s) | ||||
elif t == 'n': | ||||
|
r16417 | return "_list('%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: | ||||
raise util.Abort('unexpected revspec format character %s' % d) | ||||
else: | ||||
ret += c | ||||
pos += 1 | ||||
return ret | ||||
|
r16218 | def prettyformat(tree): | ||
def _prettyformat(tree, level, lines): | ||||
if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'): | ||||
lines.append((level, str(tree))) | ||||
else: | ||||
lines.append((level, '(%s' % tree[0])) | ||||
for s in tree[1:]: | ||||
_prettyformat(s, level + 1, lines) | ||||
lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')] | ||||
lines = [] | ||||
_prettyformat(tree, 0, lines) | ||||
output = '\n'.join((' '*l + s) for l, s in lines) | ||||
return output | ||||
|
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 | ||||
|
r12823 | # tell hggettext to extract docstrings from these functions: | ||
i18nfunctions = symbols.values() | ||||