bundlerepo.py
713 lines
| 23.3 KiB
| text/x-python
|
PythonLexer
/ mercurial / bundlerepo.py
|
r8226 | # bundlerepo.py - repository class for viewing uncompressed bundles | ||
# | ||||
# Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r1942 | |||
|
r8227 | """Repository class for viewing uncompressed bundles. | ||
This provides a read-only repository interface to bundles as if they | ||||
were part of the actual repository. | ||||
""" | ||||
|
r25920 | from __future__ import absolute_import | ||
import os | ||||
import shutil | ||||
from .i18n import _ | ||||
|
r46729 | from .node import ( | ||
hex, | ||||
nullrev, | ||||
) | ||||
|
r25920 | |||
from . import ( | ||||
bundle2, | ||||
changegroup, | ||||
changelog, | ||||
cmdutil, | ||||
discovery, | ||||
|
r39843 | encoding, | ||
|
r25920 | error, | ||
exchange, | ||||
filelog, | ||||
localrepo, | ||||
manifest, | ||||
mdiff, | ||||
pathutil, | ||||
phases, | ||||
|
r30519 | pycompat, | ||
|
r25920 | revlog, | ||
r48186 | revlogutils, | |||
|
r25920 | util, | ||
|
r31240 | vfs as vfsmod, | ||
|
r25920 | ) | ||
r47669 | from .utils import ( | |||
urlutil, | ||||
) | ||||
|
r1942 | |||
r47838 | from .revlogutils import ( | |||
constants as revlog_constants, | ||||
) | ||||
|
r43346 | |||
|
r1946 | class bundlerevlog(revlog.revlog): | ||
r47921 | def __init__(self, opener, target, radix, cgunpacker, linkmapper): | |||
|
r1942 | # How it works: | ||
|
r18410 | # To retrieve a revision, we need to know the offset of the revision in | ||
# the bundle (an unbundle object). We store this offset in the index | ||||
|
r18643 | # (start). The base of the delta is stored in the base field. | ||
|
r1942 | # | ||
|
r18410 | # To differentiate a rev in the bundle from a rev in the revlog, we | ||
|
r18643 | # check revision against repotiprev. | ||
|
r31240 | opener = vfsmod.readonlyvfs(opener) | ||
r47921 | revlog.revlog.__init__(self, opener, target=target, radix=radix) | |||
|
r35075 | self.bundle = cgunpacker | ||
|
r6750 | n = len(self) | ||
|
r18643 | self.repotiprev = n - 1 | ||
|
r43346 | self.bundlerevs = set() # used by 'bundle()' revset expression | ||
|
r35075 | for deltadata in cgunpacker.deltaiter(): | ||
|
r47445 | node, p1, p2, cs, deltabase, delta, flags, sidedata = deltadata | ||
|
r14142 | |||
size = len(delta) | ||||
|
r35075 | start = cgunpacker.tell() - size | ||
|
r14142 | |||
r43942 | if self.index.has_node(node): | |||
|
r14142 | # this can happen if two branches make the same change | ||
r43964 | self.bundlerevs.add(self.index.rev(node)) | |||
|
r1942 | continue | ||
|
r46418 | if cs == node: | ||
linkrev = nullrev | ||||
else: | ||||
linkrev = linkmapper(cs) | ||||
|
r14142 | |||
|
r1942 | for p in (p1, p2): | ||
r43942 | if not self.index.has_node(p): | |||
|
r43346 | raise error.LookupError( | ||
r47926 | p, self.display_id, _(b"unknown parent") | |||
|
r43346 | ) | ||
|
r18416 | |||
r43942 | if not self.index.has_node(deltabase): | |||
|
r43346 | raise LookupError( | ||
r47926 | deltabase, self.display_id, _(b'unknown delta base') | |||
|
r43346 | ) | ||
|
r18416 | |||
baserev = self.rev(deltabase) | ||||
r47914 | # start, size, full unc. size, base (unused), link, p1, p2, node, sidedata_offset (unused), sidedata_size (unused) | |||
r48190 | e = revlogutils.entry( | |||
flags=flags, | ||||
data_offset=start, | ||||
data_compressed_length=size, | ||||
data_delta_base=baserev, | ||||
link_rev=linkrev, | ||||
parent_rev_1=self.rev(p1), | ||||
parent_rev_2=self.rev(p2), | ||||
node_id=node, | ||||
|
r43346 | ) | ||
|
r38886 | self.index.append(e) | ||
|
r18411 | self.bundlerevs.add(n) | ||
|
r1942 | n += 1 | ||
|
r35038 | def _chunk(self, rev, df=None): | ||
|
r18643 | # Warning: in case of bundle, the diff is against what we stored as | ||
# delta base, not against rev - 1 | ||||
|
r1942 | # XXX: could use some caching | ||
|
r18643 | if rev <= self.repotiprev: | ||
|
r9676 | return revlog.revlog._chunk(self, rev) | ||
|
r12332 | self.bundle.seek(self.start(rev)) | ||
return self.bundle.read(self.length(rev)) | ||||
|
r1942 | |||
def revdiff(self, rev1, rev2): | ||||
"""return or calculate a delta between two revisions""" | ||||
|
r18643 | if rev1 > self.repotiprev and rev2 > self.repotiprev: | ||
|
r1942 | # hot path for bundle | ||
|
r18643 | revb = self.index[rev2][3] | ||
|
r1942 | if revb == rev1: | ||
|
r9676 | return self._chunk(rev2) | ||
|
r18643 | elif rev1 <= self.repotiprev and rev2 <= self.repotiprev: | ||
|
r4028 | return revlog.revlog.revdiff(self, rev1, rev2) | ||
|
r1942 | |||
|
r43346 | return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2)) | ||
|
r1942 | |||
r43089 | def _rawtext(self, node, rev, _df=None): | |||
if rev is None: | ||||
|
r16375 | rev = self.rev(node) | ||
r43089 | validated = False | |||
|
r31836 | rawtext = None | ||
|
r1942 | chain = [] | ||
|
r18415 | iterrev = rev | ||
|
r1942 | # reconstruct the revision if it is from a changegroup | ||
|
r18643 | while iterrev > self.repotiprev: | ||
|
r40088 | if self._revisioncache and self._revisioncache[1] == iterrev: | ||
rawtext = self._revisioncache[2] | ||||
|
r1942 | break | ||
|
r18415 | chain.append(iterrev) | ||
|
r18643 | iterrev = self.index[iterrev][3] | ||
r43089 | if iterrev == nullrev: | |||
|
r43347 | rawtext = b'' | ||
r43089 | elif rawtext is None: | |||
|
r43346 | r = super(bundlerevlog, self)._rawtext( | ||
self.node(iterrev), iterrev, _df=_df | ||||
) | ||||
r43089 | __, rawtext, validated = r | |||
if chain: | ||||
validated = False | ||||
|
r1942 | while chain: | ||
|
r9676 | delta = self._chunk(chain.pop()) | ||
|
r31836 | rawtext = mdiff.patches(rawtext, [delta]) | ||
r43089 | return rev, rawtext, validated | |||
|
r1942 | |||
|
r35038 | def addrevision(self, *args, **kwargs): | ||
raise NotImplementedError | ||||
def addgroup(self, *args, **kwargs): | ||||
|
r1942 | raise NotImplementedError | ||
|
r35038 | |||
def strip(self, *args, **kwargs): | ||||
|
r1942 | raise NotImplementedError | ||
|
r35038 | |||
|
r1942 | def checksize(self): | ||
raise NotImplementedError | ||||
|
r43346 | |||
|
r1946 | class bundlechangelog(bundlerevlog, changelog.changelog): | ||
|
r35075 | def __init__(self, opener, cgunpacker): | ||
|
r1946 | changelog.changelog.__init__(self, opener) | ||
|
r14142 | linkmapper = lambda x: x | ||
|
r43346 | bundlerevlog.__init__( | ||
r47838 | self, | |||
opener, | ||||
(revlog_constants.KIND_CHANGELOG, None), | ||||
r47921 | self.radix, | |||
r47838 | cgunpacker, | |||
linkmapper, | ||||
|
r43346 | ) | ||
|
r1942 | |||
|
r30373 | class bundlemanifest(bundlerevlog, manifest.manifestrevlog): | ||
|
r43346 | def __init__( | ||
|
r47538 | self, | ||
nodeconstants, | ||||
opener, | ||||
cgunpacker, | ||||
linkmapper, | ||||
dirlogstarts=None, | ||||
dir=b'', | ||||
|
r43346 | ): | ||
|
r47538 | manifest.manifestrevlog.__init__(self, nodeconstants, opener, tree=dir) | ||
|
r43346 | bundlerevlog.__init__( | ||
r47838 | self, | |||
opener, | ||||
(revlog_constants.KIND_MANIFESTLOG, dir), | ||||
r47921 | self._revlog.radix, | |||
r47838 | cgunpacker, | |||
linkmapper, | ||||
|
r43346 | ) | ||
|
r29715 | if dirlogstarts is None: | ||
dirlogstarts = {} | ||||
|
r43347 | if self.bundle.version == b"03": | ||
|
r29715 | dirlogstarts = _getfilestarts(self.bundle) | ||
self._dirlogstarts = dirlogstarts | ||||
self._linkmapper = linkmapper | ||||
|
r1942 | |||
|
r29715 | def dirlog(self, d): | ||
if d in self._dirlogstarts: | ||||
self.bundle.seek(self._dirlogstarts[d]) | ||||
return bundlemanifest( | ||||
|
r47538 | self.nodeconstants, | ||
|
r43346 | self.opener, | ||
self.bundle, | ||||
self._linkmapper, | ||||
self._dirlogstarts, | ||||
dir=d, | ||||
) | ||||
|
r29715 | return super(bundlemanifest, self).dirlog(d) | ||
|
r43346 | |||
|
r37515 | class bundlefilelog(filelog.filelog): | ||
|
r35075 | def __init__(self, opener, path, cgunpacker, linkmapper): | ||
|
r1946 | filelog.filelog.__init__(self, opener, path) | ||
|
r43346 | self._revlog = bundlerevlog( | ||
r47838 | opener, | |||
# XXX should use the unencoded path | ||||
target=(revlog_constants.KIND_FILELOG, path), | ||||
r47921 | radix=self._revlog.radix, | |||
r47838 | cgunpacker=cgunpacker, | |||
linkmapper=linkmapper, | ||||
|
r43346 | ) | ||
|
r14287 | |||
|
r17193 | class bundlepeer(localrepo.localpeer): | ||
def canpush(self): | ||||
return False | ||||
|
r43346 | |||
|
r23631 | class bundlephasecache(phases.phasecache): | ||
def __init__(self, *args, **kwargs): | ||||
super(bundlephasecache, self).__init__(*args, **kwargs) | ||||
|
r43385 | if util.safehasattr(self, 'opener'): | ||
|
r31240 | self.opener = vfsmod.readonlyvfs(self.opener) | ||
|
r23631 | |||
def write(self): | ||||
raise NotImplementedError | ||||
def _write(self, fp): | ||||
raise NotImplementedError | ||||
def _updateroots(self, phase, newroots, tr): | ||||
self.phaseroots[phase] = newroots | ||||
self.invalidate() | ||||
self.dirty = True | ||||
|
r43346 | |||
|
r35075 | def _getfilestarts(cgunpacker): | ||
|
r35076 | filespos = {} | ||
|
r35075 | for chunkdata in iter(cgunpacker.filelogheader, {}): | ||
|
r43347 | fname = chunkdata[b'filename'] | ||
|
r35076 | filespos[fname] = cgunpacker.tell() | ||
|
r35075 | for chunk in iter(lambda: cgunpacker.deltachunk(None), {}): | ||
|
r29712 | pass | ||
|
r35076 | return filespos | ||
|
r29712 | |||
|
r43346 | |||
|
r39640 | class bundlerepository(object): | ||
|
r35041 | """A repository instance that is a union of a local repo and a bundle. | ||
Instances represent a read-only repository composed of a local repository | ||||
with the contents of a bundle file applied. The repository instance is | ||||
conceptually similar to the state of a repository after an | ||||
``hg unbundle`` operation. However, the contents of the bundle are never | ||||
applied to the actual base repository. | ||||
|
r39640 | |||
Instances constructed directly are not usable as repository objects. | ||||
Use instance() or makebundlerepository() to create instances. | ||||
|
r35041 | """ | ||
|
r43346 | |||
|
r39640 | def __init__(self, bundlepath, url, tempparent): | ||
self._tempparent = tempparent | ||||
self._url = url | ||||
|
r43347 | self.ui.setconfig(b'phases', b'publish', False, b'bundlerepo') | ||
|
r2673 | |||
|
r2273 | self.tempfile = None | ||
|
r43347 | f = util.posixfile(bundlepath, b"rb") | ||
|
r39640 | bundle = exchange.readbundle(self.ui, f, bundlepath) | ||
|
r12044 | |||
|
r35044 | if isinstance(bundle, bundle2.unbundle20): | ||
self._bundlefile = bundle | ||||
|
r35045 | self._cgunpacker = None | ||
|
r35044 | |||
|
r35112 | cgpart = None | ||
|
r35113 | for part in bundle.iterparts(seekable=True): | ||
|
r43347 | if part.type == b'changegroup': | ||
|
r35112 | if cgpart: | ||
|
r43346 | raise NotImplementedError( | ||
|
r43387 | b"can't process multiple changegroups" | ||
|
r43346 | ) | ||
|
r35112 | cgpart = part | ||
|
r24073 | |||
|
r35044 | self._handlebundle2part(bundle, part) | ||
|
r24073 | |||
|
r35112 | if not cgpart: | ||
|
r43347 | raise error.Abort(_(b"No changegroups found")) | ||
|
r35112 | |||
# This is required to placate a later consumer, which expects | ||||
# the payload offset to be at the beginning of the changegroup. | ||||
# We need to do this after the iterparts() generator advances | ||||
# because iterparts() will seek to end of payload after the | ||||
# generator returns control to iterparts(). | ||||
cgpart.seek(0, os.SEEK_SET) | ||||
|
r35044 | elif isinstance(bundle, changegroup.cg1unpacker): | ||
if bundle.compressed(): | ||||
|
r43346 | f = self._writetempbundle( | ||
|
r43347 | bundle.read, b'.hg10un', header=b'HG10UN' | ||
|
r43346 | ) | ||
|
r39640 | bundle = exchange.readbundle(self.ui, f, bundlepath, self.vfs) | ||
|
r35044 | |||
self._bundlefile = bundle | ||||
|
r35045 | self._cgunpacker = bundle | ||
|
r35042 | else: | ||
|
r43347 | raise error.Abort( | ||
_(b'bundle type %s cannot be read') % type(bundle) | ||||
) | ||||
|
r26801 | |||
|
r35076 | # dict with the mapping 'filename' -> position in the changegroup. | ||
self._cgfilespos = {} | ||||
|
r5262 | |||
|
r23632 | self.firstnewrev = self.changelog.repotiprev + 1 | ||
|
r43346 | phases.retractboundary( | ||
self, | ||||
None, | ||||
phases.draft, | ||||
[ctx.node() for ctx in self[self.firstnewrev :]], | ||||
) | ||||
|
r23632 | |||
|
r35044 | def _handlebundle2part(self, bundle, part): | ||
|
r43347 | if part.type != b'changegroup': | ||
|
r35074 | return | ||
|
r33890 | |||
|
r35074 | cgstream = part | ||
|
r43347 | version = part.params.get(b'version', b'01') | ||
|
r35074 | legalcgvers = changegroup.supportedincomingversions(self) | ||
if version not in legalcgvers: | ||||
|
r43347 | msg = _(b'Unsupported changegroup version: %s') | ||
|
r35074 | raise error.Abort(msg % version) | ||
if bundle.compressed(): | ||||
|
r43347 | cgstream = self._writetempbundle(part.read, b'.cg%sun' % version) | ||
|
r35074 | |||
|
r43347 | self._cgunpacker = changegroup.getunbundler(version, cgstream, b'UN') | ||
|
r33890 | |||
|
r43347 | def _writetempbundle(self, readfn, suffix, header=b''): | ||
|
r46554 | """Write a temporary file to disk""" | ||
|
r43347 | fdtemp, temp = self.vfs.mkstemp(prefix=b"hg-bundle-", suffix=suffix) | ||
|
r33888 | self.tempfile = temp | ||
|
r43906 | with os.fdopen(fdtemp, 'wb') as fptemp: | ||
|
r33888 | fptemp.write(header) | ||
while True: | ||||
|
r43346 | chunk = readfn(2 ** 18) | ||
|
r33888 | if not chunk: | ||
break | ||||
fptemp.write(chunk) | ||||
|
r43347 | return self.vfs.open(self.tempfile, mode=b"rb") | ||
|
r33888 | |||
|
r18014 | @localrepo.unfilteredpropertycache | ||
|
r23631 | def _phasecache(self): | ||
return bundlephasecache(self, self._phasedefaults) | ||||
@localrepo.unfilteredpropertycache | ||||
|
r8260 | def changelog(self): | ||
|
r14144 | # consume the header if it exists | ||
|
r35045 | self._cgunpacker.changelogheader() | ||
c = bundlechangelog(self.svfs, self._cgunpacker) | ||||
self.manstart = self._cgunpacker.tell() | ||||
|
r8260 | return c | ||
r42710 | def _refreshchangelog(self): | |||
# changelog for bundle repo are not filecache, this method is not | ||||
# applicable. | ||||
pass | ||||
|
r39799 | @localrepo.unfilteredpropertycache | ||
def manifestlog(self): | ||||
|
r35045 | self._cgunpacker.seek(self.manstart) | ||
|
r14144 | # consume the header if it exists | ||
|
r35045 | self._cgunpacker.manifestheader() | ||
|
r28221 | linkmapper = self.unfiltered().changelog.rev | ||
|
r47538 | rootstore = bundlemanifest( | ||
self.nodeconstants, self.svfs, self._cgunpacker, linkmapper | ||||
) | ||||
|
r35045 | self.filestart = self._cgunpacker.tell() | ||
|
r39799 | |||
|
r43346 | return manifest.manifestlog( | ||
self.svfs, self, rootstore, self.narrowmatch() | ||||
) | ||||
|
r8260 | |||
|
r35012 | def _consumemanifest(self): | ||
"""Consumes the manifest portion of the bundle, setting filestart so the | ||||
file portion can be read.""" | ||||
|
r35045 | self._cgunpacker.seek(self.manstart) | ||
self._cgunpacker.manifestheader() | ||||
for delta in self._cgunpacker.deltaiter(): | ||||
|
r35012 | pass | ||
|
r35045 | self.filestart = self._cgunpacker.tell() | ||
|
r35012 | |||
|
r18014 | @localrepo.unfilteredpropertycache | ||
|
r8260 | def manstart(self): | ||
self.changelog | ||||
return self.manstart | ||||
|
r18014 | @localrepo.unfilteredpropertycache | ||
|
r8260 | def filestart(self): | ||
|
r30375 | self.manifestlog | ||
|
r35012 | |||
# If filestart was not set by self.manifestlog, that means the | ||||
# manifestlog implementation did not consume the manifests from the | ||||
# changegroup (ex: it might be consuming trees from a separate bundle2 | ||||
# part instead). So we need to manually consume it. | ||||
|
r43906 | if 'filestart' not in self.__dict__: | ||
|
r35012 | self._consumemanifest() | ||
|
r8260 | return self.filestart | ||
|
r1942 | |||
|
r2673 | def url(self): | ||
return self._url | ||||
|
r1942 | def file(self, f): | ||
|
r35076 | if not self._cgfilespos: | ||
|
r35045 | self._cgunpacker.seek(self.filestart) | ||
|
r35076 | self._cgfilespos = _getfilestarts(self._cgunpacker) | ||
|
r5262 | |||
|
r35076 | if f in self._cgfilespos: | ||
self._cgunpacker.seek(self._cgfilespos[f]) | ||||
|
r28186 | linkmapper = self.unfiltered().changelog.rev | ||
|
r35045 | return bundlefilelog(self.svfs, f, self._cgunpacker, linkmapper) | ||
|
r1942 | else: | ||
|
r37347 | return super(bundlerepository, self).file(f) | ||
|
r1942 | |||
|
r12347 | def close(self): | ||
"""Close assigned bundle file immediately.""" | ||||
|
r35043 | self._bundlefile.close() | ||
|
r12962 | if self.tempfile is not None: | ||
|
r20981 | self.vfs.unlink(self.tempfile) | ||
|
r6314 | if self._tempparent: | ||
shutil.rmtree(self._tempparent, True) | ||||
|
r2740 | |||
|
r6315 | def cancopy(self): | ||
return False | ||||
|
r17193 | def peer(self): | ||
return bundlepeer(self) | ||||
|
r7435 | def getcwd(self): | ||
|
r43346 | return encoding.getcwd() # always outside the repo | ||
|
r7435 | |||
|
r28714 | # Check if parents exist in localrepo before setting | ||
|
r47771 | def setparents(self, p1, p2=None): | ||
if p2 is None: | ||||
p2 = self.nullid | ||||
|
r28714 | p1rev = self.changelog.rev(p1) | ||
p2rev = self.changelog.rev(p2) | ||||
|
r43347 | msg = _(b"setting parent to node %s that only exists in the bundle\n") | ||
|
r28714 | if self.changelog.repotiprev < p1rev: | ||
|
r46729 | self.ui.warn(msg % hex(p1)) | ||
|
r28714 | if self.changelog.repotiprev < p2rev: | ||
|
r46729 | self.ui.warn(msg % hex(p2)) | ||
|
r28714 | return super(bundlerepository, self).setparents(p1, p2) | ||
|
r15597 | |||
|
r43346 | |||
|
r39585 | def instance(ui, path, create, intents=None, createopts=None): | ||
|
r2740 | if create: | ||
|
r43347 | raise error.Abort(_(b'cannot create new bundle repository')) | ||
|
r25830 | # internal config: bundle.mainreporoot | ||
|
r43347 | parentpath = ui.config(b"bundle", b"mainreporoot") | ||
|
r16042 | if not parentpath: | ||
# try to find the correct path to the working directory repo | ||||
|
r39843 | parentpath = cmdutil.findrepo(encoding.getcwd()) | ||
|
r16042 | if parentpath is None: | ||
|
r43347 | parentpath = b'' | ||
|
r5664 | if parentpath: | ||
# Try to make the full path relative so we get a nice, short URL. | ||||
# In particular, we don't want temp dir names in test outputs. | ||||
|
r39843 | cwd = encoding.getcwd() | ||
|
r5664 | if parentpath == cwd: | ||
|
r43347 | parentpath = b'' | ||
|
r5664 | else: | ||
|
r24834 | cwd = pathutil.normasprefix(cwd) | ||
|
r5664 | if parentpath.startswith(cwd): | ||
|
r43346 | parentpath = parentpath[len(cwd) :] | ||
r47669 | u = urlutil.url(path) | |||
|
r13826 | path = u.localpath() | ||
|
r43347 | if u.scheme == b'bundle': | ||
s = path.split(b"+", 1) | ||||
|
r2740 | if len(s) == 1: | ||
|
r5664 | repopath, bundlename = parentpath, s[0] | ||
|
r2740 | else: | ||
repopath, bundlename = s | ||||
else: | ||||
|
r5664 | repopath, bundlename = parentpath, path | ||
|
r39639 | |||
return makebundlerepository(ui, repopath, bundlename) | ||||
|
r43346 | |||
|
r39639 | def makebundlerepository(ui, repopath, bundlepath): | ||
"""Make a bundle repository object based on repo and bundle paths.""" | ||||
|
r39640 | if repopath: | ||
|
r43347 | url = b'bundle:%s+%s' % (util.expandpath(repopath), bundlepath) | ||
|
r39640 | else: | ||
|
r43347 | url = b'bundle:%s' % bundlepath | ||
|
r39640 | |||
# Because we can't make any guarantees about the type of the base | ||||
# repository, we can't have a static class representing the bundle | ||||
# repository. We also can't make any guarantees about how to even | ||||
# call the base repository's constructor! | ||||
# | ||||
# So, our strategy is to go through ``localrepo.instance()`` to construct | ||||
# a repo instance. Then, we dynamically create a new type derived from | ||||
# both it and our ``bundlerepository`` class which overrides some | ||||
# functionality. We then change the type of the constructed repository | ||||
# to this new type and initialize the bundle-specific bits of it. | ||||
try: | ||||
|
r39980 | repo = localrepo.instance(ui, repopath, create=False) | ||
|
r39640 | tempparent = None | ||
except error.RepoError: | ||||
tempparent = pycompat.mkdtemp() | ||||
try: | ||||
|
r39980 | repo = localrepo.instance(ui, tempparent, create=True) | ||
|
r39640 | except Exception: | ||
shutil.rmtree(tempparent) | ||||
raise | ||||
|
r39980 | class derivedbundlerepository(bundlerepository, repo.__class__): | ||
|
r39640 | pass | ||
repo.__class__ = derivedbundlerepository | ||||
bundlerepository.__init__(repo, bundlepath, url, tempparent) | ||||
return repo | ||||
|
r12734 | |||
|
r43346 | |||
|
r23633 | class bundletransactionmanager(object): | ||
def transaction(self): | ||||
return None | ||||
def close(self): | ||||
raise NotImplementedError | ||||
def release(self): | ||||
raise NotImplementedError | ||||
|
r43346 | |||
def getremotechanges( | ||||
ui, repo, peer, onlyheads=None, bundlename=None, force=False | ||||
): | ||||
|
r46554 | """obtains a bundle of changes incoming from peer | ||
|
r14161 | |||
"onlyheads" restricts the returned changes to those reachable from the | ||||
specified heads. | ||||
"bundlename", if given, stores the bundle to this file path permanently; | ||||
|
r14190 | otherwise it's stored to a temp file and gets deleted again when you call | ||
the returned "cleanupfn". | ||||
|
r14161 | "force" indicates whether to proceed on unrelated repos. | ||
Returns a tuple (local, csets, cleanupfn): | ||||
|
r16683 | "local" is a local repo from which to obtain the actual incoming | ||
changesets; it is a bundlerepo for the obtained bundle when the | ||||
|
r37660 | original "peer" is remote. | ||
|
r14161 | "csets" lists the incoming changeset node ids. | ||
|
r16683 | "cleanupfn" must be called without arguments when you're done processing | ||
|
r37660 | the changes; it closes both the original "peer" and the one returned | ||
|
r16683 | here. | ||
|
r46554 | """ | ||
|
r43346 | tmp = discovery.findcommonincoming(repo, peer, heads=onlyheads, force=force) | ||
|
r12734 | common, incoming, rheads = tmp | ||
if not incoming: | ||||
try: | ||||
|
r15091 | if bundlename: | ||
|
r21694 | os.unlink(bundlename) | ||
|
r14004 | except OSError: | ||
|
r12734 | pass | ||
|
r37660 | return repo, [], peer.close | ||
|
r12734 | |||
|
r22182 | commonset = set(common) | ||
rheads = [x for x in rheads if x not in commonset] | ||||
|
r12734 | bundle = None | ||
|
r14161 | bundlerepo = None | ||
|
r37660 | localrepo = peer.local() | ||
|
r17191 | if bundlename or not localrepo: | ||
|
r37660 | # create a bundle (uncompressed if peer repo is not local) | ||
|
r12734 | |||
|
r29684 | # developer config: devel.legacy.exchange | ||
|
r43347 | legexc = ui.configlist(b'devel', b'legacy.exchange') | ||
forcebundle1 = b'bundle2' not in legexc and b'bundle1' in legexc | ||||
|
r43346 | canbundle2 = ( | ||
not forcebundle1 | ||||
|
r43347 | and peer.capable(b'getbundle') | ||
and peer.capable(b'bundle2') | ||||
|
r43346 | ) | ||
|
r26544 | if canbundle2: | ||
|
r37661 | with peer.commandexecutor() as e: | ||
|
r43346 | b2 = e.callcommand( | ||
|
r43347 | b'getbundle', | ||
|
r43346 | { | ||
|
r43347 | b'source': b'incoming', | ||
b'common': common, | ||||
b'heads': rheads, | ||||
b'bundlecaps': exchange.caps20to10( | ||||
repo, role=b'client' | ||||
), | ||||
b'cg': True, | ||||
|
r43346 | }, | ||
).result() | ||||
|
r37661 | |||
|
r43346 | fname = bundle = changegroup.writechunks( | ||
ui, b2._forwardchunks(), bundlename | ||||
) | ||||
|
r26544 | else: | ||
|
r43347 | if peer.capable(b'getbundle'): | ||
|
r37661 | with peer.commandexecutor() as e: | ||
|
r43346 | cg = e.callcommand( | ||
|
r43347 | b'getbundle', | ||
|
r43346 | { | ||
|
r43347 | b'source': b'incoming', | ||
b'common': common, | ||||
b'heads': rheads, | ||||
|
r43346 | }, | ||
).result() | ||||
|
r43347 | elif onlyheads is None and not peer.capable(b'changegroupsubset'): | ||
|
r26543 | # compat with older servers when pulling all remote heads | ||
|
r37653 | |||
|
r37660 | with peer.commandexecutor() as e: | ||
|
r43346 | cg = e.callcommand( | ||
|
r43347 | b'changegroup', | ||
|
r46554 | { | ||
b'nodes': incoming, | ||||
b'source': b'incoming', | ||||
}, | ||||
|
r43346 | ).result() | ||
|
r37653 | |||
|
r26543 | rheads = None | ||
else: | ||||
|
r37660 | with peer.commandexecutor() as e: | ||
|
r43346 | cg = e.callcommand( | ||
|
r43347 | b'changegroupsubset', | ||
|
r43346 | { | ||
|
r43347 | b'bases': incoming, | ||
b'heads': rheads, | ||||
b'source': b'incoming', | ||||
|
r43346 | }, | ||
).result() | ||||
|
r37653 | |||
|
r26543 | if localrepo: | ||
|
r43347 | bundletype = b"HG10BZ" | ||
|
r26543 | else: | ||
|
r43347 | bundletype = b"HG10UN" | ||
|
r43346 | fname = bundle = bundle2.writebundle(ui, cg, bundlename, bundletype) | ||
|
r12734 | # keep written bundle? | ||
if bundlename: | ||||
bundle = None | ||||
|
r17191 | if not localrepo: | ||
|
r12734 | # use the created uncompressed bundlerepo | ||
|
r43346 | localrepo = bundlerepo = makebundlerepository( | ||
repo.baseui, repo.root, fname | ||||
) | ||||
|
r39639 | |||
|
r37660 | # this repo contains local and peer now, so filter out local again | ||
|
r14161 | common = repo.heads() | ||
|
r18568 | if localrepo: | ||
# Part of common may be remotely filtered | ||||
# So use an unfiltered version | ||||
# The discovery process probably need cleanup to avoid that | ||||
localrepo = localrepo.unfiltered() | ||||
|
r14161 | |||
|
r14412 | csets = localrepo.changelog.findmissing(common, rheads) | ||
|
r12734 | |||
|
r23633 | if bundlerepo: | ||
|
r43346 | reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev :]] | ||
|
r37661 | |||
with peer.commandexecutor() as e: | ||||
|
r43346 | remotephases = e.callcommand( | ||
|
r46554 | b'listkeys', | ||
{ | ||||
b'namespace': b'phases', | ||||
}, | ||||
|
r43346 | ).result() | ||
|
r23633 | |||
|
r37660 | pullop = exchange.pulloperation(bundlerepo, peer, heads=reponodes) | ||
|
r23633 | pullop.trmanager = bundletransactionmanager() | ||
exchange._pullapplyphases(pullop, remotephases) | ||||
|
r14161 | def cleanup(): | ||
if bundlerepo: | ||||
bundlerepo.close() | ||||
if bundle: | ||||
|
r21694 | os.unlink(bundle) | ||
|
r37660 | peer.close() | ||
|
r14161 | |||
return (localrepo, csets, cleanup) | ||||