narrowspec.py
356 lines
| 11.4 KiB
| text/x-python
|
PythonLexer
/ mercurial / narrowspec.py
|
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 | ||||
from .i18n import _ | ||||
|
r43359 | from .pycompat import getattr | ||
|
r36178 | from . import ( | ||
error, | ||||
match as matchmod, | ||||
|
r41072 | merge, | ||
|
r45383 | mergestate as mergestatemod, | ||
|
r45932 | requirements, | ||
|
r42326 | scmutil, | ||
|
r38875 | sparse, | ||
|
r36178 | util, | ||
) | ||||
|
r41072 | # The file in .hg/store/ that indicates which paths exit in the store | ||
|
r43347 | FILENAME = b'narrowspec' | ||
|
r41072 | # The file in .hg/ that indicates which paths exit in the dirstate | ||
|
r43347 | DIRSTATE_FILENAME = b'narrowspec.dirstate' | ||
|
r36178 | |||
|
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 | ||||
|
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. | ||||
|
r39567 | VALID_PREFIXES = ( | ||
b'path:', | ||||
b'rootfilesin:', | ||||
) | ||||
|
r43346 | |||
|
r36178 | def normalizesplitpattern(kind, pat): | ||
"""Returns the normalized version of a pattern and kind. | ||||
Returns a tuple with the normalized kind and normalized pattern. | ||||
""" | ||||
|
r43347 | pat = pat.rstrip(b'/') | ||
|
r36178 | _validatepattern(pat) | ||
return kind, pat | ||||
|
r43346 | |||
|
r36178 | 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. | ||||
|
r43347 | return len((s + b'x').splitlines()) | ||
|
r36178 | |||
|
r43346 | |||
|
r36178 | 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: | ||||
|
r43347 | raise error.Abort(_(b'newlines are not allowed in narrowspec paths')) | ||
|
r36178 | |||
|
r43347 | components = pat.split(b'/') | ||
if b'.' in components or b'..' in components: | ||||
raise error.Abort( | ||||
_(b'"." and ".." are not allowed in narrowspec paths') | ||||
) | ||||
|
r36178 | |||
|
r43346 | |||
|
r43347 | def normalizepattern(pattern, defaultkind=b'path'): | ||
|
r36178 | """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) | ||||
|
r43347 | return b'%s:%s' % normalizesplitpattern(kind, pat) | ||
|
r36178 | |||
|
r43346 | |||
|
r36178 | def parsepatterns(pats): | ||
|
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 | ||||
|
r43346 | |||
|
r39567 | 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): | ||||
|
r43346 | raise error.ProgrammingError( | ||
|
r43387 | b'narrow patterns should be a set; got %r' % pats | ||
|
r43346 | ) | ||
|
r39567 | |||
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). | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'invalid prefix on narrow pattern: %s') % pat, | ||
|
r43346 | hint=_( | ||
|
r43347 | b'narrow patterns must begin with one of ' | ||
b'the following: %s' | ||||
|
r43346 | ) | ||
|
r43347 | % b', '.join(VALID_PREFIXES), | ||
|
r43346 | ) | ||
|
r36178 | |||
def format(includes, excludes): | ||||
|
r43347 | output = b'[include]\n' | ||
|
r36178 | for i in sorted(includes - excludes): | ||
|
r43347 | output += i + b'\n' | ||
output += b'[exclude]\n' | ||||
|
r36178 | for e in sorted(excludes): | ||
|
r43347 | output += e + b'\n' | ||
|
r36178 | return output | ||
|
r43346 | |||
|
r36178 | 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. | ||||
|
r41825 | return matchmod.never() | ||
|
r43346 | return matchmod.match( | ||
|
r43347 | root, b'', [], include=include or [], exclude=exclude or [] | ||
|
r43346 | ) | ||
|
r36178 | |||
|
r40726 | def parseconfig(ui, spec): | ||
# maybe we should care about the profiles returned too | ||||
|
r43347 | includepats, excludepats, profiles = sparse.parseconfig(ui, spec, b'narrow') | ||
|
r40726 | if profiles: | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b"including other spec files using '%include' is not" | ||
b" supported in narrowspec" | ||||
|
r43346 | ) | ||
) | ||||
|
r40726 | |||
validatepatterns(includepats) | ||||
validatepatterns(excludepats) | ||||
return includepats, excludepats | ||||
|
r43346 | |||
|
r36178 | def load(repo): | ||
|
r42596 | # Treat "narrowspec does not exist" the same as "narrowspec file exists | ||
# and is empty". | ||||
spec = repo.svfs.tryread(FILENAME) | ||||
|
r40726 | return parseconfig(repo.ui, spec) | ||
|
r36178 | |||
|
r43346 | |||
|
r36178 | def save(repo, includepats, excludepats): | ||
|
r39575 | validatepatterns(includepats) | ||
validatepatterns(excludepats) | ||||
|
r36178 | spec = format(includepats, excludepats) | ||
|
r38908 | repo.svfs.write(FILENAME, spec) | ||
|
r36178 | |||
|
r43346 | |||
|
r41265 | def copytoworkingcopy(repo): | ||
spec = repo.svfs.read(FILENAME) | ||||
repo.vfs.write(DIRSTATE_FILENAME, spec) | ||||
|
r41072 | |||
|
r43346 | |||
|
r38905 | def savebackup(repo, backupname): | ||
|
r45932 | if requirements.NARROW_REQUIREMENT not in repo.requirements: | ||
|
r38905 | return | ||
|
r41070 | svfs = repo.svfs | ||
svfs.tryunlink(backupname) | ||||
util.copyfile(svfs.join(FILENAME), svfs.join(backupname), hardlink=True) | ||||
|
r38872 | |||
|
r43346 | |||
|
r38905 | def restorebackup(repo, backupname): | ||
|
r45932 | if requirements.NARROW_REQUIREMENT not in repo.requirements: | ||
|
r38905 | return | ||
|
r41070 | util.rename(repo.svfs.join(backupname), repo.svfs.join(FILENAME)) | ||
|
r38872 | |||
|
r43346 | |||
|
r41263 | def savewcbackup(repo, backupname): | ||
|
r45932 | if requirements.NARROW_REQUIREMENT not in repo.requirements: | ||
|
r38905 | return | ||
|
r41263 | vfs = repo.vfs | ||
vfs.tryunlink(backupname) | ||||
# It may not exist in old repos | ||||
if vfs.exists(DIRSTATE_FILENAME): | ||||
|
r43346 | util.copyfile( | ||
vfs.join(DIRSTATE_FILENAME), vfs.join(backupname), hardlink=True | ||||
) | ||||
|
r41263 | |||
def restorewcbackup(repo, backupname): | ||||
|
r45932 | if requirements.NARROW_REQUIREMENT not in repo.requirements: | ||
|
r41263 | return | ||
|
r41334 | # It may not exist in old repos | ||
if repo.vfs.exists(backupname): | ||||
util.rename(repo.vfs.join(backupname), repo.vfs.join(DIRSTATE_FILENAME)) | ||||
|
r41263 | |||
|
r43346 | |||
|
r41263 | def clearwcbackup(repo, backupname): | ||
|
r45932 | if requirements.NARROW_REQUIREMENT not in repo.requirements: | ||
|
r41263 | return | ||
|
r41334 | repo.vfs.tryunlink(backupname) | ||
|
r38872 | |||
|
r43346 | |||
|
r36178 | def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes): | ||
|
r46554 | r"""Restricts the patterns according to repo settings, | ||
|
r36178 | 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. | ||||
""" | ||||
res_excludes = set(req_excludes) | ||||
res_excludes.update(repo_excludes) | ||||
invalid_includes = [] | ||||
if not req_includes: | ||||
res_includes = set(repo_includes) | ||||
|
r43347 | elif b'path:.' not in repo_includes: | ||
|
r36178 | 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: | ||||
|
r43347 | if req_include.startswith(repo_include + b'/'): | ||
|
r36178 | valid = True | ||
res_includes.append(req_include) | ||||
break | ||||
if not valid: | ||||
invalid_includes.append(req_include) | ||||
if len(res_includes) == 0: | ||||
|
r43347 | res_excludes = {b'path:.'} | ||
|
r36178 | else: | ||
res_includes = set(res_includes) | ||||
else: | ||||
res_includes = set(req_includes) | ||||
return res_includes, res_excludes, invalid_includes | ||||
|
r41072 | |||
|
r43346 | |||
|
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) | ||||
|
r43346 | |||
|
r41072 | def _writeaddedfiles(repo, pctx, files): | ||
|
r45894 | mresult = merge.mergeresult() | ||
|
r43347 | mf = repo[b'.'].manifest() | ||
|
r41072 | for f in files: | ||
if not repo.wvfs.exists(f): | ||||
|
r45894 | mresult.addfile( | ||
f, | ||||
mergestatemod.ACTION_GET, | ||||
(mf.flags(f), False), | ||||
b"narrowspec updated", | ||||
) | ||||
|
r43346 | merge.applyupdates( | ||
repo, | ||||
|
r45894 | mresult, | ||
|
r43346 | wctx=repo[None], | ||
|
r43347 | mctx=repo[b'.'], | ||
|
r43346 | overwrite=False, | ||
wantfiledata=False, | ||||
) | ||||
|
r41072 | |||
def checkworkingcopynarrowspec(repo): | ||||
|
r42603 | # Avoid infinite recursion when updating the working copy | ||
if getattr(repo, '_updatingnarrowspec', False): | ||||
return | ||||
|
r41072 | storespec = repo.svfs.tryread(FILENAME) | ||
wcspec = repo.vfs.tryread(DIRSTATE_FILENAME) | ||||
if wcspec != storespec: | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"working copy's narrowspec is stale"), | ||
hint=_(b"run 'hg tracked --update-working-copy'"), | ||||
|
r43346 | ) | ||
|
r41072 | |||
|
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. | ||||
""" | ||||
|
r41072 | oldspec = repo.vfs.tryread(DIRSTATE_FILENAME) | ||
newspec = repo.svfs.tryread(FILENAME) | ||||
|
r42603 | repo._updatingnarrowspec = True | ||
|
r41072 | |||
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 | ||||
|
r43346 | lookup, status = ds.status( | ||
removedmatch, subrepos=[], ignored=True, clean=True, unknown=True | ||||
) | ||||
|
r41274 | trackeddirty = status.modified + status.added | ||
clean = status.clean | ||||
if assumeclean: | ||||
clean.extend(lookup) | ||||
else: | ||||
trackeddirty.extend(lookup) | ||||
_deletecleanfiles(repo, clean) | ||||
|
r42326 | uipathfn = scmutil.getuipathfn(repo) | ||
|
r41072 | for f in sorted(trackeddirty): | ||
|
r43347 | repo.ui.status( | ||
_(b'not deleting possibly dirty file %s\n') % uipathfn(f) | ||||
) | ||||
|
r42352 | for f in sorted(status.unknown): | ||
|
r43347 | repo.ui.status(_(b'not deleting unknown file %s\n') % uipathfn(f)) | ||
|
r42352 | for f in sorted(status.ignored): | ||
|
r43347 | repo.ui.status(_(b'not deleting ignored file %s\n') % uipathfn(f)) | ||
|
r41274 | for f in clean + trackeddirty: | ||
r48552 | ds.update_file(f, p1_tracked=False, wc_tracked=False) | |||
|
r41072 | |||
|
r43347 | pctx = repo[b'.'] | ||
|
r48084 | |||
# only update added files that are in the sparse checkout | ||||
addedmatch = matchmod.intersectmatchers(addedmatch, sparse.matcher(repo)) | ||||
|
r41072 | newfiles = [f for f in pctx.manifest().walk(addedmatch) if f not in ds] | ||
for f in newfiles: | ||||
r48539 | ds.update_file(f, p1_tracked=True, wc_tracked=True, possibly_dirty=True) | |||
|
r41072 | _writeaddedfiles(repo, pctx, newfiles) | ||
|
r42603 | repo._updatingnarrowspec = False | ||