view mercurial/fileset.py @ 38842:503f936489dd

lookup: add option to disambiguate prefix within revset When resolving a nodeid prefix that is not unique within the repo and the user has configured a revset that they want to disambiguate within, we now try to look up within that revset before we fail. If there is a unique match within the revset, we use that. This is of course most effective at allowing a short prefix if the revset contains few nodes. For most of our internal users at Google, "not public()" is sufficiently small that a hex digit or two is enough. The implementation is currently pretty slow, but good enough for small revsets (which is the expected use case). The scan in the revset is linear. We may want to use a prefix tree if we want to allow users to use a larger revset. Credit for the idea goes to Kyle Lippincott. Differential Revision: https://phab.mercurial-scm.org/D4037
author Martin von Zweigbergk <martinvonz@google.com>
date Wed, 28 Mar 2018 09:36:02 -0700
parents bfd5def3fe02
children 61ab546b71c3
line wrap: on
line source

# fileset.py - file 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.

from __future__ import absolute_import

import errno
import re

from .i18n import _
from . import (
    error,
    filesetlang,
    match as matchmod,
    merge,
    pycompat,
    registrar,
    scmutil,
    util,
)
from .utils import (
    stringutil,
)

# helpers for processing parsed tree
getsymbol = filesetlang.getsymbol
getstring = filesetlang.getstring
_getkindpat = filesetlang.getkindpat
getpattern = filesetlang.getpattern
getargs = filesetlang.getargs

def getmatch(mctx, x):
    if not x:
        raise error.ParseError(_("missing argument"))
    return methods[x[0]](mctx, *x[1:])

def stringmatch(mctx, x):
    return mctx.matcher([x])

def kindpatmatch(mctx, x, y):
    return stringmatch(mctx, _getkindpat(x, y, matchmod.allpatternkinds,
                                         _("pattern must be a string")))

def andmatch(mctx, x, y):
    xm = getmatch(mctx, x)
    ym = getmatch(mctx, y)
    return matchmod.intersectmatchers(xm, ym)

def ormatch(mctx, *xs):
    ms = [getmatch(mctx, x) for x in xs]
    return matchmod.unionmatcher(ms)

def notmatch(mctx, x):
    m = getmatch(mctx, x)
    return mctx.predicate(lambda f: not m(f), predrepr=('<not %r>', m))

def minusmatch(mctx, x, y):
    xm = getmatch(mctx, x)
    ym = getmatch(mctx, y)
    return matchmod.differencematcher(xm, ym)

def listmatch(mctx, *xs):
    raise error.ParseError(_("can't use a list in this context"),
                           hint=_('see \'hg help "filesets.x or y"\''))

def func(mctx, a, b):
    funcname = getsymbol(a)
    if funcname in symbols:
        return symbols[funcname](mctx, b)

    keep = lambda fn: getattr(fn, '__doc__', None) is not None

    syms = [s for (s, fn) in symbols.items() if keep(fn)]
    raise error.UnknownIdentifier(funcname, syms)

# symbols are callable like:
#  fun(mctx, x)
# with:
#  mctx - current matchctx instance
#  x - argument in tree form
symbols = filesetlang.symbols

# filesets using matchctx.status()
_statuscallers = set()

predicate = registrar.filesetpredicate()

@predicate('modified()', callstatus=True, weight=10)
def modified(mctx, x):
    """File that is modified according to :hg:`status`.
    """
    # i18n: "modified" is a keyword
    getargs(x, 0, 0, _("modified takes no arguments"))
    s = set(mctx.status().modified)
    return mctx.predicate(s.__contains__, predrepr='modified')

@predicate('added()', callstatus=True, weight=10)
def added(mctx, x):
    """File that is added according to :hg:`status`.
    """
    # i18n: "added" is a keyword
    getargs(x, 0, 0, _("added takes no arguments"))
    s = set(mctx.status().added)
    return mctx.predicate(s.__contains__, predrepr='added')

