Mercurial > hg
view hgext/narrow/narrowspec.py @ 36079:a2a6e724d61a
narrow: import experimental extension from narrowhg revision cb51d673e9c5
Adjustments:
* renamed src to hgext/narrow
* marked extension experimental
* added correct copyright header where it was missing
* updated hgrc extension enable line in library.sh
* renamed library.sh to narrow-library.sh
* dropped all files from repo root as they're not interesting
* dropped test-pyflakes.t, test-check-code.t and test-check-py3-compat.t
* renamed remaining tests to all be test-narrow-* when they didn't already
* fixed test-narrow-expanddirstate.t to refer to narrow and not narrowhg
* fixed tests that wanted `update -C .` instead of `merge --abort`
* corrected a two-space indent in narrowspec.py
* added a missing _() in narrowcommands.py
* fixed imports to pass the import checker
* narrow only adds its --include and --exclude to clone if sparse isn't
enabled to avoid breaking test-duplicateoptions.py. This is a kludge,
and we'll need to come up with a better solution in the future.
These were more or less the minimum to import something that would
pass tests and not create a bunch of files we'll never use.
Changes I intend to make as followups:
* rework the test-narrow-*-tree.t tests to use the new testcases
functionality in run-tests.py
* remove lots of monkeypatches of core things
Differential Revision: https://phab.mercurial-scm.org/D1974
author | Augie Fackler <augie@google.com> |
---|---|
date | Mon, 29 Jan 2018 16:19:33 -0500 |
parents | |
children | 9c55bbc29dcf |
line wrap: on
line source
# 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 mercurial.i18n import _ from mercurial import ( error, match as matchmod, util, ) from .. import ( share, ) 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.""" # 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 = share._getsrcrepo(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 = share._getsrcrepo(repo) repo.vfs.write(FILENAME, spec) def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes, invalid_includes=None): 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 :param invalid_includes: an array to collect invalid includes :return: include and exclude 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 = req_excludes.copy() res_excludes.update(repo_excludes) 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 and invalid_includes is not None: 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