graphlog: paths/-I/-X handling requires a new revset
authorPatrick Mezard <patrick@mezard.eu>
Thu, 23 Feb 2012 18:05:20 +0100
changeset 16161 5a627b49b4d9
parent 16160 1bfc7ba8b404
child 16166 5b0a4383cd5e
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.
hgext/graphlog.py
mercurial/revset.py
tests/test-glog.t
--- 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')))))