sparse.py
830 lines
| 25.3 KiB
| text/x-python
|
PythonLexer
/ mercurial / sparse.py
|
r33297 | # sparse.py - functionality for sparse checkouts | ||
# | ||||
# Copyright 2014 Facebook, 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 | ||||
|
r33320 | import os | ||
|
r33317 | |||
|
r33297 | from .i18n import _ | ||
|
r47771 | from .node import hex | ||
|
r33297 | from . import ( | ||
error, | ||||
|
r33320 | match as matchmod, | ||
|
r33321 | merge as mergemod, | ||
|
r45383 | mergestate as mergestatemod, | ||
|
r33648 | pathutil, | ||
|
r33320 | pycompat, | ||
|
r45932 | requirements, | ||
|
r33556 | scmutil, | ||
|
r33371 | util, | ||
|
r33297 | ) | ||
|
r44517 | from .utils import hashutil | ||
|
r33297 | |||
|
r45914 | |||
|
r33299 | # Whether sparse features are enabled. This variable is intended to be | ||
# temporary to facilitate porting sparse to core. It should eventually be | ||||
# a per-repo option, possibly a repo requirement. | ||||
enabled = False | ||||
|
r43346 | |||
|
r38874 | def parseconfig(ui, raw, action): | ||
|
r33297 | """Parse sparse config file content. | ||
|
r38874 | action is the command which is trigerring this read, can be narrow, sparse | ||
|
r33297 | Returns a tuple of includes, excludes, and profiles. | ||
""" | ||||
includes = set() | ||||
excludes = set() | ||||
|
r33550 | profiles = set() | ||
|
r33551 | current = None | ||
havesection = False | ||||
|
r43347 | for line in raw.split(b'\n'): | ||
|
r33297 | line = line.strip() | ||
|
r43347 | if not line or line.startswith(b'#'): | ||
|
r33297 | # empty or comment line, skip | ||
continue | ||||
|
r43347 | elif line.startswith(b'%include '): | ||
|
r33297 | line = line[9:].strip() | ||
if line: | ||||
|
r33550 | profiles.add(line) | ||
|
r43347 | elif line == b'[include]': | ||
|
r33551 | if havesection and current != includes: | ||
|
r33297 | # TODO pass filename into this API so we can report it. | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'%(action)s config cannot have includes ' | ||
b'after excludes' | ||||
|
r43346 | ) | ||
|
r43347 | % {b'action': action} | ||
|
r43346 | ) | ||
|
r33551 | havesection = True | ||
current = includes | ||||
|
r33297 | continue | ||
|
r43347 | elif line == b'[exclude]': | ||
|
r33551 | havesection = True | ||
|
r33297 | current = excludes | ||
elif line: | ||||
|
r33551 | if current is None: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _( | ||
b'%(action)s config entry outside of ' | ||||
b'section: %(line)s' | ||||
) | ||||
% {b'action': action, b'line': line}, | ||||
|
r43346 | hint=_( | ||
|
r43347 | b'add an [include] or [exclude] line ' | ||
b'to declare the entry type' | ||||
|
r43346 | ), | ||
) | ||||
|
r33551 | |||
|
r43347 | if line.strip().startswith(b'/'): | ||
|
r43346 | ui.warn( | ||
_( | ||||
|
r43347 | b'warning: %(action)s profile cannot use' | ||
b' paths starting with /, ignoring %(line)s\n' | ||||
|
r43346 | ) | ||
|
r43347 | % {b'action': action, b'line': line} | ||
|
r43346 | ) | ||
|
r33297 | continue | ||
current.add(line) | ||||
return includes, excludes, profiles | ||||
|
r33298 | |||
|
r43346 | |||
|
r33298 | # Exists as separate function to facilitate monkeypatching. | ||
def readprofile(repo, profile, changeid): | ||||
"""Resolve the raw content of a sparse profile file.""" | ||||
# TODO add some kind of cache here because this incurs a manifest | ||||
# resolve and can be slow. | ||||
return repo.filectx(profile, changeid=changeid).data() | ||||
|
r33300 | |||
|
r43346 | |||
|
r33300 | def patternsforrev(repo, rev): | ||
"""Obtain sparse checkout patterns for the given rev. | ||||
Returns a tuple of iterables representing includes, excludes, and | ||||
patterns. | ||||
""" | ||||
# Feature isn't enabled. No-op. | ||||
if not enabled: | ||||
|
r33550 | return set(), set(), set() | ||
|
r33300 | |||
|
r43347 | raw = repo.vfs.tryread(b'sparse') | ||
|
r33300 | if not raw: | ||
|
r33550 | return set(), set(), set() | ||
|
r33300 | |||
if rev is None: | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'cannot parse sparse patterns from working directory') | ||
|
r43346 | ) | ||
|
r33300 | |||
|
r43347 | includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse') | ||
|
r33300 | ctx = repo[rev] | ||
if profiles: | ||||
visited = set() | ||||
while profiles: | ||||
profile = profiles.pop() | ||||
if profile in visited: | ||||
continue | ||||
visited.add(profile) | ||||
try: | ||||
raw = readprofile(repo, profile, rev) | ||||
except error.ManifestLookupError: | ||||
msg = ( | ||||
|
r43347 | b"warning: sparse profile '%s' not found " | ||
b"in rev %s - ignoring it\n" % (profile, ctx) | ||||
|
r43346 | ) | ||
|
r33300 | # experimental config: sparse.missingwarning | ||
|
r43347 | if repo.ui.configbool(b'sparse', b'missingwarning'): | ||
|
r33300 | repo.ui.warn(msg) | ||
else: | ||||
repo.ui.debug(msg) | ||||
continue | ||||
|
r43347 | pincludes, pexcludes, subprofs = parseconfig( | ||
repo.ui, raw, b'sparse' | ||||
) | ||||
|
r33300 | includes.update(pincludes) | ||
excludes.update(pexcludes) | ||||
|
r33550 | profiles.update(subprofs) | ||
|
r33300 | |||
profiles = visited | ||||
if includes: | ||||
|
r43347 | includes.add(b'.hg*') | ||
|
r33300 | |||
return includes, excludes, profiles | ||||
|
r33301 | |||
|
r43346 | |||
|
r33370 | def activeconfig(repo): | ||
"""Determine the active sparse config rules. | ||||
Rules are constructed by reading the current sparse config and bringing in | ||||
referenced profiles from parents of the working directory. | ||||
""" | ||||
|
r43346 | revs = [ | ||
repo.changelog.rev(node) | ||||
for node in repo.dirstate.parents() | ||||
|
r47771 | if node != repo.nullid | ||
|
r43346 | ] | ||
|
r33301 | |||
|
r33370 | allincludes = set() | ||
allexcludes = set() | ||||
allprofiles = set() | ||||
|
r33301 | for rev in revs: | ||
|
r33370 | includes, excludes, profiles = patternsforrev(repo, rev) | ||
allincludes |= includes | ||||
allexcludes |= excludes | ||||
|
r33550 | allprofiles |= profiles | ||
|
r33301 | |||
|
r33370 | return allincludes, allexcludes, allprofiles | ||
|
r33302 | |||
|
r43346 | |||
|
r33317 | def configsignature(repo, includetemp=True): | ||
"""Obtain the signature string for the current sparse configuration. | ||||
This is used to construct a cache key for matchers. | ||||
""" | ||||
cache = repo._sparsesignaturecache | ||||
|
r43347 | signature = cache.get(b'signature') | ||
|
r33317 | |||
if includetemp: | ||||
|
r43347 | tempsignature = cache.get(b'tempsignature') | ||
|
r33317 | else: | ||
|
r43347 | tempsignature = b'0' | ||
|
r33317 | |||
if signature is None or (includetemp and tempsignature is None): | ||||
|
r44517 | signature = hex(hashutil.sha1(repo.vfs.tryread(b'sparse')).digest()) | ||
|
r43347 | cache[b'signature'] = signature | ||
|
r33317 | |||
if includetemp: | ||||
|
r43347 | raw = repo.vfs.tryread(b'tempsparse') | ||
|
r44517 | tempsignature = hex(hashutil.sha1(raw).digest()) | ||
|
r43347 | cache[b'tempsignature'] = tempsignature | ||
|
r33317 | |||
|
r43347 | return b'%s %s' % (signature, tempsignature) | ||
|
r33317 | |||
|
r43346 | |||
|
r33303 | def writeconfig(repo, includes, excludes, profiles): | ||
"""Write the sparse config file given a sparse configuration.""" | ||||
|
r43347 | with repo.vfs(b'sparse', b'wb') as fh: | ||
|
r33303 | for p in sorted(profiles): | ||
|
r43347 | fh.write(b'%%include %s\n' % p) | ||
|
r33303 | |||
if includes: | ||||
|
r43347 | fh.write(b'[include]\n') | ||
|
r33303 | for i in sorted(includes): | ||
fh.write(i) | ||||
|
r43347 | fh.write(b'\n') | ||
|
r33303 | |||
if excludes: | ||||
|
r43347 | fh.write(b'[exclude]\n') | ||
|
r33303 | for e in sorted(excludes): | ||
fh.write(e) | ||||
|
r43347 | fh.write(b'\n') | ||
|
r33303 | |||
|
r33325 | repo._sparsesignaturecache.clear() | ||
|
r33304 | |||
|
r43346 | |||
|
r33304 | def readtemporaryincludes(repo): | ||
|
r43347 | raw = repo.vfs.tryread(b'tempsparse') | ||
|
r33304 | if not raw: | ||
return set() | ||||
|
r43347 | return set(raw.split(b'\n')) | ||
|
r33304 | |||
|
r43346 | |||
|
r33304 | def writetemporaryincludes(repo, includes): | ||
|
r43347 | repo.vfs.write(b'tempsparse', b'\n'.join(sorted(includes))) | ||
|
r33325 | repo._sparsesignaturecache.clear() | ||
|
r33304 | |||
|
r43346 | |||
|
r33304 | def addtemporaryincludes(repo, additional): | ||
includes = readtemporaryincludes(repo) | ||||
for i in additional: | ||||
includes.add(i) | ||||
writetemporaryincludes(repo, includes) | ||||
|
r33320 | |||
|
r43346 | |||
|
r33321 | def prunetemporaryincludes(repo): | ||
|
r43347 | if not enabled or not repo.vfs.exists(b'tempsparse'): | ||
|
r33321 | return | ||
|
r33356 | s = repo.status() | ||
if s.modified or s.added or s.removed or s.deleted: | ||||
|
r33321 | # Still have pending changes. Don't bother trying to prune. | ||
return | ||||
sparsematch = matcher(repo, includetemp=False) | ||||
dirstate = repo.dirstate | ||||
|
r45894 | mresult = mergemod.mergeresult() | ||
|
r33321 | dropped = [] | ||
tempincludes = readtemporaryincludes(repo) | ||||
for file in tempincludes: | ||||
if file in dirstate and not sparsematch(file): | ||||
|
r43347 | message = _(b'dropping temporarily included sparse files') | ||
|
r45899 | mresult.addfile(file, mergestatemod.ACTION_REMOVE, None, message) | ||
|
r33321 | dropped.append(file) | ||
|
r43346 | mergemod.applyupdates( | ||
|
r45894 | repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False | ||
|
r43346 | ) | ||
|
r33321 | |||
# Fix dirstate | ||||
for file in dropped: | ||||
dirstate.drop(file) | ||||
|
r43347 | repo.vfs.unlink(b'tempsparse') | ||
|
r33325 | repo._sparsesignaturecache.clear() | ||
|
r43346 | msg = _( | ||
|
r43347 | b'cleaned up %d temporarily added file(s) from the ' | ||
b'sparse checkout\n' | ||||
|
r43346 | ) | ||
|
r33321 | repo.ui.status(msg % len(tempincludes)) | ||
|
r43346 | |||
|
r33447 | def forceincludematcher(matcher, includes): | ||
"""Returns a matcher that returns true for any of the forced includes | ||||
before testing against the actual matcher.""" | ||||
|
r43347 | kindpats = [(b'path', include, b'') for include in includes] | ||
includematcher = matchmod.includematcher(b'', kindpats) | ||||
|
r33447 | return matchmod.unionmatcher([includematcher, matcher]) | ||
|
r43346 | |||
|
r33320 | def matcher(repo, revs=None, includetemp=True): | ||
"""Obtain a matcher for sparse working directories for the given revs. | ||||
If multiple revisions are specified, the matcher is the union of all | ||||
revs. | ||||
``includetemp`` indicates whether to use the temporary sparse profile. | ||||
""" | ||||
# If sparse isn't enabled, sparse matcher matches everything. | ||||
if not enabled: | ||||
|
r41825 | return matchmod.always() | ||
|
r33320 | |||
if not revs or revs == [None]: | ||||
|
r43346 | revs = [ | ||
repo.changelog.rev(node) | ||||
for node in repo.dirstate.parents() | ||||
|
r47771 | if node != repo.nullid | ||
|
r43346 | ] | ||
|
r33320 | |||
signature = configsignature(repo, includetemp=includetemp) | ||||
|
r43347 | key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs))) | ||
|
r33320 | |||
result = repo._sparsematchercache.get(key) | ||||
if result: | ||||
return result | ||||
matchers = [] | ||||
for rev in revs: | ||||
try: | ||||
includes, excludes, profiles = patternsforrev(repo, rev) | ||||
if includes or excludes: | ||||
|
r43346 | matcher = matchmod.match( | ||
repo.root, | ||||
|
r43347 | b'', | ||
|
r43346 | [], | ||
include=includes, | ||||
exclude=excludes, | ||||
|
r43347 | default=b'relpath', | ||
|
r43346 | ) | ||
|
r33320 | matchers.append(matcher) | ||
except IOError: | ||||
pass | ||||
if not matchers: | ||||
|
r41825 | result = matchmod.always() | ||
|
r33320 | elif len(matchers) == 1: | ||
result = matchers[0] | ||||
else: | ||||
result = matchmod.unionmatcher(matchers) | ||||
if includetemp: | ||||
tempincludes = readtemporaryincludes(repo) | ||||
|
r33447 | result = forceincludematcher(result, tempincludes) | ||
|
r33320 | |||
repo._sparsematchercache[key] = result | ||||
return result | ||||
|
r33322 | |||
|
r43346 | |||
|
r45847 | def filterupdatesactions(repo, wctx, mctx, branchmerge, mresult): | ||
|
r33323 | """Filter updates to only lay out files that match the sparse rules.""" | ||
if not enabled: | ||||
|
r45847 | return | ||
|
r33322 | |||
oldrevs = [pctx.rev() for pctx in wctx.parents()] | ||||
oldsparsematch = matcher(repo, oldrevs) | ||||
if oldsparsematch.always(): | ||||
|
r45847 | return | ||
|
r33322 | |||
files = set() | ||||
prunedactions = {} | ||||
if branchmerge: | ||||
# If we're merging, use the wctx filter, since we're merging into | ||||
# the wctx. | ||||
|
r41442 | sparsematch = matcher(repo, [wctx.p1().rev()]) | ||
|
r33322 | else: | ||
# If we're updating, use the target context's filter, since we're | ||||
# moving to the target context. | ||||
sparsematch = matcher(repo, [mctx.rev()]) | ||||
temporaryfiles = [] | ||||
|
r45906 | for file, action in mresult.filemap(): | ||
|
r33322 | type, args, msg = action | ||
files.add(file) | ||||
if sparsematch(file): | ||||
prunedactions[file] = action | ||||
|
r45851 | elif type == mergestatemod.ACTION_MERGE: | ||
|
r33322 | temporaryfiles.append(file) | ||
prunedactions[file] = action | ||||
elif branchmerge: | ||||
|
r46096 | if type not in mergestatemod.NO_OP_ACTIONS: | ||
|
r33322 | temporaryfiles.append(file) | ||
prunedactions[file] = action | ||||
|
r45851 | elif type == mergestatemod.ACTION_FORGET: | ||
|
r33322 | prunedactions[file] = action | ||
elif file in wctx: | ||||
|
r45851 | prunedactions[file] = (mergestatemod.ACTION_REMOVE, args, msg) | ||
|
r33322 | |||
|
r45852 | # in case or rename on one side, it is possible that f1 might not | ||
# be present in sparse checkout we should include it | ||||
# TODO: should we do the same for f2? | ||||
# exists as a separate check because file can be in sparse and hence | ||||
# if we try to club this condition in above `elif type == ACTION_MERGE` | ||||
# it won't be triggered | ||||
|
r45383 | if branchmerge and type == mergestatemod.ACTION_MERGE: | ||
|
r39563 | f1, f2, fa, move, anc = args | ||
if not sparsematch(f1): | ||||
temporaryfiles.append(f1) | ||||
|
r33322 | if len(temporaryfiles) > 0: | ||
|
r43346 | repo.ui.status( | ||
_( | ||||
|
r43347 | b'temporarily included %d file(s) in the sparse ' | ||
b'checkout for merging\n' | ||||
|
r43346 | ) | ||
% len(temporaryfiles) | ||||
) | ||||
|
r33322 | addtemporaryincludes(repo, temporaryfiles) | ||
# Add the new files to the working copy so they can be merged, etc | ||||
|
r45894 | tmresult = mergemod.mergeresult() | ||
|
r43347 | message = b'temporarily adding to sparse checkout' | ||
|
r33322 | wctxmanifest = repo[None].manifest() | ||
for file in temporaryfiles: | ||||
if file in wctxmanifest: | ||||
fctx = repo[None][file] | ||||
|
r45894 | tmresult.addfile( | ||
file, | ||||
mergestatemod.ACTION_GET, | ||||
(fctx.flags(), False), | ||||
message, | ||||
) | ||||
|
r33322 | |||
|
r43346 | mergemod.applyupdates( | ||
|
r45894 | repo, tmresult, repo[None], repo[b'.'], False, wantfiledata=False | ||
|
r43346 | ) | ||
|
r33322 | |||
dirstate = repo.dirstate | ||||
|
r45894 | for file, flags, msg in tmresult.getactions([mergestatemod.ACTION_GET]): | ||
|
r33322 | dirstate.normal(file) | ||
|
r33370 | profiles = activeconfig(repo)[2] | ||
|
r33322 | changedprofiles = profiles & files | ||
# If an active profile changed during the update, refresh the checkout. | ||||
# Don't do this during a branch merge, since all incoming changes should | ||||
# have been handled by the temporary includes above. | ||||
if changedprofiles and not branchmerge: | ||||
mf = mctx.manifest() | ||||
for file in mf: | ||||
old = oldsparsematch(file) | ||||
new = sparsematch(file) | ||||
if not old and new: | ||||
flags = mf.flags(file) | ||||
|
r45851 | prunedactions[file] = ( | ||
mergestatemod.ACTION_GET, | ||||
(flags, False), | ||||
b'', | ||||
) | ||||
|
r33322 | elif old and not new: | ||
|
r45851 | prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'') | ||
|
r33322 | |||
|
r45847 | mresult.setactions(prunedactions) | ||
|
r33324 | |||
|
r43346 | |||
|
r33324 | def refreshwdir(repo, origstatus, origsparsematch, force=False): | ||
"""Refreshes working directory by taking sparse config into account. | ||||
The old status and sparse matcher is compared against the current sparse | ||||
matcher. | ||||
Will abort if a file with pending changes is being excluded or included | ||||
unless ``force`` is True. | ||||
""" | ||||
# Verify there are no pending changes | ||||
pending = set() | ||||
|
r33356 | pending.update(origstatus.modified) | ||
pending.update(origstatus.added) | ||||
pending.update(origstatus.removed) | ||||
|
r33324 | sparsematch = matcher(repo) | ||
abort = False | ||||
for f in pending: | ||||
if not sparsematch(f): | ||||
|
r43347 | repo.ui.warn(_(b"pending changes to '%s'\n") % f) | ||
|
r33324 | abort = not force | ||
if abort: | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'could not update sparseness due to pending changes') | ||
|
r43346 | ) | ||
|
r33324 | |||
|
r45894 | # Calculate merge result | ||
|
r33324 | dirstate = repo.dirstate | ||
|
r43347 | ctx = repo[b'.'] | ||
|
r33324 | added = [] | ||
lookup = [] | ||||
dropped = [] | ||||
mf = ctx.manifest() | ||||
files = set(mf) | ||||
|
r45894 | mresult = mergemod.mergeresult() | ||
|
r33324 | |||
for file in files: | ||||
old = origsparsematch(file) | ||||
new = sparsematch(file) | ||||
# Add files that are newly included, or that don't exist in | ||||
# the dirstate yet. | ||||
if (new and not old) or (old and new and not file in dirstate): | ||||
fl = mf.flags(file) | ||||
if repo.wvfs.exists(file): | ||||
|
r45899 | mresult.addfile(file, mergestatemod.ACTION_EXEC, (fl,), b'') | ||
|
r33324 | lookup.append(file) | ||
else: | ||||
|
r45899 | mresult.addfile( | ||
file, mergestatemod.ACTION_GET, (fl, False), b'' | ||||
) | ||||
|
r33324 | added.append(file) | ||
# Drop files that are newly excluded, or that still exist in | ||||
# the dirstate. | ||||
elif (old and not new) or (not old and not new and file in dirstate): | ||||
dropped.append(file) | ||||
if file not in pending: | ||||
|
r45899 | mresult.addfile(file, mergestatemod.ACTION_REMOVE, [], b'') | ||
|
r33324 | |||
# Verify there are no pending changes in newly included files | ||||
abort = False | ||||
for file in lookup: | ||||
|
r43347 | repo.ui.warn(_(b"pending changes to '%s'\n") % file) | ||
|
r33324 | abort = not force | ||
if abort: | ||||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'cannot change sparseness due to pending ' | ||
b'changes (delete the files or use ' | ||||
b'--force to bring them back dirty)' | ||||
|
r43346 | ) | ||
) | ||||
|
r33324 | |||
# Check for files that were only in the dirstate. | ||||
|
r43376 | for file, state in pycompat.iteritems(dirstate): | ||
|
r33324 | if not file in files: | ||
old = origsparsematch(file) | ||||
new = sparsematch(file) | ||||
if old and not new: | ||||
dropped.append(file) | ||||
|
r43346 | mergemod.applyupdates( | ||
|
r45894 | repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False | ||
|
r43346 | ) | ||
|
r33324 | |||
# Fix dirstate | ||||
for file in added: | ||||
dirstate.normal(file) | ||||
for file in dropped: | ||||
dirstate.drop(file) | ||||
for file in lookup: | ||||
# File exists on disk, and we're bringing it back in an unknown state. | ||||
dirstate.normallookup(file) | ||||
return added, dropped, lookup | ||||
|
r33353 | |||
|
r43346 | |||
|
r33353 | def aftercommit(repo, node): | ||
"""Perform actions after a working directory commit.""" | ||||
# This function is called unconditionally, even if sparse isn't | ||||
# enabled. | ||||
ctx = repo[node] | ||||
profiles = patternsforrev(repo, ctx.rev())[2] | ||||
# profiles will only have data if sparse is enabled. | ||||
|
r33550 | if profiles & set(ctx.files()): | ||
|
r33353 | origstatus = repo.status() | ||
origsparsematch = matcher(repo) | ||||
refreshwdir(repo, origstatus, origsparsematch, force=True) | ||||
prunetemporaryincludes(repo) | ||||
|
r33354 | |||
|
r43346 | |||
def _updateconfigandrefreshwdir( | ||||
repo, includes, excludes, profiles, force=False, removing=False | ||||
): | ||||
|
r33555 | """Update the sparse config and working directory state.""" | ||
|
r43347 | raw = repo.vfs.tryread(b'sparse') | ||
oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse') | ||||
|
r33555 | |||
oldstatus = repo.status() | ||||
oldmatch = matcher(repo) | ||||
|
r33556 | oldrequires = set(repo.requirements) | ||
|
r33555 | |||
# TODO remove this try..except once the matcher integrates better | ||||
# with dirstate. We currently have to write the updated config | ||||
# because that will invalidate the matcher cache and force a | ||||
# re-read. We ideally want to update the cached matcher on the | ||||
# repo instance then flush the new config to disk once wdir is | ||||
# updated. But this requires massive rework to matcher() and its | ||||
# consumers. | ||||
|
r45932 | if requirements.SPARSE_REQUIREMENT in oldrequires and removing: | ||
repo.requirements.discard(requirements.SPARSE_REQUIREMENT) | ||||
|
r45666 | scmutil.writereporequirements(repo) | ||
|
r45932 | elif requirements.SPARSE_REQUIREMENT not in oldrequires: | ||
repo.requirements.add(requirements.SPARSE_REQUIREMENT) | ||||
|
r45666 | scmutil.writereporequirements(repo) | ||
|
r33555 | |||
try: | ||||
|
r33556 | writeconfig(repo, includes, excludes, profiles) | ||
|
r33555 | return refreshwdir(repo, oldstatus, oldmatch, force=force) | ||
except Exception: | ||||
|
r33556 | if repo.requirements != oldrequires: | ||
repo.requirements.clear() | ||||
repo.requirements |= oldrequires | ||||
|
r45666 | scmutil.writereporequirements(repo) | ||
|
r33555 | writeconfig(repo, oldincludes, oldexcludes, oldprofiles) | ||
raise | ||||
|
r43346 | |||
|
r33354 | def clearrules(repo, force=False): | ||
"""Clears include/exclude rules from the sparse config. | ||||
The remaining sparse config only has profiles, if defined. The working | ||||
directory is refreshed, as needed. | ||||
""" | ||||
with repo.wlock(): | ||||
|
r43347 | raw = repo.vfs.tryread(b'sparse') | ||
includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse') | ||||
|
r33354 | |||
if not includes and not excludes: | ||||
return | ||||
|
r33555 | _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force) | ||
|
r33355 | |||
|
r43346 | |||
|
r33371 | def importfromfiles(repo, opts, paths, force=False): | ||
"""Import sparse config rules from files. | ||||
The updated sparse config is written out and the working directory | ||||
is refreshed, as needed. | ||||
""" | ||||
with repo.wlock(): | ||||
# read current configuration | ||||
|
r43347 | raw = repo.vfs.tryread(b'sparse') | ||
includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse') | ||||
|
r33371 | aincludes, aexcludes, aprofiles = activeconfig(repo) | ||
# Import rules on top; only take in rules that are not yet | ||||
# part of the active rules. | ||||
changed = False | ||||
for p in paths: | ||||
|
r43347 | with util.posixfile(util.expandpath(p), mode=b'rb') as fh: | ||
|
r33371 | raw = fh.read() | ||
|
r43346 | iincludes, iexcludes, iprofiles = parseconfig( | ||
|
r43347 | repo.ui, raw, b'sparse' | ||
|
r43346 | ) | ||
|
r33371 | oldsize = len(includes) + len(excludes) + len(profiles) | ||
includes.update(iincludes - aincludes) | ||||
excludes.update(iexcludes - aexcludes) | ||||
|
r33550 | profiles.update(iprofiles - aprofiles) | ||
|
r33371 | if len(includes) + len(excludes) + len(profiles) > oldsize: | ||
changed = True | ||||
profilecount = includecount = excludecount = 0 | ||||
fcounts = (0, 0, 0) | ||||
if changed: | ||||
profilecount = len(profiles - aprofiles) | ||||
includecount = len(includes - aincludes) | ||||
excludecount = len(excludes - aexcludes) | ||||
|
r43346 | fcounts = map( | ||
len, | ||||
_updateconfigandrefreshwdir( | ||||
repo, includes, excludes, profiles, force=force | ||||
), | ||||
) | ||||
printchanges( | ||||
repo.ui, opts, profilecount, includecount, excludecount, *fcounts | ||||
) | ||||
|
r33371 | |||
|
r43346 | def updateconfig( | ||
repo, | ||||
pats, | ||||
opts, | ||||
include=False, | ||||
exclude=False, | ||||
reset=False, | ||||
delete=False, | ||||
enableprofile=False, | ||||
disableprofile=False, | ||||
force=False, | ||||
usereporootpaths=False, | ||||
): | ||||
|
r33374 | """Perform a sparse config update. | ||
Only one of the actions may be performed. | ||||
The new config is written out and a working directory refresh is performed. | ||||
""" | ||||
|
r33375 | with repo.wlock(): | ||
|
r43347 | raw = repo.vfs.tryread(b'sparse') | ||
|
r43346 | oldinclude, oldexclude, oldprofiles = parseconfig( | ||
|
r43347 | repo.ui, raw, b'sparse' | ||
|
r43346 | ) | ||
|
r33374 | |||
|
r33376 | if reset: | ||
newinclude = set() | ||||
newexclude = set() | ||||
newprofiles = set() | ||||
else: | ||||
newinclude = set(oldinclude) | ||||
newexclude = set(oldexclude) | ||||
newprofiles = set(oldprofiles) | ||||
|
r33374 | |||
|
r33646 | if any(os.path.isabs(pat) for pat in pats): | ||
|
r43347 | raise error.Abort(_(b'paths cannot be absolute')) | ||
|
r33646 | |||
|
r33648 | if not usereporootpaths: | ||
# let's treat paths as relative to cwd | ||||
root, cwd = repo.root, repo.getcwd() | ||||
abspats = [] | ||||
for kindpat in pats: | ||||
kind, pat = matchmod._patsplit(kindpat, None) | ||||
if kind in matchmod.cwdrelativepatternkinds or kind is None: | ||||
|
r43347 | ap = (kind + b':' if kind else b'') + pathutil.canonpath( | ||
|
r43346 | root, cwd, pat | ||
) | ||||
|
r33648 | abspats.append(ap) | ||
else: | ||||
abspats.append(kindpat) | ||||
pats = abspats | ||||
|
r33646 | if include: | ||
|
r33376 | newinclude.update(pats) | ||
elif exclude: | ||||
newexclude.update(pats) | ||||
elif enableprofile: | ||||
newprofiles.update(pats) | ||||
elif disableprofile: | ||||
newprofiles.difference_update(pats) | ||||
elif delete: | ||||
newinclude.difference_update(pats) | ||||
newexclude.difference_update(pats) | ||||
|
r33374 | |||
|
r43346 | profilecount = len(newprofiles - oldprofiles) - len( | ||
oldprofiles - newprofiles | ||||
) | ||||
includecount = len(newinclude - oldinclude) - len( | ||||
oldinclude - newinclude | ||||
) | ||||
excludecount = len(newexclude - oldexclude) - len( | ||||
oldexclude - newexclude | ||||
) | ||||
|
r33374 | |||
|
r43346 | fcounts = map( | ||
len, | ||||
_updateconfigandrefreshwdir( | ||||
repo, | ||||
newinclude, | ||||
newexclude, | ||||
newprofiles, | ||||
force=force, | ||||
removing=reset, | ||||
), | ||||
) | ||||
|
r33376 | |||
|
r43346 | printchanges( | ||
repo.ui, opts, profilecount, includecount, excludecount, *fcounts | ||||
) | ||||
|
r33374 | |||
|
r43346 | def printchanges( | ||
ui, | ||||
opts, | ||||
profilecount=0, | ||||
includecount=0, | ||||
excludecount=0, | ||||
added=0, | ||||
dropped=0, | ||||
conflicting=0, | ||||
): | ||||
|
r33355 | """Print output summarizing sparse config changes.""" | ||
|
r43347 | with ui.formatter(b'sparse', opts) as fm: | ||
|
r33355 | fm.startitem() | ||
|
r43346 | fm.condwrite( | ||
ui.verbose, | ||||
|
r43347 | b'profiles_added', | ||
_(b'Profiles changed: %d\n'), | ||||
|
r43346 | profilecount, | ||
) | ||||
fm.condwrite( | ||||
ui.verbose, | ||||
|
r43347 | b'include_rules_added', | ||
_(b'Include rules changed: %d\n'), | ||||
|
r43346 | includecount, | ||
) | ||||
fm.condwrite( | ||||
ui.verbose, | ||||
|
r43347 | b'exclude_rules_added', | ||
_(b'Exclude rules changed: %d\n'), | ||||
|
r43346 | excludecount, | ||
) | ||||
|
r33355 | |||
# In 'plain' verbose mode, mergemod.applyupdates already outputs what | ||||
# files are added or removed outside of the templating formatter | ||||
# framework. No point in repeating ourselves in that case. | ||||
if not fm.isplain(): | ||||
|
r43346 | fm.condwrite( | ||
|
r43347 | ui.verbose, b'files_added', _(b'Files added: %d\n'), added | ||
|
r43346 | ) | ||
fm.condwrite( | ||||
|
r43347 | ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped | ||
|
r43346 | ) | ||
fm.condwrite( | ||||
ui.verbose, | ||||
|
r43347 | b'files_conflicting', | ||
_(b'Files conflicting: %d\n'), | ||||
|
r43346 | conflicting, | ||
) | ||||