diff -r 0fe7e39dc683 -r 9fd8c2a3db5a mercurial/narrowspec.py --- /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 " ". + # 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