mercurial/match.py
changeset 50695 1c31b343e514
parent 50338 81c7d04f4722
child 51569 b32c3146ec34
--- a/mercurial/match.py	Sun Jun 18 00:09:39 2023 +0200
+++ b/mercurial/match.py	Mon Jun 12 16:51:08 2023 +0200
@@ -30,6 +30,7 @@
     b're',
     b'glob',
     b'path',
+    b'filepath',
     b'relglob',
     b'relpath',
     b'relre',
@@ -181,6 +182,8 @@
     're:<regexp>' - a regular expression
     'path:<path>' - a path relative to repository root, which is matched
                     recursively
+    'filepath:<path>' - an exact path to a single file, relative to the
+                        repository root
     'rootfilesin:<path>' - a path relative to repository root, which is
                     matched non-recursively (will not match subdirectories)
     'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
@@ -334,10 +337,18 @@
     """Convert 'kind:pat' from the patterns list to tuples with kind and
     normalized and rooted patterns and with listfiles expanded."""
     kindpats = []
+    kinds_to_normalize = (
+        b'relglob',
+        b'path',
+        b'filepath',
+        b'rootfilesin',
+        b'rootglob',
+    )
+
     for kind, pat in [_patsplit(p, default) for p in patterns]:
         if kind in cwdrelativepatternkinds:
             pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
-        elif kind in (b'relglob', b'path', b'rootfilesin', b'rootglob'):
+        elif kind in kinds_to_normalize:
             pat = util.normpath(pat)
         elif kind in (b'listfile', b'listfile0'):
             try:
@@ -1340,6 +1351,10 @@
         return b''
     if kind == b're':
         return pat
+    if kind == b'filepath':
+        raise error.ProgrammingError(
+            "'filepath:' patterns should not be converted to a regex"
+        )
     if kind in (b'path', b'relpath'):
         if pat == b'.':
             return b''
@@ -1444,7 +1459,14 @@
     """
     try:
         allgroups = []
-        regexps = [_regex(k, p, globsuffix) for (k, p, s) in kindpats]
+        regexps = []
+        exact = set()
+        for (kind, pattern, _source) in kindpats:
+            if kind == b'filepath':
+                exact.add(pattern)
+                continue
+            regexps.append(_regex(kind, pattern, globsuffix))
+
         fullregexp = _joinregexes(regexps)
 
         startidx = 0
@@ -1469,9 +1491,20 @@
             allgroups.append(_joinregexes(group))
             allmatchers = [_rematcher(g) for g in allgroups]
             func = lambda s: any(m(s) for m in allmatchers)
-        return fullregexp, func
+
+        actualfunc = func
+        if exact:
+            # An empty regex will always match, so only call the regex if
+            # there were any actual patterns to match.
+            if not regexps:
+                actualfunc = lambda s: s in exact
+            else:
+                actualfunc = lambda s: s in exact or func(s)
+        return fullregexp, actualfunc
     except re.error:
         for k, p, s in kindpats:
+            if k == b'filepath':
+                continue
             try:
                 _rematcher(_regex(k, p, globsuffix))
             except re.error:
@@ -1502,7 +1535,7 @@
                     break
                 root.append(p)
             r.append(b'/'.join(root))
-        elif kind in (b'relpath', b'path'):
+        elif kind in (b'relpath', b'path', b'filepath'):
             if pat == b'.':
                 pat = b''
             r.append(pat)