narrowspec.py
393 lines
| 12.5 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. | ||||
r51081 | import weakref | |||
Gregory Szorc
|
r36178 | |||
from .i18n import _ | ||||
Gregory Szorc
|
r43359 | from .pycompat import getattr | ||
Gregory Szorc
|
r36178 | from . import ( | ||
error, | ||||
match as matchmod, | ||||
Martin von Zweigbergk
|
r41072 | merge, | ||
Augie Fackler
|
r45383 | mergestate as mergestatemod, | ||
Martin von Zweigbergk
|
r42326 | scmutil, | ||
Pulkit Goyal
|
r38875 | sparse, | ||
r51146 | txnutil, | |||
Gregory Szorc
|
r36178 | util, | ||
) | ||||
Martin von Zweigbergk
|
r41072 | # The file in .hg/store/ that indicates which paths exit in the store | ||
Augie Fackler
|
r43347 | FILENAME = b'narrowspec' | ||
Martin von Zweigbergk
|
r41072 | # The file in .hg/ that indicates which paths exit in the dirstate | ||
Augie Fackler
|
r43347 | DIRSTATE_FILENAME = b'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:', | ||||
) | ||||
Augie Fackler
|
r43346 | |||
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. | ||||
""" | ||||
Augie Fackler
|
r43347 | pat = pat.rstrip(b'/') | ||
Gregory Szorc
|
r36178 | _validatepattern(pat) | ||
return kind, pat | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
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. | ||||
Augie Fackler
|
r43347 | return len((s + b'x').splitlines()) | ||
Gregory Szorc
|
r36178 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
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: | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'newlines are not allowed in narrowspec paths')) | ||
Gregory Szorc
|
r36178 | |||
Augie Fackler
|
r43347 | components = pat.split(b'/') | ||
if b'.' in components or b'..' in components: | ||||
raise error.Abort( | ||||
_(b'"." and ".." are not allowed in narrowspec paths') | ||||
) | ||||
Gregory Szorc
|
r36178 | |||
Augie Fackler
|
r43346 | |||
Augie Fackler
|
r43347 | def normalizepattern(pattern, defaultkind=b'path'): | ||
Gregory Szorc
|
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) | ||||
Augie Fackler
|
r43347 | return b'%s:%s' % normalizesplitpattern(kind, pat) | ||
Gregory Szorc
|
r36178 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36178 | 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 | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
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). | ||||
""" | ||||
Augie Fackler
|
r49626 | with util.timedcm('narrowspec.validatepatterns(pats size=%d)', len(pats)): | ||
if not isinstance(pats, set): | ||||
raise error.ProgrammingError( | ||||
b'narrow patterns should be a set; got %r' % pats | ||||
) | ||||
Gregory Szorc
|
r39567 | |||
Augie Fackler
|
r49626 | 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( | ||||
_(b'invalid prefix on narrow pattern: %s') % pat, | ||||
hint=_( | ||||
b'narrow patterns must begin with one of ' | ||||
b'the following: %s' | ||||
) | ||||
% b', '.join(VALID_PREFIXES), | ||||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r36178 | |||
def format(includes, excludes): | ||||
Augie Fackler
|
r43347 | output = b'[include]\n' | ||
Gregory Szorc
|
r36178 | for i in sorted(includes - excludes): | ||
Augie Fackler
|
r43347 | output += i + b'\n' | ||
output += b'[exclude]\n' | ||||
Gregory Szorc
|
r36178 | for e in sorted(excludes): | ||
Augie Fackler
|
r43347 | output += e + b'\n' | ||
Gregory Szorc
|
r36178 | return output | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
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. | ||||
Martin von Zweigbergk
|
r41825 | return matchmod.never() | ||
Augie Fackler
|
r43346 | return matchmod.match( | ||
Augie Fackler
|
r43347 | root, b'', [], include=include or [], exclude=exclude or [] | ||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r36178 | |||
Martin von Zweigbergk
|
r40726 | def parseconfig(ui, spec): | ||
# maybe we should care about the profiles returned too | ||||
Augie Fackler
|
r43347 | includepats, excludepats, profiles = sparse.parseconfig(ui, spec, b'narrow') | ||
Martin von Zweigbergk
|
r40726 | if profiles: | ||
Augie Fackler
|
r43346 | raise error.Abort( | ||
_( | ||||
Augie Fackler
|
r43347 | b"including other spec files using '%include' is not" | ||
b" supported in narrowspec" | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Martin von Zweigbergk
|
r40726 | |||
validatepatterns(includepats) | ||||
validatepatterns(excludepats) | ||||
return includepats, excludepats | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36178 | def load(repo): | ||
Martin von Zweigbergk
|
r42596 | # Treat "narrowspec does not exist" the same as "narrowspec file exists | ||
# and is empty". | ||||
r51146 | spec = None | |||
if txnutil.mayhavepending(repo.root): | ||||
pending_path = b"%s.pending" % FILENAME | ||||
if repo.svfs.exists(pending_path): | ||||
spec = repo.svfs.tryread(FILENAME) | ||||
if spec is None: | ||||
spec = repo.svfs.tryread(FILENAME) | ||||
Martin von Zweigbergk
|
r40726 | return parseconfig(repo.ui, spec) | ||
Gregory Szorc
|
r36178 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36178 | def save(repo, includepats, excludepats): | ||
r51081 | repo = repo.unfiltered() | |||
Gregory Szorc
|
r39575 | validatepatterns(includepats) | ||
validatepatterns(excludepats) | ||||
Gregory Szorc
|
r36178 | spec = format(includepats, excludepats) | ||
r51081 | ||||
tr = repo.currenttransaction() | ||||
if tr is None: | ||||
r51087 | m = "changing narrow spec outside of a transaction" | |||
raise error.ProgrammingError(m) | ||||
r51081 | else: | |||
# the roundtrip is sometime different | ||||
# not taking any chance for now | ||||
value = parseconfig(repo.ui, spec) | ||||
reporef = weakref.ref(repo) | ||||
def clean_pending(tr): | ||||
r = reporef() | ||||
if r is not None: | ||||
r._pending_narrow_pats = None | ||||
tr.addpostclose(b'narrow-spec', clean_pending) | ||||
tr.addabort(b'narrow-spec', clean_pending) | ||||
repo._pending_narrow_pats = value | ||||
def write_spec(f): | ||||
f.write(spec) | ||||
tr.addfilegenerator( | ||||
# XXX think about order at some point | ||||
b"narrow-spec", | ||||
(FILENAME,), | ||||
write_spec, | ||||
location=b'store', | ||||
) | ||||
Gregory Szorc
|
r36178 | |||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r41265 | def copytoworkingcopy(repo): | ||
r51082 | repo = repo.unfiltered() | |||
tr = repo.currenttransaction() | ||||
r51080 | spec = format(*repo.narrowpats) | |||
r51082 | if tr is None: | |||
r51087 | m = "changing narrow spec outside of a transaction" | |||
raise error.ProgrammingError(m) | ||||
r51082 | else: | |||
reporef = weakref.ref(repo) | ||||
def clean_pending(tr): | ||||
r = reporef() | ||||
if r is not None: | ||||
r._pending_narrow_pats_dirstate = None | ||||
tr.addpostclose(b'narrow-spec-dirstate', clean_pending) | ||||
tr.addabort(b'narrow-spec-dirstate', clean_pending) | ||||
repo._pending_narrow_pats_dirstate = repo.narrowpats | ||||
def write_spec(f): | ||||
f.write(spec) | ||||
tr.addfilegenerator( | ||||
# XXX think about order at some point | ||||
b"narrow-spec-dirstate", | ||||
(DIRSTATE_FILENAME,), | ||||
write_spec, | ||||
location=b'plain', | ||||
) | ||||
Martin von Zweigbergk
|
r41072 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36178 | def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes): | ||
Augie Fackler
|
r46554 | r"""Restricts the patterns according to repo settings, | ||
Gregory Szorc
|
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) | ||||
Augie Fackler
|
r43347 | elif b'path:.' not in repo_includes: | ||
Gregory Szorc
|
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: | ||||
Augie Fackler
|
r43347 | if req_include.startswith(repo_include + b'/'): | ||
Gregory Szorc
|
r36178 | valid = True | ||
res_includes.append(req_include) | ||||
break | ||||
if not valid: | ||||
invalid_includes.append(req_include) | ||||
if len(res_includes) == 0: | ||||
Augie Fackler
|
r43347 | res_excludes = {b'path:.'} | ||
Gregory Szorc
|
r36178 | else: | ||
res_includes = set(res_includes) | ||||
else: | ||||
res_includes = set(req_includes) | ||||
return res_includes, res_excludes, invalid_includes | ||||
Martin von Zweigbergk
|
r41072 | |||
Augie Fackler
|
r43346 | |||
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) | ||||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r41072 | def _writeaddedfiles(repo, pctx, files): | ||
Pulkit Goyal
|
r45894 | mresult = merge.mergeresult() | ||
Augie Fackler
|
r43347 | mf = repo[b'.'].manifest() | ||
Martin von Zweigbergk
|
r41072 | for f in files: | ||
if not repo.wvfs.exists(f): | ||||
Pulkit Goyal
|
r45894 | mresult.addfile( | ||
f, | ||||
mergestatemod.ACTION_GET, | ||||
(mf.flags(f), False), | ||||
b"narrowspec updated", | ||||
) | ||||
Augie Fackler
|
r43346 | merge.applyupdates( | ||
repo, | ||||
Pulkit Goyal
|
r45894 | mresult, | ||
Augie Fackler
|
r43346 | wctx=repo[None], | ||
Augie Fackler
|
r43347 | mctx=repo[b'.'], | ||
Augie Fackler
|
r43346 | overwrite=False, | ||
wantfiledata=False, | ||||
) | ||||
Martin von Zweigbergk
|
r41072 | |||
def checkworkingcopynarrowspec(repo): | ||||
Martin von Zweigbergk
|
r42603 | # Avoid infinite recursion when updating the working copy | ||
if getattr(repo, '_updatingnarrowspec', False): | ||||
return | ||||
r51080 | storespec = repo.narrowpats | |||
r51082 | wcspec = repo._pending_narrow_pats_dirstate | |||
if wcspec is None: | ||||
oldspec = repo.vfs.tryread(DIRSTATE_FILENAME) | ||||
wcspec = parseconfig(repo.ui, oldspec) | ||||
Martin von Zweigbergk
|
r41072 | if wcspec != storespec: | ||
Martin von Zweigbergk
|
r49044 | raise error.StateError( | ||
Augie Fackler
|
r43347 | _(b"working copy's narrowspec is stale"), | ||
hint=_(b"run 'hg tracked --update-working-copy'"), | ||||
Augie Fackler
|
r43346 | ) | ||
Martin von Zweigbergk
|
r41072 | |||
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. | ||||
""" | ||||
r51082 | old = repo._pending_narrow_pats_dirstate | |||
if old is None: | ||||
oldspec = repo.vfs.tryread(DIRSTATE_FILENAME) | ||||
oldincludes, oldexcludes = parseconfig(repo.ui, oldspec) | ||||
else: | ||||
oldincludes, oldexcludes = old | ||||
r51080 | newincludes, newexcludes = repo.narrowpats | |||
Martin von Zweigbergk
|
r42603 | repo._updatingnarrowspec = True | ||
Martin von Zweigbergk
|
r41072 | |||
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) | ||||
r51030 | assert repo.currentwlock() is not None | |||
Martin von Zweigbergk
|
r41072 | ds = repo.dirstate | ||
r51030 | with ds.running_status(repo): | |||
lookup, status, _mtime_boundary = ds.status( | ||||
removedmatch, | ||||
subrepos=[], | ||||
ignored=True, | ||||
clean=True, | ||||
unknown=True, | ||||
) | ||||
Martin von Zweigbergk
|
r41274 | trackeddirty = status.modified + status.added | ||
clean = status.clean | ||||
if assumeclean: | ||||
clean.extend(lookup) | ||||
else: | ||||
trackeddirty.extend(lookup) | ||||
_deletecleanfiles(repo, clean) | ||||
Martin von Zweigbergk
|
r42326 | uipathfn = scmutil.getuipathfn(repo) | ||
Martin von Zweigbergk
|
r41072 | for f in sorted(trackeddirty): | ||
Augie Fackler
|
r43347 | repo.ui.status( | ||
_(b'not deleting possibly dirty file %s\n') % uipathfn(f) | ||||
) | ||||
Martin von Zweigbergk
|
r42352 | for f in sorted(status.unknown): | ||
Augie Fackler
|
r43347 | repo.ui.status(_(b'not deleting unknown file %s\n') % uipathfn(f)) | ||
Martin von Zweigbergk
|
r42352 | for f in sorted(status.ignored): | ||
Augie Fackler
|
r43347 | repo.ui.status(_(b'not deleting ignored file %s\n') % uipathfn(f)) | ||
Martin von Zweigbergk
|
r41274 | for f in clean + trackeddirty: | ||
r48552 | ds.update_file(f, p1_tracked=False, wc_tracked=False) | |||
Martin von Zweigbergk
|
r41072 | |||
Augie Fackler
|
r43347 | pctx = repo[b'.'] | ||
Charles Chamberlain
|
r48084 | |||
# only update added files that are in the sparse checkout | ||||
addedmatch = matchmod.intersectmatchers(addedmatch, sparse.matcher(repo)) | ||||
Martin von Zweigbergk
|
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) | |||
Martin von Zweigbergk
|
r41072 | _writeaddedfiles(repo, pctx, newfiles) | ||
Martin von Zweigbergk
|
r42603 | repo._updatingnarrowspec = False | ||