@predicate('removed()', callstatus=True, weight=10)
def removed(mctx, x):
    """File that is removed according to :hg:`status`.
    """
    # i18n: "removed" is a keyword
    getargs(x, 0, 0, _("removed takes no arguments"))
    s = set(mctx.status().removed)
    return mctx.predicate(s.__contains__, predrepr='removed')

@predicate('deleted()', callstatus=True, weight=10)
def deleted(mctx, x):
    """Alias for ``missing()``.
    """
    # i18n: "deleted" is a keyword
    getargs(x, 0, 0, _("deleted takes no arguments"))
    s = set(mctx.status().deleted)
    return mctx.predicate(s.__contains__, predrepr='deleted')

@predicate('missing()', callstatus=True, weight=10)
def missing(mctx, x):
    """File that is missing according to :hg:`status`.
    """
    # i18n: "missing" is a keyword
    getargs(x, 0, 0, _("missing takes no arguments"))
    s = set(mctx.status().deleted)
    return mctx.predicate(s.__contains__, predrepr='deleted')

@predicate('unknown()', callstatus=True, weight=50)
def unknown(mctx, x):
    """File that is unknown according to :hg:`status`."""
    # i18n: "unknown" is a keyword
    getargs(x, 0, 0, _("unknown takes no arguments"))
    s = set(mctx.status().unknown)
    return mctx.predicate(s.__contains__, predrepr='unknown')

@predicate('ignored()', callstatus=True, weight=50)
def ignored(mctx, x):
    """File that is ignored according to :hg:`status`."""
    # i18n: "ignored" is a keyword
    getargs(x, 0, 0, _("ignored takes no arguments"))
    s = set(mctx.status().ignored)
    return mctx.predicate(s.__contains__, predrepr='ignored')

@predicate('clean()', callstatus=True, weight=10)
def clean(mctx, x):
    """File that is clean according to :hg:`status`.
    """
    # i18n: "clean" is a keyword
    getargs(x, 0, 0, _("clean takes no arguments"))
    s = set(mctx.status().clean)
    return mctx.predicate(s.__contains__, predrepr='clean')

@predicate('tracked()')
def tracked(mctx, x):
    """File that is under Mercurial control."""
    # i18n: "tracked" is a keyword
    getargs(x, 0, 0, _("tracked takes no arguments"))
    return mctx.predicate(mctx.ctx.__contains__, predrepr='tracked')

@predicate('binary()', weight=30)
def binary(mctx, x):
    """File that appears to be binary (contains NUL bytes).
    """
    # i18n: "binary" is a keyword
    getargs(x, 0, 0, _("binary takes no arguments"))
    return mctx.fpredicate(lambda fctx: fctx.isbinary(),
                           predrepr='binary', cache=True)

@predicate('exec()')
def exec_(mctx, x):
    """File that is marked as executable.
    """
    # i18n: "exec" is a keyword
    getargs(x, 0, 0, _("exec takes no arguments"))
    ctx = mctx.ctx
    return mctx.predicate(lambda f: ctx.flags(f) == 'x', predrepr='exec')

@predicate('symlink()')
def symlink(mctx, x):
    """File that is marked as a symlink.
    """
    # i18n: "symlink" is a keyword
    getargs(x, 0, 0, _("symlink takes no arguments"))
    ctx = mctx.ctx
    return mctx.predicate(lambda f: ctx.flags(f) == 'l', predrepr='symlink')

@predicate('resolved()', weight=10)
def resolved(mctx, x):
    """File that is marked resolved according to :hg:`resolve -l`.
    """
    # i18n: "resolved" is a keyword
    getargs(x, 0, 0, _("resolved takes no arguments"))
    if mctx.ctx.rev() is not None:
        return mctx.never()
    ms = merge.mergestate.read(mctx.ctx.repo())
    return mctx.predicate(lambda f: f in ms and ms[f] == 'r',
                          predrepr='resolved')

