mercurial/narrowspec.py
changeset 36160 9fd8c2a3db5a
parent 36159 0fe7e39dc683
child 36470 d851951b421c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/narrowspec.py	Mon Feb 12 16:21:34 2018 -0800
@@ -0,0 +1,204 @@
+# narrowspec.py - methods for working with a narrow view of a repository
+#
+# Copyright 2017 Google, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import absolute_import
+
+import errno
+
+from .i18n import _
+from . import (
+    error,
+    hg,
+    match as matchmod,
+    util,
+)
+
+FILENAME = 'narrowspec'
+
+def _parsestoredpatterns(text):
+    """Parses the narrowspec format that's stored on disk."""
+    patlist = None
+    includepats = []
+    excludepats = []
+    for l in text.splitlines():
+        if l == '[includes]':
+            if patlist is None:
+                patlist = includepats
+            else:
+                raise error.Abort(_('narrowspec includes section must appear '
+                                    'at most once, before excludes'))
+        elif l == '[excludes]':
+            if patlist is not excludepats:
+                patlist = excludepats
+            else:
+                raise error.Abort(_('narrowspec excludes section must appear '
+                                    'at most once'))
+        else:
+            patlist.append(l)
+
+    return set(includepats), set(excludepats)
+
+def parseserverpatterns(text):
+    """Parses the narrowspec format that's returned by the server."""
+    includepats = set()
+    excludepats = set()
+
+    # We get one entry per line, in the format "<key> <value>".
+    # It's OK for value to contain other spaces.
+    for kp in (l.split(' ', 1) for l in text.splitlines()):
+        if len(kp) != 2:
+            raise error.Abort(_('Invalid narrowspec pattern line: "%s"') % kp)
+        key = kp[0]
+        pat = kp[1]
+        if key == 'include':
+            includepats.add(pat)
+        elif key == 'exclude':
+            excludepats.add(pat)
+        else:
+            raise error.Abort(_('Invalid key "%s" in server response') % key)
+
+    return includepats, excludepats
+
+def normalizesplitpattern(kind, pat):
+    """Returns the normalized version of a pattern and kind.
+
+    Returns a tuple with the normalized kind and normalized pattern.
+    """
+    pat = pat.rstrip('/')
+    _validatepattern(pat)
+    return kind, pat
+
+def _numlines(s):
+    """Returns the number of lines in s, including ending empty lines."""
+    # We use splitlines because it is Unicode-friendly and thus Python 3
+    # compatible. However, it does not count empty lines at the end, so trick
+    # it by adding a character at the end.
+    return len((s + 'x').splitlines())
+
+def _validatepattern(pat):
+    """Validates the pattern and aborts if it is invalid.
+
+    Patterns are stored in the narrowspec as newline-separated
+    POSIX-style bytestring paths. There's no escaping.
+    """
+
+    # We use newlines as separators in the narrowspec file, so don't allow them
+    # in patterns.
+    if _numlines(pat) > 1:
+        raise error.Abort(_('newlines are not allowed in narrowspec paths'))
+
+    components = pat.split('/')
+    if '.' in components or '..' in components:
+        raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
+
+def normalizepattern(pattern, defaultkind='path'):
+    """Returns the normalized version of a text-format pattern.
+
+    If the pattern has no kind, the default will be added.
+    """
+    kind, pat = matchmod._patsplit(pattern, defaultkind)
+    return '%s:%s' % normalizesplitpattern(kind, pat)
+
+def parsepatterns(pats):
+    """Parses a list of patterns into a typed pattern set."""
+    return set(normalizepattern(p) for p in pats)
+
+def format(includes, excludes):
+    output = '[includes]\n'
+    for i in sorted(includes - excludes):
+        output += i + '\n'
+    output += '[excludes]\n'
+    for e in sorted(excludes):
+        output += e + '\n'
+    return output
+
+def match(root, include=None, exclude=None):
+    if not include:
+        # Passing empty include and empty exclude to matchmod.match()
+        # gives a matcher that matches everything, so explicitly use
+        # the nevermatcher.
+        return matchmod.never(root, '')
+    return matchmod.match(root, '', [], include=include or [],
+                          exclude=exclude or [])
+
+def needsexpansion(includes):
+    return [i for i in includes if i.startswith('include:')]
+
+def load(repo):
+    if repo.shared():
+        repo = hg.sharedreposource(repo)
+    try:
+        spec = repo.vfs.read(FILENAME)
+    except IOError as e:
+        # Treat "narrowspec does not exist" the same as "narrowspec file exists
+        # and is empty".
+        if e.errno == errno.ENOENT:
+            # Without this the next call to load will use the cached
+            # non-existence of the file, which can cause some odd issues.
+            repo.invalidate(clearfilecache=True)
+            return set(), set()
+        raise
+    return _parsestoredpatterns(spec)
+
+def save(repo, includepats, excludepats):
+    spec = format(includepats, excludepats)
+    if repo.shared():
+        repo = hg.sharedreposource(repo)
+    repo.vfs.write(FILENAME, spec)
+
+def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
+    r""" Restricts the patterns according to repo settings,
+    results in a logical AND operation
+
+    :param req_includes: requested includes
+    :param req_excludes: requested excludes
+    :param repo_includes: repo includes
+    :param repo_excludes: repo excludes
+    :return: include patterns, exclude patterns, and invalid include patterns.
+
+    >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
+    (set(['f1']), {}, [])
+    >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
+    (set(['f1']), {}, [])
+    >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
+    (set(['f1/fc1']), {}, [])
+    >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
+    ([], set(['path:.']), [])
+    >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
+    (set(['f2/fc2']), {}, [])
+    >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
+    ([], set(['path:.']), [])
+    >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
+    (set(['f1/$non_exitent_var']), {}, [])
+    """
+    res_excludes = set(req_excludes)
+    res_excludes.update(repo_excludes)
+    invalid_includes = []
+    if not req_includes:
+        res_includes = set(repo_includes)
+    elif 'path:.' not in repo_includes:
+        res_includes = []
+        for req_include in req_includes:
+            req_include = util.expandpath(util.normpath(req_include))
+            if req_include in repo_includes:
+                res_includes.append(req_include)
+                continue
+            valid = False
+            for repo_include in repo_includes:
+                if req_include.startswith(repo_include + '/'):
+                    valid = True
+                    res_includes.append(req_include)
+                    break
+            if not valid:
+                invalid_includes.append(req_include)
+        if len(res_includes) == 0:
+            res_excludes = {'path:.'}
+        else:
+            res_includes = set(res_includes)
+    else:
+        res_includes = set(req_includes)
+    return res_includes, res_excludes, invalid_includes