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.
--- a/hgext/graphlog.py Thu Feb 23 17:55:07 2012 +0100
+++ b/hgext/graphlog.py Thu Feb 23 18:05:20 2012 +0100
@@ -255,9 +255,6 @@
'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 @@
# 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)
--- a/mercurial/revset.py Thu Feb 23 17:55:07 2012 +0100
+++ b/mercurial/revset.py Thu Feb 23 18:05:20 2012 +0100
@@ -112,7 +112,7 @@
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 @@
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 @@
"keyword": keyword,
"last": last,
"limit": limit,
+ "_matchfiles": _matchfiles,
"max": maxrev,
"merge": merge,
"min": minrev,
--- a/tests/test-glog.t Thu Feb 23 17:55:07 2012 +0100
+++ b/tests/test-glog.t Thu Feb 23 18:05:20 2012 +0100
@@ -1437,7 +1437,6 @@
('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 @@
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')))))