@predicate('unresolved()', weight=10)
def unresolved(mctx, x):
    """File that is marked unresolved according to :hg:`resolve -l`.
    """
    # i18n: "unresolved" is a keyword
    getargs(x, 0, 0, _("unresolved takes no arguments"))
    if mctx.ctx.rev() is not None:
        return mctx.never()
    ms = merge.mergestate.read(mctx.ctx.repo())
    return mctx.predicate(lambda f: f in ms and ms[f] == 'u',
                          predrepr='unresolved')

@predicate('hgignore()', weight=10)
def hgignore(mctx, x):
    """File that matches the active .hgignore pattern.
    """
    # i18n: "hgignore" is a keyword
    getargs(x, 0, 0, _("hgignore takes no arguments"))
    return mctx.ctx.repo().dirstate._ignore

@predicate('portable()', weight=0.5)
def portable(mctx, x):
    """File that has a portable name. (This doesn't include filenames with case
    collisions.)
    """
    # i18n: "portable" is a keyword
    getargs(x, 0, 0, _("portable takes no arguments"))
    return mctx.predicate(lambda f: util.checkwinfilename(f) is None,
                          predrepr='portable')

@predicate('grep(regex)', weight=30)
def grep(mctx, x):
    """File contains the given regular expression.
    """
    try:
        # i18n: "grep" is a keyword
        r = re.compile(getstring(x, _("grep requires a pattern")))
    except re.error as e:
        raise error.ParseError(_('invalid match pattern: %s') %
                               stringutil.forcebytestr(e))
    return mctx.fpredicate(lambda fctx: r.search(fctx.data()),
                           predrepr=('grep(%r)', r.pattern), cache=True)

def _sizetomax(s):
    try:
        s = s.strip().lower()
        for k, v in util._sizeunits:
            if s.endswith(k):
                # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
                n = s[:-len(k)]
                inc = 1.0
                if "." in n:
                    inc /= 10 ** len(n.split(".")[1])
                return int((float(n) + inc) * v) - 1
        # no extension, this is a precise value
        return int(s)
    except ValueError:
        raise error.ParseError(_("couldn't parse size: %s") % s)

def sizematcher(expr):
    """Return a function(size) -> bool from the ``size()`` expression"""
    expr = expr.strip()
    if '-' in expr: # do we have a range?
        a, b = expr.split('-', 1)
        a = util.sizetoint(a)
        b = util.sizetoint(b)
        return lambda x: x >= a and x <= b
    elif expr.startswith("<="):
        a = util.sizetoint(expr[2:])
        return lambda x: x <= a
    elif expr.startswith("<"):
        a = util.sizetoint(expr[1:])
        return lambda x: x < a
    elif expr.startswith(">="):
        a = util.sizetoint(expr[2:])
        return lambda x: x >= a
    elif expr.startswith(">"):
        a = util.sizetoint(expr[1:])
        return lambda x: x > a
    else:
        a = util.sizetoint(expr)
        b = _sizetomax(expr)
        return lambda x: x >= a and x <= b

@predicate('size(expression)', weight=10)
def size(mctx, x):
    """File size matches the given expression. Examples:

    - size('1k') - files from 1024 to 2047 bytes
    - size('< 20k') - files less than 20480 bytes
    - size('>= .5MB') - files at least 524288 bytes
    - size('4k - 1MB') - files from 4096 bytes to 1048576 bytes
    """
    # i18n: "size" is a keyword
    expr = getstring(x, _("size requires an expression"))
    m = sizematcher(expr)
    return mctx.fpredicate(lambda fctx: m(fctx.size()),
                           predrepr=('size(%r)', expr), cache=True)

