diff --git a/hgext/lfs/__init__.py b/hgext/lfs/__init__.py --- a/hgext/lfs/__init__.py +++ b/hgext/lfs/__init__.py @@ -127,10 +127,10 @@ from mercurial.i18n import _ from mercurial import ( + bundlecaches, config, context, error, - exchange, extensions, exthelper, filelog, @@ -351,7 +351,7 @@ # Make bundle choose changegroup3 instead of changegroup2. This affects # "hg bundle" command. Note: it does not cover all bundle formats like # "packed1". Using "packed1" with lfs will likely cause trouble. - exchange._bundlespeccontentopts[b"v2"][b"cg.version"] = b"03" + bundlecaches._bundlespeccontentopts[b"v2"][b"cg.version"] = b"03" @eh.filesetpredicate(b'lfs()') diff --git a/mercurial/bundlecaches.py b/mercurial/bundlecaches.py new file mode 100644 --- /dev/null +++ b/mercurial/bundlecaches.py @@ -0,0 +1,422 @@ +# bundlecaches.py - utility to deal with pre-computed bundle for servers +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from .i18n import _ + +from .thirdparty import attr + +from . import ( + error, + sslutil, + util, +) +from .utils import stringutil + +urlreq = util.urlreq + + +@attr.s +class bundlespec(object): + compression = attr.ib() + wirecompression = attr.ib() + version = attr.ib() + wireversion = attr.ib() + params = attr.ib() + contentopts = attr.ib() + + +# Maps bundle version human names to changegroup versions. +_bundlespeccgversions = { + b'v1': b'01', + b'v2': b'02', + b'packed1': b's1', + b'bundle2': b'02', # legacy +} + +# Maps bundle version with content opts to choose which part to bundle +_bundlespeccontentopts = { + b'v1': { + b'changegroup': True, + b'cg.version': b'01', + b'obsolescence': False, + b'phases': False, + b'tagsfnodescache': False, + b'revbranchcache': False, + }, + b'v2': { + b'changegroup': True, + b'cg.version': b'02', + b'obsolescence': False, + b'phases': False, + b'tagsfnodescache': True, + b'revbranchcache': True, + }, + b'packed1': {b'cg.version': b's1'}, +} +_bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2'] + +_bundlespecvariants = { + b"streamv2": { + b"changegroup": False, + b"streamv2": True, + b"tagsfnodescache": False, + b"revbranchcache": False, + } +} + +# Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE. +_bundlespecv1compengines = {b'gzip', b'bzip2', b'none'} + + +def parsebundlespec(repo, spec, strict=True): + """Parse a bundle string specification into parts. + + Bundle specifications denote a well-defined bundle/exchange format. + The content of a given specification should not change over time in + order to ensure that bundles produced by a newer version of Mercurial are + readable from an older version. + + The string currently has the form: + + -[;[;]] + + Where is one of the supported compression formats + and is (currently) a version string. A ";" can follow the type and + all text afterwards is interpreted as URI encoded, ";" delimited key=value + pairs. + + If ``strict`` is True (the default) is required. Otherwise, + it is optional. + + Returns a bundlespec object of (compression, version, parameters). + Compression will be ``None`` if not in strict mode and a compression isn't + defined. + + An ``InvalidBundleSpecification`` is raised when the specification is + not syntactically well formed. + + An ``UnsupportedBundleSpecification`` is raised when the compression or + bundle type/version is not recognized. + + Note: this function will likely eventually return a more complex data + structure, including bundle2 part information. + """ + + def parseparams(s): + if b';' not in s: + return s, {} + + params = {} + version, paramstr = s.split(b';', 1) + + for p in paramstr.split(b';'): + if b'=' not in p: + raise error.InvalidBundleSpecification( + _( + b'invalid bundle specification: ' + b'missing "=" in parameter: %s' + ) + % p + ) + + key, value = p.split(b'=', 1) + key = urlreq.unquote(key) + value = urlreq.unquote(value) + params[key] = value + + return version, params + + if strict and b'-' not in spec: + raise error.InvalidBundleSpecification( + _( + b'invalid bundle specification; ' + b'must be prefixed with compression: %s' + ) + % spec + ) + + if b'-' in spec: + compression, version = spec.split(b'-', 1) + + if compression not in util.compengines.supportedbundlenames: + raise error.UnsupportedBundleSpecification( + _(b'%s compression is not supported') % compression + ) + + version, params = parseparams(version) + + if version not in _bundlespeccgversions: + raise error.UnsupportedBundleSpecification( + _(b'%s is not a recognized bundle version') % version + ) + else: + # Value could be just the compression or just the version, in which + # case some defaults are assumed (but only when not in strict mode). + assert not strict + + spec, params = parseparams(spec) + + if spec in util.compengines.supportedbundlenames: + compression = spec + version = b'v1' + # Generaldelta repos require v2. + if b'generaldelta' in repo.requirements: + version = b'v2' + # Modern compression engines require v2. + if compression not in _bundlespecv1compengines: + version = b'v2' + elif spec in _bundlespeccgversions: + if spec == b'packed1': + compression = b'none' + else: + compression = b'bzip2' + version = spec + else: + raise error.UnsupportedBundleSpecification( + _(b'%s is not a recognized bundle specification') % spec + ) + + # Bundle version 1 only supports a known set of compression engines. + if version == b'v1' and compression not in _bundlespecv1compengines: + raise error.UnsupportedBundleSpecification( + _(b'compression engine %s is not supported on v1 bundles') + % compression + ) + + # The specification for packed1 can optionally declare the data formats + # required to apply it. If we see this metadata, compare against what the + # repo supports and error if the bundle isn't compatible. + if version == b'packed1' and b'requirements' in params: + requirements = set(params[b'requirements'].split(b',')) + missingreqs = requirements - repo.supportedformats + if missingreqs: + raise error.UnsupportedBundleSpecification( + _(b'missing support for repository features: %s') + % b', '.join(sorted(missingreqs)) + ) + + # Compute contentopts based on the version + contentopts = _bundlespeccontentopts.get(version, {}).copy() + + # Process the variants + if b"stream" in params and params[b"stream"] == b"v2": + variant = _bundlespecvariants[b"streamv2"] + contentopts.update(variant) + + engine = util.compengines.forbundlename(compression) + compression, wirecompression = engine.bundletype() + wireversion = _bundlespeccgversions[version] + + return bundlespec( + compression, wirecompression, version, wireversion, params, contentopts + ) + + +def parseclonebundlesmanifest(repo, s): + """Parses the raw text of a clone bundles manifest. + + Returns a list of dicts. The dicts have a ``URL`` key corresponding + to the URL and other keys are the attributes for the entry. + """ + m = [] + for line in s.splitlines(): + fields = line.split() + if not fields: + continue + attrs = {b'URL': fields[0]} + for rawattr in fields[1:]: + key, value = rawattr.split(b'=', 1) + key = util.urlreq.unquote(key) + value = util.urlreq.unquote(value) + attrs[key] = value + + # Parse BUNDLESPEC into components. This makes client-side + # preferences easier to specify since you can prefer a single + # component of the BUNDLESPEC. + if key == b'BUNDLESPEC': + try: + bundlespec = parsebundlespec(repo, value) + attrs[b'COMPRESSION'] = bundlespec.compression + attrs[b'VERSION'] = bundlespec.version + except error.InvalidBundleSpecification: + pass + except error.UnsupportedBundleSpecification: + pass + + m.append(attrs) + + return m + + +def isstreamclonespec(bundlespec): + # Stream clone v1 + if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1': + return True + + # Stream clone v2 + if ( + bundlespec.wirecompression == b'UN' + and bundlespec.wireversion == b'02' + and bundlespec.contentopts.get(b'streamv2') + ): + return True + + return False + + +def filterclonebundleentries(repo, entries, streamclonerequested=False): + """Remove incompatible clone bundle manifest entries. + + Accepts a list of entries parsed with ``parseclonebundlesmanifest`` + and returns a new list consisting of only the entries that this client + should be able to apply. + + There is no guarantee we'll be able to apply all returned entries because + the metadata we use to filter on may be missing or wrong. + """ + newentries = [] + for entry in entries: + spec = entry.get(b'BUNDLESPEC') + if spec: + try: + bundlespec = parsebundlespec(repo, spec, strict=True) + + # If a stream clone was requested, filter out non-streamclone + # entries. + if streamclonerequested and not isstreamclonespec(bundlespec): + repo.ui.debug( + b'filtering %s because not a stream clone\n' + % entry[b'URL'] + ) + continue + + except error.InvalidBundleSpecification as e: + repo.ui.debug(stringutil.forcebytestr(e) + b'\n') + continue + except error.UnsupportedBundleSpecification as e: + repo.ui.debug( + b'filtering %s because unsupported bundle ' + b'spec: %s\n' % (entry[b'URL'], stringutil.forcebytestr(e)) + ) + continue + # If we don't have a spec and requested a stream clone, we don't know + # what the entry is so don't attempt to apply it. + elif streamclonerequested: + repo.ui.debug( + b'filtering %s because cannot determine if a stream ' + b'clone bundle\n' % entry[b'URL'] + ) + continue + + if b'REQUIRESNI' in entry and not sslutil.hassni: + repo.ui.debug( + b'filtering %s because SNI not supported\n' % entry[b'URL'] + ) + continue + + if b'REQUIREDRAM' in entry: + try: + requiredram = util.sizetoint(entry[b'REQUIREDRAM']) + except error.ParseError: + repo.ui.debug( + b'filtering %s due to a bad REQUIREDRAM attribute\n' + % entry[b'URL'] + ) + continue + actualram = repo.ui.estimatememory() + if actualram is not None and actualram * 0.66 < requiredram: + repo.ui.debug( + b'filtering %s as it needs more than 2/3 of system memory\n' + % entry[b'URL'] + ) + continue + + newentries.append(entry) + + return newentries + + +class clonebundleentry(object): + """Represents an item in a clone bundles manifest. + + This rich class is needed to support sorting since sorted() in Python 3 + doesn't support ``cmp`` and our comparison is complex enough that ``key=`` + won't work. + """ + + def __init__(self, value, prefers): + self.value = value + self.prefers = prefers + + def _cmp(self, other): + for prefkey, prefvalue in self.prefers: + avalue = self.value.get(prefkey) + bvalue = other.value.get(prefkey) + + # Special case for b missing attribute and a matches exactly. + if avalue is not None and bvalue is None and avalue == prefvalue: + return -1 + + # Special case for a missing attribute and b matches exactly. + if bvalue is not None and avalue is None and bvalue == prefvalue: + return 1 + + # We can't compare unless attribute present on both. + if avalue is None or bvalue is None: + continue + + # Same values should fall back to next attribute. + if avalue == bvalue: + continue + + # Exact matches come first. + if avalue == prefvalue: + return -1 + if bvalue == prefvalue: + return 1 + + # Fall back to next attribute. + continue + + # If we got here we couldn't sort by attributes and prefers. Fall + # back to index order. + return 0 + + def __lt__(self, other): + return self._cmp(other) < 0 + + def __gt__(self, other): + return self._cmp(other) > 0 + + def __eq__(self, other): + return self._cmp(other) == 0 + + def __le__(self, other): + return self._cmp(other) <= 0 + + def __ge__(self, other): + return self._cmp(other) >= 0 + + def __ne__(self, other): + return self._cmp(other) != 0 + + +def sortclonebundleentries(ui, entries): + prefers = ui.configlist(b'ui', b'clonebundleprefers') + if not prefers: + return list(entries) + + def _split(p): + if b'=' not in p: + hint = _(b"each comma separated item should be key=value pairs") + raise error.Abort( + _(b"invalid ui.clonebundleprefers item: %s") % p, hint=hint + ) + return p.split(b'=', 1) + + prefers = [_split(p) for p in prefers] + + items = sorted(clonebundleentry(v, prefers) for v in entries) + return [i.value for i in items] diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -26,6 +26,7 @@ archival, bookmarks, bundle2, + bundlecaches, changegroup, cmdutil, copies, @@ -1547,7 +1548,9 @@ bundletype = opts.get(b'type', b'bzip2').lower() try: - bundlespec = exchange.parsebundlespec(repo, bundletype, strict=False) + bundlespec = bundlecaches.parsebundlespec( + repo, bundletype, strict=False + ) except error.UnsupportedBundleSpecification as e: raise error.Abort( pycompat.bytestr(e), diff --git a/mercurial/exchange.py b/mercurial/exchange.py --- a/mercurial/exchange.py +++ b/mercurial/exchange.py @@ -16,10 +16,10 @@ nullid, nullrev, ) -from .thirdparty import attr from . import ( bookmarks as bookmod, bundle2, + bundlecaches, changegroup, discovery, error, @@ -34,7 +34,6 @@ pycompat, requirements, scmutil, - sslutil, streamclone, url as urlmod, util, @@ -50,202 +49,6 @@ _NARROWACL_SECTION = b'narrowacl' -# Maps bundle version human names to changegroup versions. -_bundlespeccgversions = { - b'v1': b'01', - b'v2': b'02', - b'packed1': b's1', - b'bundle2': b'02', # legacy -} - -# Maps bundle version with content opts to choose which part to bundle -_bundlespeccontentopts = { - b'v1': { - b'changegroup': True, - b'cg.version': b'01', - b'obsolescence': False, - b'phases': False, - b'tagsfnodescache': False, - b'revbranchcache': False, - }, - b'v2': { - b'changegroup': True, - b'cg.version': b'02', - b'obsolescence': False, - b'phases': False, - b'tagsfnodescache': True, - b'revbranchcache': True, - }, - b'packed1': {b'cg.version': b's1'}, -} -_bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2'] - -_bundlespecvariants = { - b"streamv2": { - b"changegroup": False, - b"streamv2": True, - b"tagsfnodescache": False, - b"revbranchcache": False, - } -} - -# Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE. -_bundlespecv1compengines = {b'gzip', b'bzip2', b'none'} - - -@attr.s -class bundlespec(object): - compression = attr.ib() - wirecompression = attr.ib() - version = attr.ib() - wireversion = attr.ib() - params = attr.ib() - contentopts = attr.ib() - - -def parsebundlespec(repo, spec, strict=True): - """Parse a bundle string specification into parts. - - Bundle specifications denote a well-defined bundle/exchange format. - The content of a given specification should not change over time in - order to ensure that bundles produced by a newer version of Mercurial are - readable from an older version. - - The string currently has the form: - - -[;[;]] - - Where is one of the supported compression formats - and is (currently) a version string. A ";" can follow the type and - all text afterwards is interpreted as URI encoded, ";" delimited key=value - pairs. - - If ``strict`` is True (the default) is required. Otherwise, - it is optional. - - Returns a bundlespec object of (compression, version, parameters). - Compression will be ``None`` if not in strict mode and a compression isn't - defined. - - An ``InvalidBundleSpecification`` is raised when the specification is - not syntactically well formed. - - An ``UnsupportedBundleSpecification`` is raised when the compression or - bundle type/version is not recognized. - - Note: this function will likely eventually return a more complex data - structure, including bundle2 part information. - """ - - def parseparams(s): - if b';' not in s: - return s, {} - - params = {} - version, paramstr = s.split(b';', 1) - - for p in paramstr.split(b';'): - if b'=' not in p: - raise error.InvalidBundleSpecification( - _( - b'invalid bundle specification: ' - b'missing "=" in parameter: %s' - ) - % p - ) - - key, value = p.split(b'=', 1) - key = urlreq.unquote(key) - value = urlreq.unquote(value) - params[key] = value - - return version, params - - if strict and b'-' not in spec: - raise error.InvalidBundleSpecification( - _( - b'invalid bundle specification; ' - b'must be prefixed with compression: %s' - ) - % spec - ) - - if b'-' in spec: - compression, version = spec.split(b'-', 1) - - if compression not in util.compengines.supportedbundlenames: - raise error.UnsupportedBundleSpecification( - _(b'%s compression is not supported') % compression - ) - - version, params = parseparams(version) - - if version not in _bundlespeccgversions: - raise error.UnsupportedBundleSpecification( - _(b'%s is not a recognized bundle version') % version - ) - else: - # Value could be just the compression or just the version, in which - # case some defaults are assumed (but only when not in strict mode). - assert not strict - - spec, params = parseparams(spec) - - if spec in util.compengines.supportedbundlenames: - compression = spec - version = b'v1' - # Generaldelta repos require v2. - if b'generaldelta' in repo.requirements: - version = b'v2' - # Modern compression engines require v2. - if compression not in _bundlespecv1compengines: - version = b'v2' - elif spec in _bundlespeccgversions: - if spec == b'packed1': - compression = b'none' - else: - compression = b'bzip2' - version = spec - else: - raise error.UnsupportedBundleSpecification( - _(b'%s is not a recognized bundle specification') % spec - ) - - # Bundle version 1 only supports a known set of compression engines. - if version == b'v1' and compression not in _bundlespecv1compengines: - raise error.UnsupportedBundleSpecification( - _(b'compression engine %s is not supported on v1 bundles') - % compression - ) - - # The specification for packed1 can optionally declare the data formats - # required to apply it. If we see this metadata, compare against what the - # repo supports and error if the bundle isn't compatible. - if version == b'packed1' and b'requirements' in params: - requirements = set(params[b'requirements'].split(b',')) - missingreqs = requirements - repo.supportedformats - if missingreqs: - raise error.UnsupportedBundleSpecification( - _(b'missing support for repository features: %s') - % b', '.join(sorted(missingreqs)) - ) - - # Compute contentopts based on the version - contentopts = _bundlespeccontentopts.get(version, {}).copy() - - # Process the variants - if b"stream" in params and params[b"stream"] == b"v2": - variant = _bundlespecvariants[b"streamv2"] - contentopts.update(variant) - - engine = util.compengines.forbundlename(compression) - compression, wirecompression = engine.bundletype() - wireversion = _bundlespeccgversions[version] - - return bundlespec( - compression, wirecompression, version, wireversion, params, contentopts - ) - def readbundle(ui, fh, fname, vfs=None): header = changegroup.readexactly(fh, 4) @@ -2867,7 +2670,7 @@ # attempt. pullop.clonebundleattempted = True - entries = parseclonebundlesmanifest(repo, res) + entries = bundlecaches.parseclonebundlesmanifest(repo, res) if not entries: repo.ui.note( _( @@ -2877,7 +2680,7 @@ ) return - entries = filterclonebundleentries( + entries = bundlecaches.filterclonebundleentries( repo, entries, streamclonerequested=pullop.streamclonerequested ) @@ -2898,7 +2701,7 @@ ) return - entries = sortclonebundleentries(repo.ui, entries) + entries = bundlecaches.sortclonebundleentries(repo.ui, entries) url = entries[0][b'URL'] repo.ui.status(_(b'applying clone bundle from %s\n') % url) @@ -2923,214 +2726,6 @@ ) -def parseclonebundlesmanifest(repo, s): - """Parses the raw text of a clone bundles manifest. - - Returns a list of dicts. The dicts have a ``URL`` key corresponding - to the URL and other keys are the attributes for the entry. - """ - m = [] - for line in s.splitlines(): - fields = line.split() - if not fields: - continue - attrs = {b'URL': fields[0]} - for rawattr in fields[1:]: - key, value = rawattr.split(b'=', 1) - key = urlreq.unquote(key) - value = urlreq.unquote(value) - attrs[key] = value - - # Parse BUNDLESPEC into components. This makes client-side - # preferences easier to specify since you can prefer a single - # component of the BUNDLESPEC. - if key == b'BUNDLESPEC': - try: - bundlespec = parsebundlespec(repo, value) - attrs[b'COMPRESSION'] = bundlespec.compression - attrs[b'VERSION'] = bundlespec.version - except error.InvalidBundleSpecification: - pass - except error.UnsupportedBundleSpecification: - pass - - m.append(attrs) - - return m - - -def isstreamclonespec(bundlespec): - # Stream clone v1 - if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1': - return True - - # Stream clone v2 - if ( - bundlespec.wirecompression == b'UN' - and bundlespec.wireversion == b'02' - and bundlespec.contentopts.get(b'streamv2') - ): - return True - - return False - - -def filterclonebundleentries(repo, entries, streamclonerequested=False): - """Remove incompatible clone bundle manifest entries. - - Accepts a list of entries parsed with ``parseclonebundlesmanifest`` - and returns a new list consisting of only the entries that this client - should be able to apply. - - There is no guarantee we'll be able to apply all returned entries because - the metadata we use to filter on may be missing or wrong. - """ - newentries = [] - for entry in entries: - spec = entry.get(b'BUNDLESPEC') - if spec: - try: - bundlespec = parsebundlespec(repo, spec, strict=True) - - # If a stream clone was requested, filter out non-streamclone - # entries. - if streamclonerequested and not isstreamclonespec(bundlespec): - repo.ui.debug( - b'filtering %s because not a stream clone\n' - % entry[b'URL'] - ) - continue - - except error.InvalidBundleSpecification as e: - repo.ui.debug(stringutil.forcebytestr(e) + b'\n') - continue - except error.UnsupportedBundleSpecification as e: - repo.ui.debug( - b'filtering %s because unsupported bundle ' - b'spec: %s\n' % (entry[b'URL'], stringutil.forcebytestr(e)) - ) - continue - # If we don't have a spec and requested a stream clone, we don't know - # what the entry is so don't attempt to apply it. - elif streamclonerequested: - repo.ui.debug( - b'filtering %s because cannot determine if a stream ' - b'clone bundle\n' % entry[b'URL'] - ) - continue - - if b'REQUIRESNI' in entry and not sslutil.hassni: - repo.ui.debug( - b'filtering %s because SNI not supported\n' % entry[b'URL'] - ) - continue - - if b'REQUIREDRAM' in entry: - try: - requiredram = util.sizetoint(entry[b'REQUIREDRAM']) - except error.ParseError: - repo.ui.debug( - b'filtering %s due to a bad REQUIREDRAM attribute\n' - % entry[b'URL'] - ) - continue - actualram = repo.ui.estimatememory() - if actualram is not None and actualram * 0.66 < requiredram: - repo.ui.debug( - b'filtering %s as it needs more than 2/3 of system memory\n' - % entry[b'URL'] - ) - continue - - newentries.append(entry) - - return newentries - - -class clonebundleentry(object): - """Represents an item in a clone bundles manifest. - - This rich class is needed to support sorting since sorted() in Python 3 - doesn't support ``cmp`` and our comparison is complex enough that ``key=`` - won't work. - """ - - def __init__(self, value, prefers): - self.value = value - self.prefers = prefers - - def _cmp(self, other): - for prefkey, prefvalue in self.prefers: - avalue = self.value.get(prefkey) - bvalue = other.value.get(prefkey) - - # Special case for b missing attribute and a matches exactly. - if avalue is not None and bvalue is None and avalue == prefvalue: - return -1 - - # Special case for a missing attribute and b matches exactly. - if bvalue is not None and avalue is None and bvalue == prefvalue: - return 1 - - # We can't compare unless attribute present on both. - if avalue is None or bvalue is None: - continue - - # Same values should fall back to next attribute. - if avalue == bvalue: - continue - - # Exact matches come first. - if avalue == prefvalue: - return -1 - if bvalue == prefvalue: - return 1 - - # Fall back to next attribute. - continue - - # If we got here we couldn't sort by attributes and prefers. Fall - # back to index order. - return 0 - - def __lt__(self, other): - return self._cmp(other) < 0 - - def __gt__(self, other): - return self._cmp(other) > 0 - - def __eq__(self, other): - return self._cmp(other) == 0 - - def __le__(self, other): - return self._cmp(other) <= 0 - - def __ge__(self, other): - return self._cmp(other) >= 0 - - def __ne__(self, other): - return self._cmp(other) != 0 - - -def sortclonebundleentries(ui, entries): - prefers = ui.configlist(b'ui', b'clonebundleprefers') - if not prefers: - return list(entries) - - def _split(p): - if b'=' not in p: - hint = _(b"each comma separated item should be key=value pairs") - raise error.Abort( - _(b"invalid ui.clonebundleprefers item: %s") % p, hint=hint - ) - return p.split(b'=', 1) - - prefers = [_split(p) for p in prefers] - - items = sorted(clonebundleentry(v, prefers) for v in entries) - return [i.value for i in items] - - def trypullbundlefromurl(ui, repo, url): """Attempt to apply a bundle from a URL.""" with repo.lock(), repo.transaction(b'bundleurl') as tr: diff --git a/mercurial/wireprotov1server.py b/mercurial/wireprotov1server.py --- a/mercurial/wireprotov1server.py +++ b/mercurial/wireprotov1server.py @@ -19,6 +19,7 @@ from . import ( bundle2, + bundlecaches, changegroup as changegroupmod, discovery, encoding, @@ -387,8 +388,8 @@ manifest = repo.vfs.tryread(b'pullbundles.manifest') if not manifest: return None - res = exchange.parseclonebundlesmanifest(repo, manifest) - res = exchange.filterclonebundleentries(repo, res) + res = bundlecaches.parseclonebundlesmanifest(repo, manifest) + res = bundlecaches.filterclonebundleentries(repo, res) if not res: return None cl = repo.unfiltered().changelog diff --git a/tests/flagprocessorext.py b/tests/flagprocessorext.py --- a/tests/flagprocessorext.py +++ b/tests/flagprocessorext.py @@ -6,8 +6,8 @@ import zlib from mercurial import ( + bundlecaches, changegroup, - exchange, extensions, revlog, util, @@ -134,8 +134,8 @@ revlog.REVIDX_FLAGS_ORDER.extend(flags) # Teach exchange to use changegroup 3 - for k in exchange._bundlespeccontentopts.keys(): - exchange._bundlespeccontentopts[k][b"cg.version"] = b"03" + for k in bundlecaches._bundlespeccontentopts.keys(): + bundlecaches._bundlespeccontentopts[k][b"cg.version"] = b"03" # Register flag processors for each extension flagutil.addflagprocessor( diff --git a/tests/test-pull-bundle.t b/tests/test-pull-bundle.t --- a/tests/test-pull-bundle.t +++ b/tests/test-pull-bundle.t @@ -52,7 +52,7 @@ > 1.hg BUNDLESPEC=bzip2-v2 heads=ed1b79f46b9a29f5a6efa59cf12fcfca43bead5a bases=bbd179dfa0a71671c253b3ae0aa1513b60d199fa > 0.hg BUNDLESPEC=gzip-v2 heads=bbd179dfa0a71671c253b3ae0aa1513b60d199fa > EOF - $ hg --config blackbox.track=debug --debug serve -p $HGPORT2 -d --pid-file=../repo.pid + $ hg --config blackbox.track=debug --debug serve -p $HGPORT2 -d --pid-file=../repo.pid -E ../error.txt listening at http://*:$HGPORT2/ (bound to $LOCALIP:$HGPORT2) (glob) (?) $ cat ../repo.pid >> $DAEMON_PIDS $ cd .. @@ -64,6 +64,7 @@ new changesets bbd179dfa0a7 (1 drafts) updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat error.txt $ cd repo.pullbundle $ hg pull -r 1 pulling from http://localhost:$HGPORT2/