narrowspec.py
316 lines
| 11.3 KiB
| text/x-python
|
PythonLexer
/ mercurial / narrowspec.py
Gregory Szorc
|
r36178 | # 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, | ||||
match as matchmod, | ||||
Martin von Zweigbergk
|
r41072 | merge, | ||
Martin von Zweigbergk
|
r38905 | repository, | ||
Pulkit Goyal
|
r38875 | sparse, | ||
Gregory Szorc
|
r36178 | util, | ||
) | ||||
Martin von Zweigbergk
|
r41072 | # The file in .hg/store/ that indicates which paths exit in the store | ||
Gregory Szorc
|
r36178 | FILENAME = 'narrowspec' | ||
Martin von Zweigbergk
|
r41072 | # The file in .hg/ that indicates which paths exit in the dirstate | ||
DIRSTATE_FILENAME = 'narrowspec.dirstate' | ||||
Gregory Szorc
|
r36178 | |||
Gregory Szorc
|
r39567 | # Pattern prefixes that are allowed in narrow patterns. This list MUST | ||
# only contain patterns that are fast and safe to evaluate. Keep in mind | ||||
# that patterns are supplied by clients and executed on remote servers | ||||
Gregory Szorc
|
r39836 | # as part of wire protocol commands. That means that changes to this | ||
# data structure influence the wire protocol and should not be taken | ||||
# lightly - especially removals. | ||||
Gregory Szorc
|
r39567 | VALID_PREFIXES = ( | ||
b'path:', | ||||
b'rootfilesin:', | ||||
) | ||||
Gregory Szorc
|
r36178 | 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): | ||||
Gregory Szorc
|
r39567 | """Parses an iterable of patterns into a typed pattern set. | ||
Patterns are assumed to be ``path:`` if no prefix is present. | ||||
For safety and performance reasons, only some prefixes are allowed. | ||||
See ``validatepatterns()``. | ||||
This function should be used on patterns that come from the user to | ||||
normalize and validate them to the internal data structure used for | ||||
representing patterns. | ||||
""" | ||||
res = {normalizepattern(orig) for orig in pats} | ||||
validatepatterns(res) | ||||
return res | ||||
def validatepatterns(pats): | ||||
"""Validate that patterns are in the expected data structure and format. | ||||
And that is a set of normalized patterns beginning with ``path:`` or | ||||
``rootfilesin:``. | ||||
This function should be used to validate internal data structures | ||||
and patterns that are loaded from sources that use the internal, | ||||
prefixed pattern representation (but can't necessarily be fully trusted). | ||||
""" | ||||
if not isinstance(pats, set): | ||||
raise error.ProgrammingError('narrow patterns should be a set; ' | ||||
'got %r' % pats) | ||||
for pat in pats: | ||||
if not pat.startswith(VALID_PREFIXES): | ||||
# Use a Mercurial exception because this can happen due to user | ||||
# bugs (e.g. manually updating spec file). | ||||
raise error.Abort(_('invalid prefix on narrow pattern: %s') % pat, | ||||
hint=_('narrow patterns must begin with one of ' | ||||
'the following: %s') % | ||||
', '.join(VALID_PREFIXES)) | ||||
Gregory Szorc
|
r36178 | |||
def format(includes, excludes): | ||||
Pulkit Goyal
|
r38875 | output = '[include]\n' | ||
Gregory Szorc
|
r36178 | for i in sorted(includes - excludes): | ||
output += i + '\n' | ||||
Pulkit Goyal
|
r38875 | output += '[exclude]\n' | ||
Gregory Szorc
|
r36178 | 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. | ||||
Martin von Zweigbergk
|
r41825 | return matchmod.never() | ||
Gregory Szorc
|
r36178 | return matchmod.match(root, '', [], include=include or [], | ||
exclude=exclude or []) | ||||
Martin von Zweigbergk
|
r40726 | def parseconfig(ui, spec): | ||
# maybe we should care about the profiles returned too | ||||
includepats, excludepats, profiles = sparse.parseconfig(ui, spec, 'narrow') | ||||
if profiles: | ||||
raise error.Abort(_("including other spec files using '%include' is not" | ||||
" supported in narrowspec")) | ||||
validatepatterns(includepats) | ||||
validatepatterns(excludepats) | ||||
return includepats, excludepats | ||||
Gregory Szorc
|
r36178 | def load(repo): | ||
try: | ||||
Martin von Zweigbergk
|
r38908 | spec = repo.svfs.read(FILENAME) | ||
Gregory Szorc
|
r36178 | except IOError as e: | ||
# Treat "narrowspec does not exist" the same as "narrowspec file exists | ||||
# and is empty". | ||||
if e.errno == errno.ENOENT: | ||||
return set(), set() | ||||
raise | ||||
Gregory Szorc
|
r39575 | |||
Martin von Zweigbergk
|
r40726 | return parseconfig(repo.ui, spec) | ||
Gregory Szorc
|
r36178 | |||
def save(repo, includepats, excludepats): | ||||
Gregory Szorc
|
r39575 | validatepatterns(includepats) | ||
validatepatterns(excludepats) | ||||
Gregory Szorc
|
r36178 | spec = format(includepats, excludepats) | ||
Martin von Zweigbergk
|
r38908 | repo.svfs.write(FILENAME, spec) | ||
Gregory Szorc
|
r36178 | |||
Martin von Zweigbergk
|
r41265 | def copytoworkingcopy(repo): | ||
spec = repo.svfs.read(FILENAME) | ||||
repo.vfs.write(DIRSTATE_FILENAME, spec) | ||||
Martin von Zweigbergk
|
r41072 | |||
Martin von Zweigbergk
|
r38905 | def savebackup(repo, backupname): | ||
if repository.NARROW_REQUIREMENT not in repo.requirements: | ||||
return | ||||
Martin von Zweigbergk
|
r41070 | svfs = repo.svfs | ||
svfs.tryunlink(backupname) | ||||
util.copyfile(svfs.join(FILENAME), svfs.join(backupname), hardlink=True) | ||||
Martin von Zweigbergk
|
r38872 | |||
Martin von Zweigbergk
|
r38905 | def restorebackup(repo, backupname): | ||
if repository.NARROW_REQUIREMENT not in repo.requirements: | ||||
return | ||||
Martin von Zweigbergk
|
r41070 | util.rename(repo.svfs.join(backupname), repo.svfs.join(FILENAME)) | ||
Martin von Zweigbergk
|
r38872 | |||
Martin von Zweigbergk
|
r41263 | def savewcbackup(repo, backupname): | ||
Martin von Zweigbergk
|
r38905 | if repository.NARROW_REQUIREMENT not in repo.requirements: | ||
return | ||||
Martin von Zweigbergk
|
r41263 | vfs = repo.vfs | ||
vfs.tryunlink(backupname) | ||||
# It may not exist in old repos | ||||
if vfs.exists(DIRSTATE_FILENAME): | ||||
util.copyfile(vfs.join(DIRSTATE_FILENAME), vfs.join(backupname), | ||||
hardlink=True) | ||||
def restorewcbackup(repo, backupname): | ||||
if repository.NARROW_REQUIREMENT not in repo.requirements: | ||||
return | ||||
Martin von Zweigbergk
|
r41334 | # It may not exist in old repos | ||
if repo.vfs.exists(backupname): | ||||
util.rename(repo.vfs.join(backupname), repo.vfs.join(DIRSTATE_FILENAME)) | ||||
Martin von Zweigbergk
|
r41263 | |||
def clearwcbackup(repo, backupname): | ||||
if repository.NARROW_REQUIREMENT not in repo.requirements: | ||||
return | ||||
Martin von Zweigbergk
|
r41334 | repo.vfs.tryunlink(backupname) | ||
Martin von Zweigbergk
|
r38872 | |||
Gregory Szorc
|
r36178 | 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 | ||||
Martin von Zweigbergk
|
r41072 | |||
# These two are extracted for extensions (specifically for Google's CitC file | ||||
# system) | ||||
def _deletecleanfiles(repo, files): | ||||
for f in files: | ||||
repo.wvfs.unlinkpath(f) | ||||
def _writeaddedfiles(repo, pctx, files): | ||||
actions = merge.emptyactions() | ||||
Martin von Zweigbergk
|
r41213 | addgaction = actions[merge.ACTION_GET].append | ||
Martin von Zweigbergk
|
r41072 | mf = repo['.'].manifest() | ||
for f in files: | ||||
if not repo.wvfs.exists(f): | ||||
addgaction((f, (mf.flags(f), False), "narrowspec updated")) | ||||
merge.applyupdates(repo, actions, wctx=repo[None], | ||||
mctx=repo['.'], overwrite=False) | ||||
def checkworkingcopynarrowspec(repo): | ||||
storespec = repo.svfs.tryread(FILENAME) | ||||
wcspec = repo.vfs.tryread(DIRSTATE_FILENAME) | ||||
if wcspec != storespec: | ||||
raise error.Abort(_("working copy's narrowspec is stale"), | ||||
hint=_("run 'hg tracked --update-working-copy'")) | ||||
Martin von Zweigbergk
|
r41274 | def updateworkingcopy(repo, assumeclean=False): | ||
"""updates the working copy and dirstate from the store narrowspec | ||||
When assumeclean=True, files that are not known to be clean will also | ||||
be deleted. It is then up to the caller to make sure they are clean. | ||||
""" | ||||
Martin von Zweigbergk
|
r41072 | oldspec = repo.vfs.tryread(DIRSTATE_FILENAME) | ||
newspec = repo.svfs.tryread(FILENAME) | ||||
oldincludes, oldexcludes = parseconfig(repo.ui, oldspec) | ||||
newincludes, newexcludes = parseconfig(repo.ui, newspec) | ||||
oldmatch = match(repo.root, include=oldincludes, exclude=oldexcludes) | ||||
newmatch = match(repo.root, include=newincludes, exclude=newexcludes) | ||||
addedmatch = matchmod.differencematcher(newmatch, oldmatch) | ||||
removedmatch = matchmod.differencematcher(oldmatch, newmatch) | ||||
ds = repo.dirstate | ||||
lookup, status = ds.status(removedmatch, subrepos=[], ignored=False, | ||||
clean=True, unknown=False) | ||||
Martin von Zweigbergk
|
r41274 | trackeddirty = status.modified + status.added | ||
clean = status.clean | ||||
if assumeclean: | ||||
assert not trackeddirty | ||||
clean.extend(lookup) | ||||
else: | ||||
trackeddirty.extend(lookup) | ||||
_deletecleanfiles(repo, clean) | ||||
Martin von Zweigbergk
|
r41072 | for f in sorted(trackeddirty): | ||
repo.ui.status(_('not deleting possibly dirty file %s\n') % f) | ||||
Martin von Zweigbergk
|
r41274 | for f in clean + trackeddirty: | ||
Martin von Zweigbergk
|
r41072 | ds.drop(f) | ||
repo.narrowpats = newincludes, newexcludes | ||||
repo._narrowmatch = newmatch | ||||
pctx = repo['.'] | ||||
newfiles = [f for f in pctx.manifest().walk(addedmatch) if f not in ds] | ||||
for f in newfiles: | ||||
ds.normallookup(f) | ||||
_writeaddedfiles(repo, pctx, newfiles) | ||||