# HG changeset patch # User Patrick Mezard # Date 2012-02-23 17:05:20 # Node ID 5a627b49b4d94a627b6e990f07f7a5544e9376bf # Parent 1bfc7ba8b404b650bb360d36bd41c11a80d6f5ab graphlog: paths/-I/-X handling requires a new revset The filtering logic of match objects cannot be reproduced with the existing revsets as it operates at changeset files level. A changeset touching "a" and "b" is matched by "-I a -X b" but not by "file(a) and not file(b)". To solve this, a new internal "_matchfiles(...)" revset is introduced. It works like "file(x)" but accepts more than one argument and its arguments are prefixed with "p:", "i:" and "x:" to be used as patterns, include patterns or exclude patterns respectively. The _matchfiles revset is kept private for now: - There are probably smarter ways to pass the arguments in a user-friendly way - A "rev:" argument is likely appear at some point to emulate log command behaviour with regard to filesets: they are evaluated for the parent revision and applied everywhere instead of being reevaluated for each revision. diff --git a/hgext/graphlog.py b/hgext/graphlog.py --- a/hgext/graphlog.py +++ b/hgext/graphlog.py @@ -255,9 +255,6 @@ def revset(repo, pats, opts): 'removed': ('removes("*")', None), 'date': ('date(%(val)r)', None), 'branch': ('branch(%(val)r)', ' or '), - 'exclude': ('not file(%(val)r)', ' and '), - 'include': ('file(%(val)r)', ' and '), - '_pats': ('file(%(val)r)', ' or '), '_patslog': ('filelog(%(val)r)', ' or '), 'keyword': ('keyword(%(val)r)', ' or '), 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '), @@ -281,8 +278,21 @@ def revset(repo, pats, opts): # try to find matching entries on the slow path. slowpath = True if slowpath: - # See cmdutil.walkchangerevs() slow path - opts['_pats'] = list(pats) + # See cmdutil.walkchangerevs() slow path. + # + # pats/include/exclude cannot be represented as separate + # revset expressions as their filtering logic applies at file + # level. For instance "-I a -X a" matches a revision touching + # "a" and "b" while "file(a) and not file(b)" does not. + matchargs = [] + for p in pats: + matchargs.append('p:' + p) + for p in opts.get('include', []): + matchargs.append('i:' + p) + for p in opts.get('exclude', []): + matchargs.append('x:' + p) + matchargs = ','.join(('%r' % p) for p in matchargs) + opts['rev'] = opts.get('rev', []) + ['_matchfiles(%s)' % matchargs] else: opts['_patslog'] = list(pats) diff --git a/mercurial/revset.py b/mercurial/revset.py --- a/mercurial/revset.py +++ b/mercurial/revset.py @@ -112,7 +112,7 @@ def getlist(x): def getargs(x, min, max, err): l = getlist(x) - if len(l) < min or len(l) > max: + if len(l) < min or (max >= 0 and len(l) > max): raise error.ParseError(err) return l @@ -493,23 +493,52 @@ def grep(repo, subset, x): break return l +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 + # patterns and 'x:' for exclude patterns. + + # i18n: "_matchfiles" is a keyword + l = getargs(x, 1, -1, _("_matchfiles requires at least one argument")) + pats, inc, exc = [], [], [] + hasset = False + for arg in l: + 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) + else: + raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix) + if not hasset and matchmod.patkind(value) == 'set': + hasset = True + m = None + s = [] + for r in subset: + c = repo[r] + if not m or hasset: + m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc, + exclude=exc, ctx=c) + for f in c.files(): + if m(f): + s.append(r) + break + return s + def hasfile(repo, subset, x): """``file(pattern)`` Changesets affecting files matched by pattern. """ # i18n: "file" is a keyword pat = getstring(x, _("file requires a pattern")) - m = None - s = [] - for r in subset: - 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.files(): - if m(f): - s.append(r) - break - return s + return _matchfiles(repo, subset, ('string', 'p:' + pat)) def head(repo, subset, x): """``head()`` @@ -943,6 +972,7 @@ symbols = { "keyword": keyword, "last": last, "limit": limit, + "_matchfiles": _matchfiles, "max": maxrev, "merge": merge, "min": minrev, diff --git a/tests/test-glog.t b/tests/test-glog.t --- a/tests/test-glog.t +++ b/tests/test-glog.t @@ -1437,7 +1437,6 @@ Test log -G options ('group', ('group', ('or', ('or', ('func', ('symbol', 'branch'), ('string', 'default')), ('func', ('symbol', 'branch'), ('string', 'branch'))), ('func', ('symbol', 'branch'), ('string', 'branch'))))) $ testlog -k expand -k merge ('group', ('group', ('or', ('func', ('symbol', 'keyword'), ('string', 'expand')), ('func', ('symbol', 'keyword'), ('string', 'merge'))))) - $ hg log -G --include 'some file' --exclude 'another file' $ hg log -G --follow --template 'nodetag {rev}\n' | grep nodetag | wc -l \s*36 (re) $ hg log -G --removed --template 'nodetag {rev}\n' | grep nodetag | wc -l @@ -1527,4 +1526,9 @@ Dedicated repo for --follow and paths fi Test falling back to slow path for non-existing files $ testlog a c - ('group', ('group', ('or', ('func', ('symbol', 'file'), ('string', 'a')), ('func', ('symbol', 'file'), ('string', 'c'))))) + ('group', ('group', ('func', ('symbol', '_matchfiles'), ('list', ('string', 'p:a'), ('string', 'p:c'))))) + +Test multiple --include/--exclude/paths + + $ testlog --include a --include e --exclude b --exclude e a e + ('group', ('group', ('func', ('symbol', '_matchfiles'), ('list', ('list', ('list', ('list', ('list', ('string', 'p:a'), ('string', 'p:e')), ('string', 'i:a')), ('string', 'i:e')), ('string', 'x:b')), ('string', 'x:e')))))