mercurial/minifileset.py
author Raphaël Gomès <rgomes@octobus.net>
Mon, 12 Jun 2023 16:51:08 +0200
changeset 50695 1c31b343e514
parent 48966 6000f5b25c9b
child 51901 f4733654f144
permissions -rw-r--r--
match: add `filepath:` pattern to match an exact filepath relative to the root It's useful in certain automated workflows to make sure we recurse in directories whose name conflicts with files in other revisions. In addition it makes it possible to avoid building a potentially costly regex, improving performance when the set of files to match explicitly is large. The benchmark below are run in the following configuration : # data-env-vars.name = mozilla-central-2018-08-01-zstd-sparse-revlog # benchmark.name = files # benchmark.variants.rev = tip # benchmark.variants.files = all-list-filepath-sorted # bin-env-vars.hg.flavor = no-rust It also includes timings using the re2 engine (through the `google-re2` module) to show how much can be saved by just using a better regexp engine. Pattern time (seconds) time using re2 ----------------------------------------------------------- just "." 0.4 0.4 list of "filepath:…" 1.3 1.3 list of "path:…" 25.7 3.9 list of patterns 29.7 10.4 As you can see, Without re2, using "filepath:" instead of "path:" is a huge win. With re2, it is still about three times faster to not have to build the regex.

# minifileset.py - a simple language to select files
#
# Copyright 2017 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.


from .i18n import _
from . import (
    error,
    fileset,
    filesetlang,
    pycompat,
)


def _sizep(x):
    # i18n: "size" is a keyword
    expr = filesetlang.getstring(x, _(b"size requires an expression"))
    return fileset.sizematcher(expr)


def _compile(tree):
    if not tree:
        raise error.ParseError(_(b"missing argument"))
    op = tree[0]
    if op == b'withstatus':
        return _compile(tree[1])
    elif op in {b'symbol', b'string', b'kindpat'}:
        name = filesetlang.getpattern(
            tree, {b'path'}, _(b'invalid file pattern')
        )
        if name.startswith(b'**'):  # file extension test, ex. "**.tar.gz"
            ext = name[2:]
            for c in pycompat.bytestr(ext):
                if c in b'*{}[]?/\\':
                    raise error.ParseError(_(b'reserved character: %s') % c)
            return lambda n, s: n.endswith(ext)
        elif name.startswith(b'path:'):  # directory or full path test
            p = name[5:]  # prefix
            pl = len(p)
            f = lambda n, s: n.startswith(p) and (
                len(n) == pl or n[pl : pl + 1] == b'/'
            )
            return f
        raise error.ParseError(
            _(b"unsupported file pattern: %s") % name,
            hint=_(b'paths must be prefixed with "path:"'),
        )
    elif op in {b'or', b'patterns'}:
        funcs = [_compile(x) for x in tree[1:]]
        return lambda n, s: any(f(n, s) for f in funcs)
    elif op == b'and':
        func1 = _compile(tree[1])
        func2 = _compile(tree[2])
        return lambda n, s: func1(n, s) and func2(n, s)
    elif op == b'not':
        return lambda n, s: not _compile(tree[1])(n, s)
    elif op == b'func':
        symbols = {
            b'all': lambda n, s: True,
            b'none': lambda n, s: False,
            b'size': lambda n, s: _sizep(tree[2])(s),
        }

        name = filesetlang.getsymbol(tree[1])
        if name in symbols:
            return symbols[name]

        raise error.UnknownIdentifier(name, symbols.keys())
    elif op == b'minus':  # equivalent to 'x and not y'
        func1 = _compile(tree[1])
        func2 = _compile(tree[2])
        return lambda n, s: func1(n, s) and not func2(n, s)
    elif op == b'list':
        raise error.ParseError(
            _(b"can't use a list in this context"),
            hint=_(b'see \'hg help "filesets.x or y"\''),
        )
    raise error.ProgrammingError(b'illegal tree: %r' % (tree,))


def compile(text):
    """generate a function (path, size) -> bool from filter specification.

    "text" could contain the operators defined by the fileset language for
    common logic operations, and parenthesis for grouping.  The supported path
    tests are '**.extname' for file extension test, and '"path:dir/subdir"'
    for prefix test.  The ``size()`` predicate is borrowed from filesets to test
    file size.  The predicates ``all()`` and ``none()`` are also supported.

    '(**.php & size(">10MB")) | **.zip | (path:bin & !path:bin/README)' for
    example, will catch all php files whose size is greater than 10 MB, all
    files whose name ends with ".zip", and all files under "bin" in the repo
    root except for "bin/README".
    """
    tree = filesetlang.parse(text)
    tree = filesetlang.analyze(tree)
    tree = filesetlang.optimize(tree)
    return _compile(tree)