diff --git a/mercurial/fileset.py b/mercurial/fileset.py --- a/mercurial/fileset.py +++ b/mercurial/fileset.py @@ -43,6 +43,9 @@ def getmatch(mctx, x): raise error.ParseError(_("missing argument")) return methods[x[0]](mctx, *x[1:]) +def getmatchwithstatus(mctx, x, hint): + return getmatch(mctx, x) + def stringmatch(mctx, x): return mctx.matcher([x]) @@ -443,6 +446,7 @@ def subrepo(mctx, x): return mctx.predicate(sstate.__contains__, predrepr='subrepo') methods = { + 'withstatus': getmatchwithstatus, 'string': stringmatch, 'symbol': stringmatch, 'kindpat': kindpatmatch, diff --git a/mercurial/filesetlang.py b/mercurial/filesetlang.py --- a/mercurial/filesetlang.py +++ b/mercurial/filesetlang.py @@ -171,6 +171,82 @@ def _analyze(x): return (op, x[1], ta) raise error.ProgrammingError('invalid operator %r' % op) +def _insertstatushints(x): + """Insert hint nodes where status should be calculated (first path) + + This works in bottom-up way, summing up status names and inserting hint + nodes at 'and' and 'or' as needed. Thus redundant hint nodes may be left. + + Returns (status-names, new-tree) at the given subtree, where status-names + is a sum of status names referenced in the given subtree. + """ + if x is None: + return (), x + + op = x[0] + if op in {'string', 'symbol', 'kindpat'}: + return (), x + if op == 'not': + h, t = _insertstatushints(x[1]) + return h, (op, t) + if op == 'and': + ha, ta = _insertstatushints(x[1]) + hb, tb = _insertstatushints(x[2]) + hr = ha + hb + if ha and hb: + return hr, ('withstatus', (op, ta, tb), ('string', ' '.join(hr))) + return hr, (op, ta, tb) + if op == 'or': + hs, ts = zip(*(_insertstatushints(y) for y in x[1:])) + hr = sum(hs, ()) + if sum(bool(h) for h in hs) > 1: + return hr, ('withstatus', (op,) + ts, ('string', ' '.join(hr))) + return hr, (op,) + ts + if op == 'list': + hs, ts = zip(*(_insertstatushints(y) for y in x[1:])) + return sum(hs, ()), (op,) + ts + if op == 'func': + f = getsymbol(x[1]) + # don't propagate 'ha' crossing a function boundary + ha, ta = _insertstatushints(x[2]) + if getattr(symbols.get(f), '_callstatus', False): + return (f,), ('withstatus', (op, x[1], ta), ('string', f)) + return (), (op, x[1], ta) + raise error.ProgrammingError('invalid operator %r' % op) + +def _mergestatushints(x, instatus): + """Remove redundant status hint nodes (second path) + + This is the top-down path to eliminate inner hint nodes. + """ + if x is None: + return x + + op = x[0] + if op == 'withstatus': + if instatus: + # drop redundant hint node + return _mergestatushints(x[1], instatus) + t = _mergestatushints(x[1], instatus=True) + return (op, t, x[2]) + if op in {'string', 'symbol', 'kindpat'}: + return x + if op == 'not': + t = _mergestatushints(x[1], instatus) + return (op, t) + if op == 'and': + ta = _mergestatushints(x[1], instatus) + tb = _mergestatushints(x[2], instatus) + return (op, ta, tb) + if op in {'list', 'or'}: + ts = tuple(_mergestatushints(y, instatus) for y in x[1:]) + return (op,) + ts + if op == 'func': + # don't propagate 'instatus' crossing a function boundary + ta = _mergestatushints(x[2], instatus=False) + return (op, x[1], ta) + raise error.ProgrammingError('invalid operator %r' % op) + def analyze(x): """Transform raw parsed tree to evaluatable tree which can be fed to optimize() or getmatch() @@ -178,7 +254,9 @@ def analyze(x): All pseudo operations should be mapped to real operations or functions defined in methods or symbols table respectively. """ - return _analyze(x) + t = _analyze(x) + _h, t = _insertstatushints(t) + return _mergestatushints(t, instatus=False) def _optimizeandops(op, ta, tb): if tb is not None and tb[0] == 'not': @@ -205,6 +283,9 @@ def _optimize(x): return 0, x op = x[0] + if op == 'withstatus': + w, t = _optimize(x[1]) + return w, (op, t, x[2]) if op in {'string', 'symbol'}: return WEIGHT_CHECK_FILENAME, x if op == 'kindpat': diff --git a/mercurial/minifileset.py b/mercurial/minifileset.py --- a/mercurial/minifileset.py +++ b/mercurial/minifileset.py @@ -24,7 +24,9 @@ def _compile(tree): if not tree: raise error.ParseError(_("missing argument")) op = tree[0] - if op in {'symbol', 'string', 'kindpat'}: + if op == 'withstatus': + return _compile(tree[1]) + elif op in {'symbol', 'string', 'kindpat'}: name = filesetlang.getpattern(tree, {'path'}, _('invalid file pattern')) if name.startswith('**'): # file extension test, ex. "**.tar.gz" ext = name[2:] diff --git a/tests/test-fileset.t b/tests/test-fileset.t --- a/tests/test-fileset.t +++ b/tests/test-fileset.t @@ -175,18 +175,22 @@ Show parsed tree at stages: (func (symbol 'grep') (string 'b')) - (func - (symbol 'clean') - None))) + (withstatus + (func + (symbol 'clean') + None) + (string 'clean')))) * optimized: (or (patterns (symbol 'a1') (symbol 'a2')) (and - (func - (symbol 'clean') - None) + (withstatus + (func + (symbol 'clean') + None) + (string 'clean')) (func (symbol 'grep') (string 'b')))) @@ -374,6 +378,156 @@ Test files status in different revisions b2 c1 +Test insertion of status hints + + $ fileset -p optimized 'added()' + * optimized: + (withstatus + (func + (symbol 'added') + None) + (string 'added')) + c1 + + $ fileset -p optimized 'a* & removed()' + * optimized: + (and + (symbol 'a*') + (withstatus + (func + (symbol 'removed') + None) + (string 'removed'))) + a2 + + $ fileset -p optimized 'a* - removed()' + * optimized: + (minus + (symbol 'a*') + (withstatus + (func + (symbol 'removed') + None) + (string 'removed'))) + a1 + + $ fileset -p analyzed -p optimized '(added() + removed()) - a*' + * analyzed: + (and + (withstatus + (or + (func + (symbol 'added') + None) + (func + (symbol 'removed') + None)) + (string 'added removed')) + (not + (symbol 'a*'))) + * optimized: + (and + (not + (symbol 'a*')) + (withstatus + (or + (func + (symbol 'added') + None) + (func + (symbol 'removed') + None)) + (string 'added removed'))) + c1 + + $ fileset -p optimized 'a* + b* + added() + unknown()' + * optimized: + (withstatus + (or + (patterns + (symbol 'a*') + (symbol 'b*')) + (func + (symbol 'added') + None) + (func + (symbol 'unknown') + None)) + (string 'added unknown')) + a1 + a2 + b1 + b2 + c1 + c3 + + $ fileset -p analyzed -p optimized 'removed() & missing() & a*' + * analyzed: + (and + (withstatus + (and + (func + (symbol 'removed') + None) + (func + (symbol 'missing') + None)) + (string 'removed missing')) + (symbol 'a*')) + * optimized: + (and + (symbol 'a*') + (withstatus + (and + (func + (symbol 'removed') + None) + (func + (symbol 'missing') + None)) + (string 'removed missing'))) + + $ fileset -p optimized 'clean() & revs(0, added())' + * optimized: + (and + (withstatus + (func + (symbol 'clean') + None) + (string 'clean')) + (func + (symbol 'revs') + (list + (symbol '0') + (withstatus + (func + (symbol 'added') + None) + (string 'added'))))) + b1 + + $ fileset -p optimized 'clean() & status(null, 0, b* & added())' + * optimized: + (and + (withstatus + (func + (symbol 'clean') + None) + (string 'clean')) + (func + (symbol 'status') + (list + (symbol 'null') + (symbol '0') + (and + (symbol 'b*') + (withstatus + (func + (symbol 'added') + None) + (string 'added')))))) + b1 + Test files properties >>> open('bin', 'wb').write(b'\0a') and None