diff -r 0e369eca888f -r 706aa203b396 mercurial/minifileset.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/minifileset.py Wed Jan 10 22:23:34 2018 -0500 @@ -0,0 +1,91 @@ +# 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 __future__ import absolute_import + +from .i18n import _ +from . import ( + error, + fileset, +) + +def _compile(tree): + if not tree: + raise error.ParseError(_("missing argument")) + op = tree[0] + if op == 'symbol': + name = fileset.getstring(tree, _('invalid file pattern')) + if name.startswith('**'): # file extension test, ex. "**.tar.gz" + ext = name[2:] + for c in ext: + if c in '*{}[]?/\\': + raise error.ParseError(_('reserved character: %s') % c) + return lambda n, s: n.endswith(ext) + raise error.ParseError(_('invalid symbol: %s') % name) + elif op == 'string': + # TODO: teach fileset about 'path:', so that this can be a symbol and + # not require quoting. + name = fileset.getstring(tree, _('invalid path literal')) + if name.startswith('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] == '/') + return f + raise error.ParseError(_("invalid string"), + hint=_('paths must be prefixed with "path:"')) + elif op == 'or': + func1 = _compile(tree[1]) + func2 = _compile(tree[2]) + return lambda n, s: func1(n, s) or func2(n, s) + elif op == 'and': + func1 = _compile(tree[1]) + func2 = _compile(tree[2]) + return lambda n, s: func1(n, s) and func2(n, s) + elif op == 'not': + return lambda n, s: not _compile(tree[1])(n, s) + elif op == 'group': + return _compile(tree[1]) + elif op == 'func': + symbols = { + 'all': lambda n, s: True, + 'none': lambda n, s: False, + 'size': lambda n, s: fileset.sizematcher(tree[2])(s), + } + + x = tree[1] + name = x[1] + if x[0] == 'symbol' and name in symbols: + return symbols[name] + + raise error.UnknownIdentifier(name, symbols.keys()) + elif op == '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 == 'negate': + raise error.ParseError(_("can't use negate operator in this context")) + elif op == 'list': + raise error.ParseError(_("can't use a list in this context"), + hint=_('see hg help "filesets.x or y"')) + raise error.ProgrammingError('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 = fileset.parse(text) + return _compile(tree)