@predicate('encoding(name)', weight=30)
def encoding(mctx, x):
    """File can be successfully decoded with the given character
    encoding. May not be useful for encodings other than ASCII and
    UTF-8.
    """

    # i18n: "encoding" is a keyword
    enc = getstring(x, _("encoding requires an encoding name"))

    def encp(fctx):
        d = fctx.data()
        try:
            d.decode(pycompat.sysstr(enc))
            return True
        except LookupError:
            raise error.Abort(_("unknown encoding '%s'") % enc)
        except UnicodeDecodeError:
            return False

    return mctx.fpredicate(encp, predrepr=('encoding(%r)', enc), cache=True)

@predicate('eol(style)', weight=30)
def eol(mctx, x):
    """File contains newlines of the given style (dos, unix, mac). Binary
    files are excluded, files with mixed line endings match multiple
    styles.
    """

    # i18n: "eol" is a keyword
    enc = getstring(x, _("eol requires a style name"))

    def eolp(fctx):
        if fctx.isbinary():
            return False
        d = fctx.data()
        if (enc == 'dos' or enc == 'win') and '\r\n' in d:
            return True
        elif enc == 'unix' and re.search('(?<!\r)\n', d):
            return True
        elif enc == 'mac' and re.search('\r(?!\n)', d):
            return True
        return False
    return mctx.fpredicate(eolp, predrepr=('eol(%r)', enc), cache=True)

@predicate('copied()')
def copied(mctx, x):
    """File that is recorded as being copied.
    """
    # i18n: "copied" is a keyword
    getargs(x, 0, 0, _("copied takes no arguments"))
    def copiedp(fctx):
        p = fctx.parents()
        return p and p[0].path() != fctx.path()
    return mctx.fpredicate(copiedp, predrepr='copied', cache=True)

@predicate('revs(revs, pattern)', weight=10)
def revs(mctx, x):
    """Evaluate set in the specified revisions. If the revset match multiple
    revs, this will return file matching pattern in any of the revision.
    """
    # i18n: "revs" is a keyword
    r, x = getargs(x, 2, 2, _("revs takes two arguments"))
    # i18n: "revs" is a keyword
    revspec = getstring(r, _("first argument to revs must be a revision"))
    repo = mctx.ctx.repo()
    revs = scmutil.revrange(repo, [revspec])

    matchers = []
    for r in revs:
        ctx = repo[r]
        matchers.append(getmatch(mctx.switch(ctx, _buildstatus(ctx, x)), x))
    if not matchers:
        return mctx.never()
    if len(matchers) == 1:
        return matchers[0]
    return matchmod.unionmatcher(matchers)

@predicate('status(base, rev, pattern)', weight=10)
def status(mctx, x):
    """Evaluate predicate using status change between ``base`` and
    ``rev``. Examples:

    - ``status(3, 7, added())`` - matches files added from "3" to "7"
    """
    repo = mctx.ctx.repo()
    # i18n: "status" is a keyword
    b, r, x = getargs(x, 3, 3, _("status takes three arguments"))
    # i18n: "status" is a keyword
    baseerr = _("first argument to status must be a revision")
    baserevspec = getstring(b, baseerr)
    if not baserevspec:
        raise error.ParseError(baseerr)
    reverr = _("second argument to status must be a revision")
    revspec = getstring(r, reverr)
    if not revspec:
        raise error.ParseError(reverr)
    basectx, ctx = scmutil.revpair(repo, [baserevspec, revspec])
    return getmatch(mctx.switch(ctx, _buildstatus(ctx, x, basectx=basectx)), x)

@predicate('subrepo([pattern])')
def subrepo(mctx, x):
    """Subrepositories whose paths match the given pattern.
    """
    # i18n: "subrepo" is a keyword
    getargs(x, 0, 1, _("subrepo takes at most one argument"))
    ctx = mctx.ctx
    sstate = ctx.substate
    if x:
        pat = getpattern(x, matchmod.allpatternkinds,
                         # i18n: "subrepo" is a keyword
                         _("subrepo requires a pattern or no arguments"))
        fast = not matchmod.patkind(pat)
        if fast:
            def m(s):
                return (s == pat)
        else:
            m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx)
        return mctx.predicate(lambda f: f in sstate and m(f),
                              predrepr=('subrepo(%r)', pat))
    else:
        return mctx.predicate(sstate.__contains__, predrepr='subrepo')

methods = {
    'string': stringmatch,
    'symbol': stringmatch,
    'kindpat': kindpatmatch,
    'and': andmatch,
    'or': ormatch,
    'minus': minusmatch,
    'list': listmatch,
    'not': notmatch,
    'func': func,
}

class matchctx(object):
    def __init__(self, ctx, status=None, badfn=None):
        self.ctx = ctx
        self._status = status
        self._badfn = badfn

    def status(self):
        return self._status

    def matcher(self, patterns):
        return self.ctx.match(patterns, badfn=self._badfn)

    def predicate(self, predfn, predrepr=None, cache=False):
        """Create a matcher to select files by predfn(filename)"""
        if cache:
            predfn = util.cachefunc(predfn)
        repo = self.ctx.repo()
        return matchmod.predicatematcher(repo.root, repo.getcwd(), predfn,
                                         predrepr=predrepr, badfn=self._badfn)

    def fpredicate(self, predfn, predrepr=None, cache=False):
        """Create a matcher to select files by predfn(fctx) at the current
        revision

        Missing files are ignored.
        """
        ctx = self.ctx
        if ctx.rev() is None:
            def fctxpredfn(f):
                try:
                    fctx = ctx[f]
                except error.LookupError:
                    return False
                try:
                    fctx.audit()
                except error.Abort:
                    return False
                try:
                    return predfn(fctx)
                except (IOError, OSError) as e:
                    # open()-ing a directory fails with EACCES on Windows
                    if e.errno in (errno.ENOENT, errno.EACCES, errno.ENOTDIR,
                                   errno.EISDIR):
                        return False
                    raise
        else:
            def fctxpredfn(f):
                try:
                    fctx = ctx[f]
                except error.LookupError:
                    return False
                return predfn(fctx)
        return self.predicate(fctxpredfn, predrepr=predrepr, cache=cache)

    def never(self):
        """Create a matcher to select nothing"""
        repo = self.ctx.repo()
        return matchmod.nevermatcher(repo.root, repo.getcwd(),
                                     badfn=self._badfn)

    def switch(self, ctx, status=None):
        return matchctx(ctx, status, self._badfn)

# filesets using matchctx.switch()
_switchcallers = [
    'revs',
    'status',
]

def _intree(funcs, tree):
    if isinstance(tree, tuple):
        if tree[0] == 'func' and tree[1][0] == 'symbol':
            if tree[1][1] in funcs:
                return True
            if tree[1][1] in _switchcallers:
                # arguments won't be evaluated in the current context
                return False
        for s in tree[1:]:
            if _intree(funcs, s):
                return True
    return False

def match(ctx, expr, badfn=None):
    """Create a matcher for a single fileset expression"""
    tree = filesetlang.parse(expr)
    tree = filesetlang.analyze(tree)
    tree = filesetlang.optimize(tree)
    mctx = matchctx(ctx, _buildstatus(ctx, tree), badfn=badfn)
    return getmatch(mctx, tree)

def _buildstatus(ctx, tree, basectx=None):
    # do we need status info?

    if _intree(_statuscallers, tree):
        unknown = _intree(['unknown'], tree)
        ignored = _intree(['ignored'], tree)

        if basectx is None:
            basectx = ctx.p1()
        return basectx.status(ctx, listunknown=unknown, listignored=ignored,
                              listclean=True)
    else:
        return None

def loadpredicate(ui, extname, registrarobj):
    """Load fileset predicates from specified registrarobj
    """
    for name, func in registrarobj._table.iteritems():
        symbols[name] = func
        if func._callstatus:
            _statuscallers.add(name)

# load built-in predicates explicitly to setup _statuscallers
loadpredicate(None, None, predicate)

# tell hggettext to extract docstrings from these functions:
i18nfunctions = symbols.values()