exchange.py
2823 lines
| 94.3 KiB
| text/x-python
|
PythonLexer
/ mercurial / exchange.py
|
r21024 | # exchange.py - utility to exchange data between repos. | ||
|
r20345 | # | ||
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
|
r20345 | # | ||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r27523 | from __future__ import absolute_import | ||
|
r34323 | import collections | ||
|
r45033 | import weakref | ||
|
r27523 | |||
from .i18n import _ | ||||
from .node import ( | ||||
hex, | ||||
|
r38827 | nullrev, | ||
|
r27523 | ) | ||
from . import ( | ||||
bookmarks as bookmod, | ||||
bundle2, | ||||
r46369 | bundlecaches, | |||
|
r27523 | changegroup, | ||
discovery, | ||||
error, | ||||
|
r39665 | exchangev2, | ||
|
r27523 | lock as lockmod, | ||
|
r35348 | logexchange, | ||
|
r38826 | narrowspec, | ||
|
r27523 | obsolete, | ||
|
r43858 | obsutil, | ||
|
r27523 | phases, | ||
pushkey, | ||||
|
r32896 | pycompat, | ||
|
r45932 | requirements, | ||
|
r27523 | scmutil, | ||
streamclone, | ||||
url as urlmod, | ||||
util, | ||||
|
r40527 | wireprototypes, | ||
|
r27523 | ) | ||
|
r44517 | from .utils import ( | ||
hashutil, | ||||
stringutil, | ||||
r47669 | urlutil, | |||
|
r44517 | ) | ||
r48000 | from .interfaces import repository | |||
|
r20345 | |||
|
r28883 | urlerr = util.urlerr | ||
urlreq = util.urlreq | ||||
|
r43347 | _NARROWACL_SECTION = b'narrowacl' | ||
|
r38825 | |||
|
r26639 | |||
|
r21064 | def readbundle(ui, fh, fname, vfs=None): | ||
|
r21065 | header = changegroup.readexactly(fh, 4) | ||
|
r21063 | |||
|
r21065 | alg = None | ||
|
r21063 | if not fname: | ||
|
r43347 | fname = b"stream" | ||
if not header.startswith(b'HG') and header.startswith(b'\0'): | ||||
|
r21063 | fh = changegroup.headerlessfixup(fh, header) | ||
|
r43347 | header = b"HG10" | ||
alg = b'UN' | ||||
|
r21063 | elif vfs: | ||
fname = vfs.join(fname) | ||||
|
r21065 | magic, version = header[0:2], header[2:4] | ||
|
r21063 | |||
|
r43347 | if magic != b'HG': | ||
raise error.Abort(_(b'%s: not a Mercurial bundle') % fname) | ||||
if version == b'10': | ||||
|
r21065 | if alg is None: | ||
alg = changegroup.readexactly(fh, 2) | ||||
|
r22390 | return changegroup.cg1unpacker(fh, alg) | ||
|
r43347 | elif version.startswith(b'2'): | ||
|
r25640 | return bundle2.getunbundler(ui, fh, magicstring=magic + version) | ||
|
r43347 | elif version == b'S1': | ||
|
r26756 | return streamclone.streamcloneapplier(fh) | ||
|
r21065 | else: | ||
|
r43347 | raise error.Abort( | ||
_(b'%s: unknown bundle version %s') % (fname, version) | ||||
) | ||||
|
r21063 | |||
|
r43346 | |||
|
r27883 | def getbundlespec(ui, fh): | ||
"""Infer the bundlespec from a bundle file handle. | ||||
The input file handle is seeked and the original seek position is not | ||||
restored. | ||||
""" | ||||
|
r43346 | |||
|
r27883 | def speccompression(alg): | ||
|
r30440 | try: | ||
return util.compengines.forbundletype(alg).bundletype()[0] | ||||
except KeyError: | ||||
return None | ||||
|
r27883 | |||
b = readbundle(ui, fh, None) | ||||
if isinstance(b, changegroup.cg1unpacker): | ||||
alg = b._type | ||||
|
r43347 | if alg == b'_truncatedBZ': | ||
alg = b'BZ' | ||||
|
r27883 | comp = speccompression(alg) | ||
if not comp: | ||||
|
r43347 | raise error.Abort(_(b'unknown compression algorithm: %s') % alg) | ||
return b'%s-v1' % comp | ||||
|
r27883 | elif isinstance(b, bundle2.unbundle20): | ||
|
r43347 | if b'Compression' in b.params: | ||
comp = speccompression(b.params[b'Compression']) | ||||
|
r27883 | if not comp: | ||
|
r43347 | raise error.Abort( | ||
_(b'unknown compression algorithm: %s') % comp | ||||
) | ||||
|
r27883 | else: | ||
|
r43347 | comp = b'none' | ||
|
r27883 | |||
version = None | ||||
for part in b.iterparts(): | ||||
|
r43347 | if part.type == b'changegroup': | ||
version = part.params[b'version'] | ||||
if version in (b'01', b'02'): | ||||
version = b'v2' | ||||
|
r27883 | else: | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'changegroup version %s does not have ' | ||
b'a known bundlespec' | ||||
|
r43346 | ) | ||
% version, | ||||
|
r43387 | hint=_(b'try upgrading your Mercurial client'), | ||
|
r43346 | ) | ||
|
r43347 | elif part.type == b'stream2' and version is None: | ||
|
r37185 | # A stream2 part requires to be part of a v2 bundle | ||
|
r43347 | requirements = urlreq.unquote(part.params[b'requirements']) | ||
|
r37185 | splitted = requirements.split() | ||
params = bundle2._formatrequirementsparams(splitted) | ||||
|
r43347 | return b'none-v2;stream=v2;%s' % params | ||
|
r27883 | |||
if not version: | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'could not identify changegroup version in bundle') | ||
|
r43346 | ) | ||
|
r27883 | |||
|
r43347 | return b'%s-%s' % (comp, version) | ||
|
r27883 | elif isinstance(b, streamclone.streamcloneapplier): | ||
requirements = streamclone.readbundle1header(fh)[2] | ||||
|
r37184 | formatted = bundle2._formatrequirementsparams(requirements) | ||
|
r43347 | return b'none-packed1;%s' % formatted | ||
|
r27883 | else: | ||
|
r43347 | raise error.Abort(_(b'unknown bundle type: %s') % b) | ||
|
r27883 | |||
|
r43346 | |||
|
r29808 | def _computeoutgoing(repo, heads, common): | ||
"""Computes which revs are outgoing given a set of common | ||||
and a set of heads. | ||||
This is a separate function so extensions can have access to | ||||
the logic. | ||||
Returns a discovery.outgoing object. | ||||
""" | ||||
cl = repo.changelog | ||||
if common: | ||||
hasnode = cl.hasnode | ||||
common = [n for n in common if hasnode(n)] | ||||
else: | ||||
|
r47771 | common = [repo.nullid] | ||
|
r29808 | if not heads: | ||
heads = cl.heads() | ||||
return discovery.outgoing(repo, common, heads) | ||||
|
r43346 | |||
r40803 | def _checkpublish(pushop): | |||
repo = pushop.repo | ||||
ui = repo.ui | ||||
|
r43347 | behavior = ui.config(b'experimental', b'auto-publish') | ||
if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'): | ||||
r40803 | return | |||
|
r43347 | remotephases = listkeys(pushop.remote, b'phases') | ||
if not remotephases.get(b'publishing', False): | ||||
r40803 | return | |||
if pushop.revs is None: | ||||
|
r43347 | published = repo.filtered(b'served').revs(b'not public()') | ||
r40803 | else: | |||
|
r43347 | published = repo.revs(b'::%ln - public()', pushop.revs) | ||
r48286 | # we want to use pushop.revs in the revset even if they themselves are | |||
# secret, but we don't want to have anything that the server won't see | ||||
# in the result of this expression | ||||
published &= repo.filtered(b'served') | ||||
r40803 | if published: | |||
|
r43347 | if behavior == b'warn': | ||
ui.warn( | ||||
_(b'%i changesets about to be published\n') % len(published) | ||||
) | ||||
elif behavior == b'confirm': | ||||
|
r43346 | if ui.promptchoice( | ||
|
r43387 | _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No') | ||
|
r43346 | % len(published) | ||
): | ||||
|
r46489 | raise error.CanceledError(_(b'user quit')) | ||
|
r43347 | elif behavior == b'abort': | ||
msg = _(b'push would publish %i changesets') % len(published) | ||||
|
r43346 | hint = _( | ||
|
r43347 | b"use --publish or adjust 'experimental.auto-publish'" | ||
b" config" | ||||
|
r43346 | ) | ||
r40803 | raise error.Abort(msg, hint=hint) | |||
|
r43346 | |||
|
r29682 | def _forcebundle1(op): | ||
"""return true if a pull/push must use bundle1 | ||||
|
r24650 | |||
|
r29683 | This function is used to allow testing of the older bundle version""" | ||
ui = op.repo.ui | ||||
|
r30332 | # The goal is this config is to allow developer to choose the bundle | ||
|
r29683 | # version used during exchanged. This is especially handy during test. | ||
# Value is a list of bundle version to be picked from, highest version | ||||
# should be used. | ||||
# | ||||
# developer config: devel.legacy.exchange | ||||
|
r43347 | exchange = ui.configlist(b'devel', b'legacy.exchange') | ||
forcebundle1 = b'bundle2' not in exchange and b'bundle1' in exchange | ||||
return forcebundle1 or not op.remote.capable(b'bundle2') | ||||
|
r24650 | |||
|
r43346 | |||
|
r20346 | class pushoperation(object): | ||
"""A object that represent a single push operation | ||||
|
r28456 | Its purpose is to carry push related state and very common operations. | ||
|
r20346 | |||
|
r28456 | A new pushoperation should be created at the beginning of each push and | ||
discarded afterward. | ||||
|
r20346 | """ | ||
|
r43346 | def __init__( | ||
self, | ||||
repo, | ||||
remote, | ||||
force=False, | ||||
revs=None, | ||||
newbranch=False, | ||||
bookmarks=(), | ||||
publish=False, | ||||
pushvars=None, | ||||
): | ||||
|
r20346 | # repo we push from | ||
self.repo = repo | ||||
|
r20347 | self.ui = repo.ui | ||
|
r20348 | # repo we push to | ||
self.remote = remote | ||||
|
r20349 | # force option provided | ||
self.force = force | ||||
|
r20350 | # revs to be pushed (None is "all") | ||
self.revs = revs | ||||
|
r22623 | # bookmark explicitly pushed | ||
self.bookmarks = bookmarks | ||||
|
r20351 | # allow push of new branch | ||
self.newbranch = newbranch | ||||
|
r21901 | # step already performed | ||
# (used to check what steps have been already performed through bundle2) | ||||
self.stepsdone = set() | ||||
|
r22615 | # Integer version of the changegroup push result | ||
|
r20439 | # - None means nothing to push | ||
# - 0 means HTTP error | ||||
# - 1 means we pushed and remote head count is unchanged *or* | ||||
# we have outgoing changesets but refused to push | ||||
# - other values as described by addchangegroup() | ||||
|
r22615 | self.cgresult = None | ||
|
r22624 | # Boolean value for the bookmark push | ||
self.bkresult = None | ||||
|
r21024 | # discover.outgoing object (contains common and outgoing data) | ||
|
r20440 | self.outgoing = None | ||
r32709 | # all remote topological heads before the push | |||
|
r20462 | self.remoteheads = None | ||
r32709 | # Details of the remote branch pre and post push | |||
# | ||||
# mapping: {'branch': ([remoteheads], | ||||
# [newheads], | ||||
# [unsyncedheads], | ||||
# [discardedheads])} | ||||
# - branch: the branch name | ||||
# - remoteheads: the list of remote heads known locally | ||||
# None if the branch is new | ||||
# - newheads: the new remote heads (known locally) with outgoing pushed | ||||
# - unsyncedheads: the list of remote heads unknown locally. | ||||
# - discardedheads: the list of remote heads made obsolete by the push | ||||
self.pushbranchmap = None | ||||
|
r20464 | # testable as a boolean indicating if any nodes are missing locally. | ||
self.incoming = None | ||||
|
r34820 | # summary of the remote phase situation | ||
self.remotephases = None | ||||
|
r22019 | # phases changes that must be pushed along side the changesets | ||
self.outdatedphases = None | ||||
# phases changes that must be pushed if changeset push fails | ||||
self.fallbackoutdatedphases = None | ||||
|
r22034 | # outgoing obsmarkers | ||
|
r22035 | self.outobsmarkers = set() | ||
|
r43194 | # outgoing bookmarks, list of (bm, oldnode | '', newnode | '') | ||
|
r22239 | self.outbookmarks = [] | ||
|
r23437 | # transaction manager | ||
self.trmanager = None | ||||
|
r25485 | # map { pushkey partid -> callback handling failure} | ||
# used to handle exception from mandatory pushkey part failure | ||||
self.pkfailcb = {} | ||||
|
r33886 | # an iterable of pushvars or None | ||
self.pushvars = pushvars | ||||
r40722 | # publish pushed changesets | |||
self.publish = publish | ||||
|
r20346 | |||
|
r22014 | @util.propertycache | ||
def futureheads(self): | ||||
"""future remote heads if the changeset push succeeds""" | ||||
|
r45704 | return self.outgoing.ancestorsof | ||
|
r22014 | |||
|
r22015 | @util.propertycache | ||
def fallbackheads(self): | ||||
"""future remote heads if the changeset push fails""" | ||||
if self.revs is None: | ||||
# not target to push, all common are relevant | ||||
return self.outgoing.commonheads | ||||
unfi = self.repo.unfiltered() | ||||
|
r45704 | # I want cheads = heads(::ancestorsof and ::commonheads) | ||
# (ancestorsof is revs with secret changeset filtered out) | ||||
|
r22015 | # | ||
# This can be expressed as: | ||||
|
r45704 | # cheads = ( (ancestorsof and ::commonheads) | ||
# + (commonheads and ::ancestorsof))" | ||||
|
r22015 | # ) | ||
# | ||||
# while trying to push we already computed the following: | ||||
# common = (::commonheads) | ||||
|
r45704 | # missing = ((commonheads::ancestorsof) - commonheads) | ||
|
r22015 | # | ||
# We can pick: | ||||
|
r45704 | # * ancestorsof part of common (::commonheads) | ||
|
r26184 | common = self.outgoing.common | ||
r43962 | rev = self.repo.changelog.index.rev | |||
cheads = [node for node in self.revs if rev(node) in common] | ||||
|
r22015 | # and | ||
# * commonheads parents on missing | ||||
|
r43346 | revset = unfi.set( | ||
|
r43347 | b'%ln and parents(roots(%ln))', | ||
|
r43346 | self.outgoing.commonheads, | ||
self.outgoing.missing, | ||||
) | ||||
|
r22015 | cheads.extend(c.node() for c in revset) | ||
return cheads | ||||
|
r22016 | @property | ||
def commonheads(self): | ||||
"""set of all common heads after changeset bundle push""" | ||||
|
r22615 | if self.cgresult: | ||
|
r22016 | return self.futureheads | ||
else: | ||||
return self.fallbackheads | ||||
|
r22015 | |||
|
r43346 | |||
|
r22650 | # mapping of message used when pushing bookmark | ||
|
r43346 | bookmsgmap = { | ||
|
r43347 | b'update': ( | ||
_(b"updating bookmark %s\n"), | ||||
|
r46522 | _(b'updating bookmark %s failed\n'), | ||
|
r43346 | ), | ||
|
r43347 | b'export': ( | ||
_(b"exporting bookmark %s\n"), | ||||
|
r46522 | _(b'exporting bookmark %s failed\n'), | ||
|
r43346 | ), | ||
|
r43347 | b'delete': ( | ||
_(b"deleting remote bookmark %s\n"), | ||||
|
r46522 | _(b'deleting remote bookmark %s failed\n'), | ||
|
r43346 | ), | ||
} | ||||
def push( | ||||
repo, | ||||
remote, | ||||
force=False, | ||||
revs=None, | ||||
newbranch=False, | ||||
bookmarks=(), | ||||
publish=False, | ||||
opargs=None, | ||||
): | ||||
|
r46554 | """Push outgoing changesets (limited by revs) from a local | ||
|
r20345 | repository to remote. Return an integer: | ||
- None means nothing to push | ||||
- 0 means HTTP error | ||||
- 1 means we pushed and remote head count is unchanged *or* | ||||
we have outgoing changesets but refused to push | ||||
- other values as described by addchangegroup() | ||||
|
r46554 | """ | ||
|
r26729 | if opargs is None: | ||
opargs = {} | ||||
|
r43346 | pushop = pushoperation( | ||
repo, | ||||
remote, | ||||
force, | ||||
revs, | ||||
newbranch, | ||||
bookmarks, | ||||
publish, | ||||
**pycompat.strkwargs(opargs) | ||||
) | ||||
|
r20348 | if pushop.remote.local(): | ||
|
r43346 | missing = ( | ||
set(pushop.repo.requirements) - pushop.remote.local().supported | ||||
) | ||||
|
r20345 | if missing: | ||
|
r43346 | msg = _( | ||
|
r43347 | b"required features are not" | ||
b" supported in the destination:" | ||||
b" %s" | ||||
) % (b', '.join(sorted(missing))) | ||||
|
r26587 | raise error.Abort(msg) | ||
|
r20345 | |||
|
r20348 | if not pushop.remote.canpush(): | ||
|
r43347 | raise error.Abort(_(b"destination does not support push")) | ||
if not pushop.remote.capable(b'unbundle'): | ||||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'cannot push: destination does not support the ' | ||
b'unbundle wire protocol command' | ||||
|
r43346 | ) | ||
) | ||||
|
r47447 | for category in sorted(bundle2.read_remote_wanted_sidedata(pushop.remote)): | ||
# Check that a computer is registered for that category for at least | ||||
# one revlog kind. | ||||
for kind, computers in repo._sidedata_computers.items(): | ||||
if computers.get(category): | ||||
break | ||||
else: | ||||
raise error.Abort( | ||||
_( | ||||
b'cannot push: required sidedata category not supported' | ||||
b" by this client: '%s'" | ||||
) | ||||
% pycompat.bytestr(category) | ||||
) | ||||
|
r33788 | # get lock as we might write phase data | ||
wlock = lock = None | ||||
|
r20345 | try: | ||
|
r42513 | # bundle2 push may receive a reply bundle touching bookmarks | ||
# requiring the wlock. Take it now to ensure proper ordering. | ||||
|
r43347 | maypushback = pushop.ui.configbool(b'experimental', b'bundle2.pushback') | ||
|
r43346 | if ( | ||
(not _forcebundle1(pushop)) | ||||
and maypushback | ||||
and not bookmod.bookmarksinstore(repo) | ||||
): | ||||
|
r33788 | wlock = pushop.repo.wlock() | ||
lock = pushop.repo.lock() | ||||
|
r43346 | pushop.trmanager = transactionmanager( | ||
|
r43347 | pushop.repo, b'push-response', pushop.remote.url() | ||
|
r43346 | ) | ||
|
r38111 | except error.LockUnavailable as err: | ||
|
r20345 | # source repo cannot be locked. | ||
# We do not abort the push, but just disable the local phase | ||||
# synchronisation. | ||||
|
r43347 | msg = b'cannot lock source repository: %s\n' % stringutil.forcebytestr( | ||
|
r43346 | err | ||
) | ||||
|
r20347 | pushop.ui.debug(msg) | ||
|
r33789 | |||
|
r41926 | with wlock or util.nullcontextmanager(): | ||
with lock or util.nullcontextmanager(): | ||||
with pushop.trmanager or util.nullcontextmanager(): | ||||
pushop.repo.checkpush(pushop) | ||||
_checkpublish(pushop) | ||||
_pushdiscovery(pushop) | ||||
|
r44365 | if not pushop.force: | ||
_checksubrepostate(pushop) | ||||
|
r41926 | if not _forcebundle1(pushop): | ||
_pushbundle2(pushop) | ||||
_pushchangeset(pushop) | ||||
_pushsyncphase(pushop) | ||||
_pushobsolete(pushop) | ||||
_pushbookmark(pushop) | ||||
|
r33667 | |||
|
r43347 | if repo.ui.configbool(b'experimental', b'remotenames'): | ||
|
r38634 | logexchange.pullremotenames(repo, remote) | ||
|
r22616 | return pushop | ||
|
r20352 | |||
|
r43346 | |||
|
r22018 | # list of steps to perform discovery before push | ||
pushdiscoveryorder = [] | ||||
# Mapping between step name and function | ||||
# | ||||
# This exists to help extensions wrap steps if necessary | ||||
pushdiscoverymapping = {} | ||||
|
r43346 | |||
|
r22018 | def pushdiscovery(stepname): | ||
"""decorator for function performing discovery before push | ||||
The function is added to the step -> function mapping and appended to the | ||||
list of steps. Beware that decorated function will be added in order (this | ||||
may matter). | ||||
You can only use this decorator for a new step, if you want to wrap a step | ||||
from an extension, change the pushdiscovery dictionary directly.""" | ||||
|
r43346 | |||
|
r22018 | def dec(func): | ||
assert stepname not in pushdiscoverymapping | ||||
pushdiscoverymapping[stepname] = func | ||||
pushdiscoveryorder.append(stepname) | ||||
return func | ||||
|
r43346 | |||
|
r22018 | return dec | ||
|
r43346 | |||
|
r20466 | def _pushdiscovery(pushop): | ||
|
r22018 | """Run all discovery steps""" | ||
for stepname in pushdiscoveryorder: | ||||
step = pushdiscoverymapping[stepname] | ||||
step(pushop) | ||||
|
r43346 | |||
|
r44365 | def _checksubrepostate(pushop): | ||
"""Ensure all outgoing referenced subrepo revisions are present locally""" | ||||
for n in pushop.outgoing.missing: | ||||
ctx = pushop.repo[n] | ||||
if b'.hgsub' in ctx.manifest() and b'.hgsubstate' in ctx.files(): | ||||
for subpath in sorted(ctx.substate): | ||||
sub = ctx.sub(subpath) | ||||
sub.verify(onpush=True) | ||||
|
r43347 | @pushdiscovery(b'changeset') | ||
|
r22018 | def _pushdiscoverychangeset(pushop): | ||
"""discover the changeset that need to be pushed""" | ||||
|
r20466 | fci = discovery.findcommonincoming | ||
|
r35306 | if pushop.revs: | ||
|
r43346 | commoninc = fci( | ||
pushop.repo, | ||||
pushop.remote, | ||||
force=pushop.force, | ||||
ancestorsof=pushop.revs, | ||||
) | ||||
|
r35306 | else: | ||
commoninc = fci(pushop.repo, pushop.remote, force=pushop.force) | ||||
|
r20466 | common, inc, remoteheads = commoninc | ||
fco = discovery.findcommonoutgoing | ||||
|
r43346 | outgoing = fco( | ||
pushop.repo, | ||||
pushop.remote, | ||||
onlyheads=pushop.revs, | ||||
commoninc=commoninc, | ||||
force=pushop.force, | ||||
) | ||||
|
r20466 | pushop.outgoing = outgoing | ||
pushop.remoteheads = remoteheads | ||||
pushop.incoming = inc | ||||
|
r43346 | |||
|
r43347 | @pushdiscovery(b'phase') | ||
|
r22019 | def _pushdiscoveryphase(pushop): | ||
"""discover the phase that needs to be pushed | ||||
(computed for both success and failure case for changesets push)""" | ||||
outgoing = pushop.outgoing | ||||
unfi = pushop.repo.unfiltered() | ||||
|
r43347 | remotephases = listkeys(pushop.remote, b'phases') | ||
|
r37775 | |||
|
r43346 | if ( | ||
|
r43347 | pushop.ui.configbool(b'ui', b'_usedassubrepo') | ||
|
r43346 | and remotephases # server supports phases | ||
and not pushop.outgoing.missing # no changesets to be pushed | ||||
|
r43347 | and remotephases.get(b'publishing', False) | ||
|
r43346 | ): | ||
|
r25337 | # When: | ||
# - this is a subrepo push | ||||
# - and remote support phase | ||||
# - and no changeset are to be pushed | ||||
# - and remote is publishing | ||||
|
r34818 | # We may be in issue 3781 case! | ||
|
r25337 | # We drop the possible phase synchronisation done by | ||
# courtesy to publish changesets possibly locally draft | ||||
# on the remote. | ||||
|
r34819 | pushop.outdatedphases = [] | ||
pushop.fallbackoutdatedphases = [] | ||||
return | ||||
|
r34820 | |||
|
r43346 | pushop.remotephases = phases.remotephasessummary( | ||
pushop.repo, pushop.fallbackheads, remotephases | ||||
) | ||||
|
r34820 | droots = pushop.remotephases.draftroots | ||
|
r43347 | extracond = b'' | ||
|
r34820 | if not pushop.remotephases.publishing: | ||
|
r43347 | extracond = b' and public()' | ||
revset = b'heads((%%ln::%%ln) %s)' % extracond | ||||
|
r22019 | # Get the list of all revs draft on remote by public here. | ||
# XXX Beware that revset break if droots is not strictly | ||||
# XXX root we may want to ensure it is but it is costly | ||||
fallback = list(unfi.set(revset, droots, pushop.fallbackheads)) | ||||
r40722 | if not pushop.remotephases.publishing and pushop.publish: | |||
|
r43346 | future = list( | ||
unfi.set( | ||||
|
r43347 | b'%ln and (not public() or %ln::)', pushop.futureheads, droots | ||
|
r43346 | ) | ||
) | ||||
r40722 | elif not outgoing.missing: | |||
|
r22019 | future = fallback | ||
else: | ||||
# adds changeset we are going to push as draft | ||||
# | ||||
|
r23139 | # should not be necessary for publishing server, but because of an | ||
|
r22019 | # issue fixed in xxxxx we have to do it anyway. | ||
|
r43346 | fdroots = list( | ||
|
r43347 | unfi.set(b'roots(%ln + %ln::)', outgoing.missing, droots) | ||
|
r43346 | ) | ||
|
r22019 | fdroots = [f.node() for f in fdroots] | ||
future = list(unfi.set(revset, fdroots, pushop.futureheads)) | ||||
pushop.outdatedphases = future | ||||
pushop.fallbackoutdatedphases = fallback | ||||
|
r43346 | |||
|
r43347 | @pushdiscovery(b'obsmarker') | ||
|
r22035 | def _pushdiscoveryobsmarkers(pushop): | ||
|
r37775 | if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt): | ||
return | ||||
if not pushop.repo.obsstore: | ||||
return | ||||
|
r43347 | if b'obsolete' not in listkeys(pushop.remote, b'namespaces'): | ||
|
r37775 | return | ||
repo = pushop.repo | ||||
# very naive computation, that can be quite expensive on big repo. | ||||
# However: evolution is currently slow on them anyway. | ||||
|
r43347 | nodes = (c.node() for c in repo.set(b'::%ln', pushop.futureheads)) | ||
|
r37775 | pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes) | ||
|
r22035 | |||
|
r43346 | |||
|
r43347 | @pushdiscovery(b'bookmarks') | ||
|
r22239 | def _pushdiscoverybookmarks(pushop): | ||
ui = pushop.ui | ||||
repo = pushop.repo.unfiltered() | ||||
remote = pushop.remote | ||||
|
r43347 | ui.debug(b"checking for updated bookmarks\n") | ||
|
r22239 | ancestors = () | ||
if pushop.revs: | ||||
|
r38623 | revnums = pycompat.maplist(repo.changelog.rev, pushop.revs) | ||
|
r22239 | ancestors = repo.changelog.ancestors(revnums, inclusive=True) | ||
|
r37775 | |||
|
r43347 | remotebookmark = bookmod.unhexlifybookmarks(listkeys(remote, b'bookmarks')) | ||
|
r22239 | |||
|
r43346 | explicit = { | ||
repo._bookmarks.expandname(bookmark) for bookmark in pushop.bookmarks | ||||
} | ||||
|
r22651 | |||
|
r30583 | comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark) | ||
|
r36956 | return _processcompared(pushop, ancestors, explicit, remotebookmark, comp) | ||
|
r43346 | |||
|
r36956 | def _processcompared(pushop, pushed, explicit, remotebms, comp): | ||
|
r43184 | """take decision on bookmarks to push to the remote repo | ||
|
r36956 | |||
|
r43184 | Exists to help extensions alter this behavior. | ||
|
r36956 | """ | ||
|
r23081 | addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp | ||
|
r30583 | |||
|
r36956 | repo = pushop.repo | ||
|
r22239 | for b, scid, dcid in advsrc: | ||
|
r22651 | if b in explicit: | ||
explicit.remove(b) | ||||
|
r36956 | if not pushed or repo[scid].rev() in pushed: | ||
|
r22239 | pushop.outbookmarks.append((b, dcid, scid)) | ||
|
r22651 | # search added bookmark | ||
for b, scid, dcid in addsrc: | ||||
if b in explicit: | ||||
explicit.remove(b) | ||||
|
r44854 | if bookmod.isdivergent(b): | ||
pushop.ui.warn(_(b'cannot push divergent bookmark %s!\n') % b) | ||||
pushop.bkresult = 2 | ||||
else: | ||||
pushop.outbookmarks.append((b, b'', scid)) | ||||
|
r22651 | # search for overwritten bookmark | ||
|
r30583 | for b, scid, dcid in list(advdst) + list(diverge) + list(differ): | ||
|
r22651 | if b in explicit: | ||
explicit.remove(b) | ||||
pushop.outbookmarks.append((b, dcid, scid)) | ||||
# search for bookmark to delete | ||||
for b, scid, dcid in adddst: | ||||
if b in explicit: | ||||
explicit.remove(b) | ||||
# treat as "deleted locally" | ||||
|
r43347 | pushop.outbookmarks.append((b, dcid, b'')) | ||
|
r23082 | # identical bookmarks shouldn't get reported | ||
for b, scid, dcid in same: | ||||
if b in explicit: | ||||
explicit.remove(b) | ||||
|
r22651 | |||
if explicit: | ||||
explicit = sorted(explicit) | ||||
# we should probably list all of them | ||||
|
r43346 | pushop.ui.warn( | ||
_( | ||||
|
r43347 | b'bookmark %s does not exist on the local ' | ||
b'or remote repository!\n' | ||||
|
r43346 | ) | ||
% explicit[0] | ||||
) | ||||
|
r22651 | pushop.bkresult = 2 | ||
pushop.outbookmarks.sort() | ||||
|
r22239 | |||
|
r43346 | |||
|
r20465 | def _pushcheckoutgoing(pushop): | ||
outgoing = pushop.outgoing | ||||
unfi = pushop.repo.unfiltered() | ||||
if not outgoing.missing: | ||||
# nothing to push | ||||
scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded) | ||||
return False | ||||
# something to push | ||||
if not pushop.force: | ||||
# if repo.obsstore == False --> no obsolete | ||||
# then, save the iteration | ||||
if unfi.obsstore: | ||||
r45782 | # this message are here for 80 char limit reason | |||
mso = _(b"push includes obsolete changeset: %s!") | ||||
mspd = _(b"push includes phase-divergent changeset: %s!") | ||||
mscd = _(b"push includes content-divergent changeset: %s!") | ||||
mst = { | ||||
b"orphan": _(b"push includes orphan changeset: %s!"), | ||||
b"phase-divergent": mspd, | ||||
b"content-divergent": mscd, | ||||
} | ||||
# If we are to push if there is at least one | ||||
# obsolete or unstable changeset in missing, at | ||||
# least one of the missinghead will be obsolete or | ||||
# unstable. So checking heads only is ok | ||||
for node in outgoing.ancestorsof: | ||||
|
r20465 | ctx = unfi[node] | ||
if ctx.obsolete(): | ||||
r45782 | raise error.Abort(mso % ctx) | |||
|
r33696 | elif ctx.isunstable(): | ||
r45782 | # TODO print more than one instability in the abort | |||
# message | ||||
raise error.Abort(mst[ctx.instabilities()[0]] % ctx) | ||||
|
r25836 | |||
|
r26935 | discovery.checkheads(pushop) | ||
|
r20465 | return True | ||
|
r43346 | |||
|
r22017 | # List of names of steps to perform for an outgoing bundle2, order matters. | ||
b2partsgenorder = [] | ||||
# Mapping between step name and function | ||||
# | ||||
# This exists to help extensions wrap steps if necessary | ||||
b2partsgenmapping = {} | ||||
|
r43346 | |||
|
r24731 | def b2partsgenerator(stepname, idx=None): | ||
|
r22017 | """decorator for function generating bundle2 part | ||
The function is added to the step -> function mapping and appended to the | ||||
list of steps. Beware that decorated functions will be added in order | ||||
(this may matter). | ||||
You can only use this decorator for new steps, if you want to wrap a step | ||||
from an extension, attack the b2partsgenmapping dictionary directly.""" | ||||
|
r43346 | |||
|
r22017 | def dec(func): | ||
assert stepname not in b2partsgenmapping | ||||
b2partsgenmapping[stepname] = func | ||||
|
r24731 | if idx is None: | ||
b2partsgenorder.append(stepname) | ||||
else: | ||||
b2partsgenorder.insert(idx, stepname) | ||||
|
r22017 | return func | ||
|
r43346 | |||
|
r22017 | return dec | ||
|
r43346 | |||
|
r26428 | def _pushb2ctxcheckheads(pushop, bundler): | ||
"""Generate race condition checking parts | ||||
|
r26781 | Exists as an independent function to aid extensions | ||
|
r26428 | """ | ||
r32709 | # * 'force' do not check for push race, | |||
# * if we don't push anything, there are nothing to check. | ||||
|
r45704 | if not pushop.force and pushop.outgoing.ancestorsof: | ||
|
r43347 | allowunrelated = b'related' in bundler.capabilities.get( | ||
b'checkheads', () | ||||
) | ||||
r33133 | emptyremote = pushop.pushbranchmap is None | |||
if not allowunrelated or emptyremote: | ||||
|
r43347 | bundler.newpart(b'check:heads', data=iter(pushop.remoteheads)) | ||
r32709 | else: | |||
affected = set() | ||||
|
r43376 | for branch, heads in pycompat.iteritems(pushop.pushbranchmap): | ||
r32709 | remoteheads, newheads, unsyncedheads, discardedheads = heads | |||
if remoteheads is not None: | ||||
remote = set(remoteheads) | ||||
affected |= set(discardedheads) & remote | ||||
affected |= remote - set(newheads) | ||||
if affected: | ||||
data = iter(sorted(affected)) | ||||
|
r43347 | bundler.newpart(b'check:updated-heads', data=data) | ||
|
r26428 | |||
|
r43346 | |||
|
r34822 | def _pushing(pushop): | ||
"""return True if we are pushing anything""" | ||||
|
r43346 | return bool( | ||
pushop.outgoing.missing | ||||
or pushop.outdatedphases | ||||
or pushop.outobsmarkers | ||||
or pushop.outbookmarks | ||||
) | ||||
|
r34822 | |||
|
r43347 | @b2partsgenerator(b'check-bookmarks') | ||
|
r35260 | def _pushb2checkbookmarks(pushop, bundler): | ||
"""insert bookmark move checking""" | ||||
if not _pushing(pushop) or pushop.force: | ||||
return | ||||
b2caps = bundle2.bundle2caps(pushop.remote) | ||||
|
r43347 | hasbookmarkcheck = b'bookmarks' in b2caps | ||
|
r35260 | if not (pushop.outbookmarks and hasbookmarkcheck): | ||
return | ||||
data = [] | ||||
for book, old, new in pushop.outbookmarks: | ||||
data.append((book, old)) | ||||
|
r47538 | checkdata = bookmod.binaryencode(pushop.repo, data) | ||
|
r43347 | bundler.newpart(b'check:bookmarks', data=checkdata) | ||
@b2partsgenerator(b'check-phases') | ||||
|
r34822 | def _pushb2checkphases(pushop, bundler): | ||
"""insert phase move checking""" | ||||
if not _pushing(pushop) or pushop.force: | ||||
return | ||||
b2caps = bundle2.bundle2caps(pushop.remote) | ||||
|
r43347 | hasphaseheads = b'heads' in b2caps.get(b'phases', ()) | ||
|
r34822 | if pushop.remotephases is not None and hasphaseheads: | ||
# check that the remote phase has not changed | ||||
|
r45677 | checks = {p: [] for p in phases.allphases} | ||
|
r34822 | checks[phases.public].extend(pushop.remotephases.publicheads) | ||
checks[phases.draft].extend(pushop.remotephases.draftroots) | ||||
|
r45677 | if any(pycompat.itervalues(checks)): | ||
for phase in checks: | ||||
checks[phase].sort() | ||||
|
r34822 | checkdata = phases.binaryencode(checks) | ||
|
r43347 | bundler.newpart(b'check:phases', data=checkdata) | ||
@b2partsgenerator(b'changeset') | ||||
|
r21899 | def _pushb2ctx(pushop, bundler): | ||
"""handle changegroup push through bundle2 | ||||
|
r22615 | addchangegroup result is stored in the ``pushop.cgresult`` attribute. | ||
|
r21899 | """ | ||
|
r43347 | if b'changesets' in pushop.stepsdone: | ||
|
r21902 | return | ||
|
r43347 | pushop.stepsdone.add(b'changesets') | ||
|
r21899 | # Send known heads to the server for race detection. | ||
|
r21903 | if not _pushcheckoutgoing(pushop): | ||
return | ||||
|
r28876 | pushop.repo.prepushoutgoinghooks(pushop) | ||
|
r26428 | |||
_pushb2ctxcheckheads(pushop, bundler) | ||||
|
r23180 | b2caps = bundle2.bundle2caps(pushop.remote) | ||
|
r43347 | version = b'01' | ||
cgversions = b2caps.get(b'changegroup') | ||||
|
r28668 | if cgversions: # 3.1 and 3.2 ship with an empty value | ||
|
r43346 | cgversions = [ | ||
v | ||||
for v in cgversions | ||||
if v in changegroup.supportedoutgoingversions(pushop.repo) | ||||
] | ||||
|
r23180 | if not cgversions: | ||
|
r43347 | raise error.Abort(_(b'no common changegroup version')) | ||
|
r23180 | version = max(cgversions) | ||
|
r47447 | |||
remote_sidedata = bundle2.read_remote_wanted_sidedata(pushop.remote) | ||||
|
r43346 | cgstream = changegroup.makestream( | ||
|
r47447 | pushop.repo, | ||
pushop.outgoing, | ||||
version, | ||||
b'push', | ||||
bundlecaps=b2caps, | ||||
remote_sidedata=remote_sidedata, | ||||
|
r43346 | ) | ||
|
r43347 | cgpart = bundler.newpart(b'changegroup', data=cgstream) | ||
|
r28668 | if cgversions: | ||
|
r43347 | cgpart.addparam(b'version', version) | ||
|
r46129 | if scmutil.istreemanifest(pushop.repo): | ||
|
r43347 | cgpart.addparam(b'treemanifest', b'1') | ||
r48000 | if repository.REPO_FEATURE_SIDE_DATA in pushop.repo.features: | |||
r43401 | cgpart.addparam(b'exp-sidedata', b'1') | |||
|
r43346 | |||
|
r21899 | def handlereply(op): | ||
|
r23139 | """extract addchangegroup returns from server reply""" | ||
|
r21899 | cgreplies = op.records.getreplies(cgpart.id) | ||
|
r43347 | assert len(cgreplies[b'changegroup']) == 1 | ||
pushop.cgresult = cgreplies[b'changegroup'][0][b'return'] | ||||
|
r43346 | |||
|
r21899 | return handlereply | ||
|
r43346 | |||
|
r43347 | @b2partsgenerator(b'phase') | ||
|
r22020 | def _pushb2phases(pushop, bundler): | ||
"""handle phase push through bundle2""" | ||||
|
r43347 | if b'phases' in pushop.stepsdone: | ||
|
r22020 | return | ||
b2caps = bundle2.bundle2caps(pushop.remote) | ||||
|
r34837 | ui = pushop.repo.ui | ||
|
r43347 | legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange') | ||
haspushkey = b'pushkey' in b2caps | ||||
hasphaseheads = b'heads' in b2caps.get(b'phases', ()) | ||||
|
r34837 | |||
if hasphaseheads and not legacyphase: | ||||
|
r34911 | return _pushb2phaseheads(pushop, bundler) | ||
|
r34837 | elif haspushkey: | ||
|
r34911 | return _pushb2phasespushkey(pushop, bundler) | ||
|
r34823 | |||
|
r43346 | |||
|
r34837 | def _pushb2phaseheads(pushop, bundler): | ||
"""push phase information through a bundle2 - binary part""" | ||||
|
r43347 | pushop.stepsdone.add(b'phases') | ||
|
r34837 | if pushop.outdatedphases: | ||
|
r45677 | updates = {p: [] for p in phases.allphases} | ||
|
r34837 | updates[0].extend(h.node() for h in pushop.outdatedphases) | ||
phasedata = phases.binaryencode(updates) | ||||
|
r43347 | bundler.newpart(b'phase-heads', data=phasedata) | ||
|
r34837 | |||
|
r43346 | |||
|
r34823 | def _pushb2phasespushkey(pushop, bundler): | ||
"""push phase information through a bundle2 - pushkey part""" | ||||
|
r43347 | pushop.stepsdone.add(b'phases') | ||
|
r22020 | part2node = [] | ||
|
r25502 | |||
def handlefailure(pushop, exc): | ||||
targetid = int(exc.partid) | ||||
for partid, node in part2node: | ||||
if partid == targetid: | ||||
|
r43347 | raise error.Abort(_(b'updating %s to public failed') % node) | ||
|
r25502 | |||
|
r22020 | enc = pushkey.encode | ||
for newremotehead in pushop.outdatedphases: | ||||
|
r43347 | part = bundler.newpart(b'pushkey') | ||
part.addparam(b'namespace', enc(b'phases')) | ||||
part.addparam(b'key', enc(newremotehead.hex())) | ||||
part.addparam(b'old', enc(b'%d' % phases.draft)) | ||||
part.addparam(b'new', enc(b'%d' % phases.public)) | ||||
|
r22020 | part2node.append((part.id, newremotehead)) | ||
|
r25502 | pushop.pkfailcb[part.id] = handlefailure | ||
|
r22020 | def handlereply(op): | ||
for partid, node in part2node: | ||||
partrep = op.records.getreplies(partid) | ||||
|
r43347 | results = partrep[b'pushkey'] | ||
|
r22020 | assert len(results) <= 1 | ||
msg = None | ||||
if not results: | ||||
|
r43347 | msg = _(b'server ignored update of %s to public!\n') % node | ||
elif not int(results[0][b'return']): | ||||
msg = _(b'updating %s to public failed!\n') % node | ||||
|
r22020 | if msg is not None: | ||
pushop.ui.warn(msg) | ||||
|
r43346 | |||
|
r22020 | return handlereply | ||
|
r21904 | |||
|
r43346 | |||
|
r43347 | @b2partsgenerator(b'obsmarkers') | ||
|
r22347 | def _pushb2obsmarkers(pushop, bundler): | ||
|
r43347 | if b'obsmarkers' in pushop.stepsdone: | ||
|
r22347 | return | ||
remoteversions = bundle2.obsmarkersversion(bundler.capabilities) | ||||
if obsolete.commonversion(remoteversions) is None: | ||||
return | ||||
|
r43347 | pushop.stepsdone.add(b'obsmarkers') | ||
|
r22347 | if pushop.outobsmarkers: | ||
|
r43858 | markers = obsutil.sortedmarkers(pushop.outobsmarkers) | ||
r32515 | bundle2.buildobsmarkerspart(bundler, markers) | |||
|
r22347 | |||
|
r43346 | |||
|
r43347 | @b2partsgenerator(b'bookmarks') | ||
|
r22242 | def _pushb2bookmarks(pushop, bundler): | ||
|
r25895 | """handle bookmark push through bundle2""" | ||
|
r43347 | if b'bookmarks' in pushop.stepsdone: | ||
|
r22242 | return | ||
b2caps = bundle2.bundle2caps(pushop.remote) | ||||
|
r35265 | |||
|
r43347 | legacy = pushop.repo.ui.configlist(b'devel', b'legacy.exchange') | ||
legacybooks = b'bookmarks' in legacy | ||||
if not legacybooks and b'bookmarks' in b2caps: | ||||
|
r35265 | return _pushb2bookmarkspart(pushop, bundler) | ||
|
r43347 | elif b'pushkey' in b2caps: | ||
|
r35263 | return _pushb2bookmarkspushkey(pushop, bundler) | ||
|
r43346 | |||
|
r35265 | def _bmaction(old, new): | ||
"""small utility for bookmark pushing""" | ||||
if not old: | ||||
|
r43347 | return b'export' | ||
|
r35265 | elif not new: | ||
|
r43347 | return b'delete' | ||
return b'update' | ||||
|
r35265 | |||
|
r43346 | |||
|
r43082 | def _abortonsecretctx(pushop, node, b): | ||
"""abort if a given bookmark points to a secret changeset""" | ||||
if node and pushop.repo[node].phase() == phases.secret: | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'cannot push bookmark %s as it points to a secret changeset') % b | ||
|
r43346 | ) | ||
|
r43082 | |||
|
r35265 | def _pushb2bookmarkspart(pushop, bundler): | ||
|
r43347 | pushop.stepsdone.add(b'bookmarks') | ||
|
r35265 | if not pushop.outbookmarks: | ||
return | ||||
allactions = [] | ||||
data = [] | ||||
for book, old, new in pushop.outbookmarks: | ||||
|
r43082 | _abortonsecretctx(pushop, new, book) | ||
|
r35265 | data.append((book, new)) | ||
allactions.append((book, _bmaction(old, new))) | ||||
|
r47538 | checkdata = bookmod.binaryencode(pushop.repo, data) | ||
|
r43347 | bundler.newpart(b'bookmarks', data=checkdata) | ||
|
r35265 | |||
def handlereply(op): | ||||
ui = pushop.ui | ||||
# if success | ||||
for book, action in allactions: | ||||
ui.status(bookmsgmap[action][0] % book) | ||||
return handlereply | ||||
|
r43346 | |||
|
r35263 | def _pushb2bookmarkspushkey(pushop, bundler): | ||
|
r43347 | pushop.stepsdone.add(b'bookmarks') | ||
|
r22242 | part2book = [] | ||
enc = pushkey.encode | ||||
|
r25501 | |||
def handlefailure(pushop, exc): | ||||
targetid = int(exc.partid) | ||||
for partid, book, action in part2book: | ||||
if partid == targetid: | ||||
raise error.Abort(bookmsgmap[action][1].rstrip() % book) | ||||
# we should not be called for part we did not generated | ||||
assert False | ||||
|
r22242 | for book, old, new in pushop.outbookmarks: | ||
|
r43082 | _abortonsecretctx(pushop, new, book) | ||
|
r43347 | part = bundler.newpart(b'pushkey') | ||
part.addparam(b'namespace', enc(b'bookmarks')) | ||||
part.addparam(b'key', enc(book)) | ||||
part.addparam(b'old', enc(hex(old))) | ||||
part.addparam(b'new', enc(hex(new))) | ||||
action = b'update' | ||||
|
r22650 | if not old: | ||
|
r43347 | action = b'export' | ||
|
r22650 | elif not new: | ||
|
r43347 | action = b'delete' | ||
|
r22650 | part2book.append((part.id, book, action)) | ||
|
r25501 | pushop.pkfailcb[part.id] = handlefailure | ||
|
r22650 | |||
|
r22242 | def handlereply(op): | ||
|
r22650 | ui = pushop.ui | ||
for partid, book, action in part2book: | ||||
|
r22242 | partrep = op.records.getreplies(partid) | ||
|
r43347 | results = partrep[b'pushkey'] | ||
|
r22242 | assert len(results) <= 1 | ||
if not results: | ||||
|
r43347 | pushop.ui.warn(_(b'server ignored bookmark %s update\n') % book) | ||
|
r22242 | else: | ||
|
r43347 | ret = int(results[0][b'return']) | ||
|
r22242 | if ret: | ||
|
r22650 | ui.status(bookmsgmap[action][0] % book) | ||
|
r22242 | else: | ||
|
r22650 | ui.warn(bookmsgmap[action][1] % book) | ||
|
r22649 | if pushop.bkresult is not None: | ||
pushop.bkresult = 1 | ||||
|
r43346 | |||
|
r22242 | return handlereply | ||
|
r43346 | |||
|
r43347 | @b2partsgenerator(b'pushvars', idx=0) | ||
|
r33656 | def _getbundlesendvars(pushop, bundler): | ||
'''send shellvars via bundle2''' | ||||
|
r33886 | pushvars = pushop.pushvars | ||
if pushvars: | ||||
shellvars = {} | ||||
for raw in pushvars: | ||||
|
r43347 | if b'=' not in raw: | ||
|
r43346 | msg = ( | ||
|
r43347 | b"unable to parse variable '%s', should follow " | ||
b"'KEY=VALUE' or 'KEY=' format" | ||||
|
r43346 | ) | ||
|
r33886 | raise error.Abort(msg % raw) | ||
|
r43347 | k, v = raw.split(b'=', 1) | ||
|
r33886 | shellvars[k] = v | ||
|
r43347 | part = bundler.newpart(b'pushvars') | ||
|
r33656 | |||
|
r43376 | for key, value in pycompat.iteritems(shellvars): | ||
|
r33656 | part.addparam(key, value, mandatory=False) | ||
|
r22242 | |||
|
r43346 | |||
|
r21061 | def _pushbundle2(pushop): | ||
"""push data to the remote using bundle2 | ||||
The only currently supported type of data is changegroup but this will | ||||
evolve in the future.""" | ||||
|
r21644 | bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote)) | ||
|
r43346 | pushback = pushop.trmanager and pushop.ui.configbool( | ||
|
r43347 | b'experimental', b'bundle2.pushback' | ||
|
r43346 | ) | ||
|
r23439 | |||
|
r21142 | # create reply capability | ||
|
r43346 | capsblob = bundle2.encodecaps( | ||
|
r43347 | bundle2.getrepocaps(pushop.repo, allowpushback=pushback, role=b'client') | ||
|
r43346 | ) | ||
|
r43347 | bundler.newpart(b'replycaps', data=capsblob) | ||
|
r21904 | replyhandlers = [] | ||
|
r22017 | for partgenname in b2partsgenorder: | ||
partgen = b2partsgenmapping[partgenname] | ||||
|
r21904 | ret = partgen(pushop, bundler) | ||
|
r21941 | if callable(ret): | ||
replyhandlers.append(ret) | ||||
|
r21904 | # do not push if nothing to push | ||
|
r21903 | if bundler.nbparts <= 1: | ||
return | ||||
|
r21061 | stream = util.chunkbuffer(bundler.getchunks()) | ||
|
r21182 | try: | ||
|
r25485 | try: | ||
|
r37664 | with pushop.remote.commandexecutor() as e: | ||
|
r43346 | reply = e.callcommand( | ||
|
r43347 | b'unbundle', | ||
|
r43346 | { | ||
|
r43347 | b'bundle': stream, | ||
b'heads': [b'force'], | ||||
b'url': pushop.remote.url(), | ||||
|
r43346 | }, | ||
).result() | ||||
|
r25660 | except error.BundleValueError as exc: | ||
|
r47739 | raise error.RemoteError(_(b'missing support for %s') % exc) | ||
|
r25485 | try: | ||
trgetter = None | ||||
if pushback: | ||||
trgetter = pushop.trmanager.transaction | ||||
op = bundle2.processbundle(pushop.repo, reply, trgetter) | ||||
|
r25660 | except error.BundleValueError as exc: | ||
|
r47739 | raise error.RemoteError(_(b'missing support for %s') % exc) | ||
|
r26829 | except bundle2.AbortFromPart as exc: | ||
|
r47207 | pushop.ui.error(_(b'remote: %s\n') % exc) | ||
|
r30908 | if exc.hint is not None: | ||
|
r47207 | pushop.ui.error(_(b'remote: %s\n') % (b'(%s)' % exc.hint)) | ||
|
r47739 | raise error.RemoteError(_(b'push failed on remote')) | ||
|
r25660 | except error.PushkeyFailed as exc: | ||
|
r25485 | partid = int(exc.partid) | ||
if partid not in pushop.pkfailcb: | ||||
raise | ||||
pushop.pkfailcb[partid](pushop, exc) | ||||
|
r21904 | for rephand in replyhandlers: | ||
rephand(op) | ||||
|
r21061 | |||
|
r43346 | |||
|
r20463 | def _pushchangeset(pushop): | ||
"""Make the actual push of changeset bundle to remote repo""" | ||||
|
r43347 | if b'changesets' in pushop.stepsdone: | ||
|
r21902 | return | ||
|
r43347 | pushop.stepsdone.add(b'changesets') | ||
|
r21903 | if not _pushcheckoutgoing(pushop): | ||
return | ||||
|
r33667 | |||
# Should have verified this in push(). | ||||
|
r43347 | assert pushop.remote.capable(b'unbundle') | ||
|
r33667 | |||
|
r28876 | pushop.repo.prepushoutgoinghooks(pushop) | ||
|
r20463 | outgoing = pushop.outgoing | ||
# TODO: get bundlecaps from remote | ||||
bundlecaps = None | ||||
# create a changegroup from local | ||||
|
r43346 | if pushop.revs is None and not ( | ||
outgoing.excluded or pushop.repo.changelog.filteredrevs | ||||
): | ||||
|
r20463 | # push everything, | ||
# use the fast path, no race possible on push | ||||
|
r43346 | cg = changegroup.makechangegroup( | ||
pushop.repo, | ||||
outgoing, | ||||
|
r43347 | b'01', | ||
b'push', | ||||
|
r43346 | fastpath=True, | ||
bundlecaps=bundlecaps, | ||||
) | ||||
|
r20463 | else: | ||
|
r43346 | cg = changegroup.makechangegroup( | ||
|
r43347 | pushop.repo, outgoing, b'01', b'push', bundlecaps=bundlecaps | ||
|
r43346 | ) | ||
|
r20463 | |||
# apply changegroup to remote | ||||
|
r33667 | # local repo finds heads on server, finds out what | ||
# revs it must push. once revs transferred, if server | ||||
# finds it has different heads (someone else won | ||||
# commit/push race), server aborts. | ||||
if pushop.force: | ||||
|
r43347 | remoteheads = [b'force'] | ||
|
r20463 | else: | ||
|
r33667 | remoteheads = pushop.remoteheads | ||
# ssh: return remote's addchangegroup() | ||||
# http: return remote's addchangegroup() or 0 for error | ||||
|
r43346 | pushop.cgresult = pushop.remote.unbundle(cg, remoteheads, pushop.repo.url()) | ||
|
r20463 | |||
|
r20441 | def _pushsyncphase(pushop): | ||
|
r21024 | """synchronise phase information locally and remotely""" | ||
|
r20468 | cheads = pushop.commonheads | ||
|
r20441 | # even when we don't push, exchanging phase data is useful | ||
|
r43347 | remotephases = listkeys(pushop.remote, b'phases') | ||
|
r43346 | if ( | ||
|
r43347 | pushop.ui.configbool(b'ui', b'_usedassubrepo') | ||
|
r43346 | and remotephases # server supports phases | ||
and pushop.cgresult is None # nothing was pushed | ||||
|
r43347 | and remotephases.get(b'publishing', False) | ||
|
r43346 | ): | ||
|
r20441 | # When: | ||
# - this is a subrepo push | ||||
# - and remote support phase | ||||
# - and no changeset was pushed | ||||
# - and remote is publishing | ||||
# We may be in issue 3871 case! | ||||
# We drop the possible phase synchronisation done by | ||||
# courtesy to publish changesets possibly locally draft | ||||
# on the remote. | ||||
|
r43347 | remotephases = {b'publishing': b'True'} | ||
|
r43346 | if not remotephases: # old server or public only reply from non-publishing | ||
|
r20441 | _localphasemove(pushop, cheads) | ||
# don't push any phase data as there is nothing to push | ||||
else: | ||||
|
r43346 | ana = phases.analyzeremotephases(pushop.repo, cheads, remotephases) | ||
|
r20441 | pheads, droots = ana | ||
### Apply remote phase on local | ||||
|
r43347 | if remotephases.get(b'publishing', False): | ||
|
r20441 | _localphasemove(pushop, cheads) | ||
|
r43346 | else: # publish = False | ||
|
r20441 | _localphasemove(pushop, pheads) | ||
_localphasemove(pushop, cheads, phases.draft) | ||||
### Apply local phase on remote | ||||
|
r22615 | if pushop.cgresult: | ||
|
r43347 | if b'phases' in pushop.stepsdone: | ||
|
r22020 | # phases already pushed though bundle2 | ||
return | ||||
|
r22019 | outdated = pushop.outdatedphases | ||
else: | ||||
outdated = pushop.fallbackoutdatedphases | ||||
|
r43347 | pushop.stepsdone.add(b'phases') | ||
|
r22020 | |||
|
r22019 | # filter heads already turned public by the push | ||
outdated = [c for c in outdated if c.node() not in pheads] | ||||
|
r23376 | # fallback to independent pushkey command | ||
for newremotehead in outdated: | ||||
|
r37665 | with pushop.remote.commandexecutor() as e: | ||
|
r43346 | r = e.callcommand( | ||
|
r43347 | b'pushkey', | ||
|
r43346 | { | ||
|
r43347 | b'namespace': b'phases', | ||
b'key': newremotehead.hex(), | ||||
b'old': b'%d' % phases.draft, | ||||
b'new': b'%d' % phases.public, | ||||
|
r43346 | }, | ||
).result() | ||||
|
r37665 | |||
|
r23376 | if not r: | ||
|
r43346 | pushop.ui.warn( | ||
|
r43347 | _(b'updating %s to public failed!\n') % newremotehead | ||
|
r43346 | ) | ||
|
r20441 | |||
|
r20438 | def _localphasemove(pushop, nodes, phase=phases.public): | ||
"""move <nodes> to <phase> in the local source repo""" | ||||
|
r23437 | if pushop.trmanager: | ||
|
r43346 | phases.advanceboundary( | ||
pushop.repo, pushop.trmanager.transaction(), phase, nodes | ||||
) | ||||
|
r20438 | else: | ||
# repo is not locked, do not change any phases! | ||||
# Informs the user that phases should have been moved when | ||||
# applicable. | ||||
actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()] | ||||
phasestr = phases.phasenames[phase] | ||||
if actualmoves: | ||||
|
r43346 | pushop.ui.status( | ||
_( | ||||
|
r43347 | b'cannot lock source repo, skipping ' | ||
b'local %s phase update\n' | ||||
|
r43346 | ) | ||
% phasestr | ||||
) | ||||
|
r20438 | |||
|
r20433 | def _pushobsolete(pushop): | ||
|
r20434 | """utility function to push obsolete markers to a remote""" | ||
|
r43347 | if b'obsmarkers' in pushop.stepsdone: | ||
|
r22036 | return | ||
|
r20433 | repo = pushop.repo | ||
remote = pushop.remote | ||||
|
r43347 | pushop.stepsdone.add(b'obsmarkers') | ||
|
r22350 | if pushop.outobsmarkers: | ||
|
r43347 | pushop.ui.debug(b'try to push obsolete markers to remote\n') | ||
|
r20432 | rslts = [] | ||
|
r43858 | markers = obsutil.sortedmarkers(pushop.outobsmarkers) | ||
|
r43572 | remotedata = obsolete._pushkeyescape(markers) | ||
|
r20432 | for key in sorted(remotedata, reverse=True): | ||
# reverse sort to ensure we end with dump0 | ||||
data = remotedata[key] | ||||
|
r43347 | rslts.append(remote.pushkey(b'obsolete', key, b'', data)) | ||
|
r20432 | if [r for r in rslts if not r]: | ||
|
r43347 | msg = _(b'failed to push some obsolete markers!\n') | ||
|
r20432 | repo.ui.warn(msg) | ||
|
r43346 | |||
|
r20431 | def _pushbookmark(pushop): | ||
|
r20352 | """Update bookmark position on remote""" | ||
|
r43347 | if pushop.cgresult == 0 or b'bookmarks' in pushop.stepsdone: | ||
|
r22228 | return | ||
|
r43347 | pushop.stepsdone.add(b'bookmarks') | ||
|
r20431 | ui = pushop.ui | ||
remote = pushop.remote | ||||
|
r22650 | |||
|
r22239 | for b, old, new in pushop.outbookmarks: | ||
|
r43347 | action = b'update' | ||
|
r22650 | if not old: | ||
|
r43347 | action = b'export' | ||
|
r22650 | elif not new: | ||
|
r43347 | action = b'delete' | ||
|
r37665 | |||
with remote.commandexecutor() as e: | ||||
|
r43346 | r = e.callcommand( | ||
|
r43347 | b'pushkey', | ||
|
r43346 | { | ||
|
r43347 | b'namespace': b'bookmarks', | ||
b'key': b, | ||||
b'old': hex(old), | ||||
b'new': hex(new), | ||||
|
r43346 | }, | ||
).result() | ||||
|
r37665 | |||
if r: | ||||
|
r22650 | ui.status(bookmsgmap[action][0] % b) | ||
|
r20352 | else: | ||
|
r22650 | ui.warn(bookmsgmap[action][1] % b) | ||
# discovery can have set the value form invalid entry | ||||
if pushop.bkresult is not None: | ||||
pushop.bkresult = 1 | ||||
|
r20469 | |||
|
r43346 | |||
|
r20472 | class pulloperation(object): | ||
"""A object that represent a single pull operation | ||||
|
r23219 | It purpose is to carry pull related state and very common operation. | ||
|
r20472 | |||
|
r21024 | A new should be created at the beginning of each pull and discarded | ||
|
r20472 | afterward. | ||
""" | ||||
|
r43346 | def __init__( | ||
self, | ||||
repo, | ||||
remote, | ||||
heads=None, | ||||
force=False, | ||||
bookmarks=(), | ||||
remotebookmarks=None, | ||||
streamclonerequested=None, | ||||
includepats=None, | ||||
excludepats=None, | ||||
depth=None, | ||||
): | ||||
|
r20596 | # repo we pull into | ||
|
r20472 | self.repo = repo | ||
|
r20596 | # repo we pull from | ||
|
r20473 | self.remote = remote | ||
|
r20474 | # revision we try to pull (None is "all") | ||
self.heads = heads | ||||
|
r22654 | # bookmark pulled explicitly | ||
|
r43346 | self.explicitbookmarks = [ | ||
repo._bookmarks.expandname(bookmark) for bookmark in bookmarks | ||||
] | ||||
|
r20475 | # do we force pull? | ||
self.force = force | ||||
|
r26448 | # whether a streaming clone was requested | ||
self.streamclonerequested = streamclonerequested | ||||
|
r23436 | # transaction manager | ||
self.trmanager = None | ||||
|
r20487 | # set of common changeset between local and remote before pull | ||
self.common = None | ||||
# set of pulled head | ||||
self.rheads = None | ||||
|
r21024 | # list of missing changeset to fetch remotely | ||
|
r20488 | self.fetch = None | ||
|
r22654 | # remote bookmarks data | ||
|
r25446 | self.remotebookmarks = remotebookmarks | ||
|
r21024 | # result of changegroup pulling (used as return code by pull) | ||
|
r20898 | self.cgresult = None | ||
|
r22937 | # list of step already done | ||
self.stepsdone = set() | ||||
|
r26689 | # Whether we attempted a clone from pre-generated bundles. | ||
self.clonebundleattempted = False | ||||
|
r39589 | # Set of file patterns to include. | ||
self.includepats = includepats | ||||
# Set of file patterns to exclude. | ||||
self.excludepats = excludepats | ||||
|
r40367 | # Number of ancestor changesets to pull from each pulled head. | ||
self.depth = depth | ||||
|
r20487 | |||
@util.propertycache | ||||
def pulledsubset(self): | ||||
"""heads of the set of changeset target by the pull""" | ||||
# compute target subset | ||||
if self.heads is None: | ||||
# We pulled every thing possible | ||||
# sync on everything common | ||||
|
r20878 | c = set(self.common) | ||
ret = list(self.common) | ||||
for n in self.rheads: | ||||
if n not in c: | ||||
ret.append(n) | ||||
return ret | ||||
|
r20487 | else: | ||
# We pulled a specific subset | ||||
# sync on this subset | ||||
return self.heads | ||||
|
r20477 | |||
|
r26464 | @util.propertycache | ||
|
r26465 | def canusebundle2(self): | ||
|
r29682 | return not _forcebundle1(self) | ||
|
r26465 | |||
@util.propertycache | ||||
|
r26464 | def remotebundle2caps(self): | ||
return bundle2.bundle2caps(self.remote) | ||||
|
r20477 | def gettransaction(self): | ||
|
r23436 | # deprecated; talk to trmanager directly | ||
return self.trmanager.transaction() | ||||
|
r43346 | |||
|
r33790 | class transactionmanager(util.transactional): | ||
|
r23543 | """An object to manage the life cycle of a transaction | ||
|
r23436 | |||
It creates the transaction on demand and calls the appropriate hooks when | ||||
closing the transaction.""" | ||||
|
r43346 | |||
|
r23436 | def __init__(self, repo, source, url): | ||
self.repo = repo | ||||
self.source = source | ||||
self.url = url | ||||
self._tr = None | ||||
def transaction(self): | ||||
"""Return an open transaction object, constructing if necessary""" | ||||
if not self._tr: | ||||
r47669 | trname = b'%s\n%s' % (self.source, urlutil.hidepassword(self.url)) | |||
|
r23436 | self._tr = self.repo.transaction(trname) | ||
|
r43347 | self._tr.hookargs[b'source'] = self.source | ||
self._tr.hookargs[b'url'] = self.url | ||||
|
r20477 | return self._tr | ||
|
r23436 | def close(self): | ||
|
r20477 | """close transaction if created""" | ||
if self._tr is not None: | ||||
|
r23222 | self._tr.close() | ||
|
r20477 | |||
|
r23436 | def release(self): | ||
|
r20477 | """release transaction if created""" | ||
if self._tr is not None: | ||||
self._tr.release() | ||||
|
r20469 | |||
|
r43346 | |||
|
r37775 | def listkeys(remote, namespace): | ||
with remote.commandexecutor() as e: | ||||
|
r43347 | return e.callcommand(b'listkeys', {b'namespace': namespace}).result() | ||
|
r37775 | |||
|
r43346 | |||
|
r37516 | def _fullpullbundle2(repo, pullop): | ||
# The server may send a partial reply, i.e. when inlining | ||||
# pre-computed bundles. In that case, update the common | ||||
# set based on the results and pull another bundle. | ||||
# | ||||
# There are two indicators that the process is finished: | ||||
# - no changeset has been added, or | ||||
# - all remote heads are known locally. | ||||
# The head check must use the unfiltered view as obsoletion | ||||
# markers can hide heads. | ||||
unfi = repo.unfiltered() | ||||
unficl = unfi.changelog | ||||
|
r43346 | |||
|
r37516 | def headsofdiff(h1, h2): | ||
"""Returns heads(h1 % h2)""" | ||||
|
r43347 | res = unfi.set(b'heads(%ln %% %ln)', h1, h2) | ||
|
r44937 | return {ctx.node() for ctx in res} | ||
|
r43346 | |||
|
r37516 | def headsofunion(h1, h2): | ||
"""Returns heads((h1 + h2) - null)""" | ||||
|
r43347 | res = unfi.set(b'heads((%ln + %ln - null))', h1, h2) | ||
|
r44937 | return {ctx.node() for ctx in res} | ||
|
r43346 | |||
|
r37516 | while True: | ||
old_heads = unficl.heads() | ||||
clstart = len(unficl) | ||||
_pullbundle2(pullop) | ||||
|
r45932 | if requirements.NARROW_REQUIREMENT in repo.requirements: | ||
|
r37516 | # XXX narrow clones filter the heads on the server side during | ||
# XXX getbundle and result in partial replies as well. | ||||
# XXX Disable pull bundles in this case as band aid to avoid | ||||
# XXX extra round trips. | ||||
break | ||||
if clstart == len(unficl): | ||||
break | ||||
if all(unficl.hasnode(n) for n in pullop.rheads): | ||||
break | ||||
new_heads = headsofdiff(unficl.heads(), old_heads) | ||||
pullop.common = headsofunion(new_heads, pullop.common) | ||||
pullop.rheads = set(pullop.rheads) - pullop.common | ||||
|
r43346 | |||
|
r45033 | def add_confirm_callback(repo, pullop): | ||
|
r46554 | """adds a finalize callback to transaction which can be used to show stats | ||
to user and confirm the pull before committing transaction""" | ||||
|
r45033 | |||
tr = pullop.trmanager.transaction() | ||||
scmutil.registersummarycallback( | ||||
repo, tr, txnname=b'pull', as_validator=True | ||||
) | ||||
reporef = weakref.ref(repo.unfiltered()) | ||||
def prompt(tr): | ||||
repo = reporef() | ||||
cm = _(b'accept incoming changes (yn)?$$ &Yes $$ &No') | ||||
if repo.ui.promptchoice(cm): | ||||
|
r46273 | raise error.Abort(b"user aborted") | ||
|
r45033 | |||
tr.addvalidator(b'900-pull-prompt', prompt) | ||||
|
r43346 | def pull( | ||
repo, | ||||
remote, | ||||
heads=None, | ||||
force=False, | ||||
bookmarks=(), | ||||
opargs=None, | ||||
streamclonerequested=None, | ||||
includepats=None, | ||||
excludepats=None, | ||||
depth=None, | ||||
|
r45033 | confirm=None, | ||
|
r43346 | ): | ||
|
r26440 | """Fetch repository data from a remote. | ||
This is the main function used to retrieve data from a remote repository. | ||||
``repo`` is the local repository to clone into. | ||||
``remote`` is a peer instance. | ||||
``heads`` is an iterable of revisions we want to pull. ``None`` (the | ||||
default) means to pull everything from the remote. | ||||
``bookmarks`` is an iterable of bookmarks requesting to be pulled. By | ||||
default, all remote bookmarks are pulled. | ||||
``opargs`` are additional keyword arguments to pass to ``pulloperation`` | ||||
initialization. | ||||
|
r26448 | ``streamclonerequested`` is a boolean indicating whether a "streaming | ||
clone" is requested. A "streaming clone" is essentially a raw file copy | ||||
of revlogs from the server. This only works when the local repository is | ||||
empty. The default value of ``None`` means to respect the server | ||||
configuration for preferring stream clones. | ||||
|
r39589 | ``includepats`` and ``excludepats`` define explicit file patterns to | ||
include and exclude in storage, respectively. If not defined, narrow | ||||
patterns from the repo instance are used, if available. | ||||
|
r40367 | ``depth`` is an integer indicating the DAG depth of history we're | ||
interested in. If defined, for each revision specified in ``heads``, we | ||||
will fetch up to this many of its ancestors and data associated with them. | ||||
|
r45033 | ``confirm`` is a boolean indicating whether the pull should be confirmed | ||
before committing the transaction. This overrides HGPLAIN. | ||||
|
r26440 | |||
Returns the ``pulloperation`` created for this pull. | ||||
""" | ||||
|
r25445 | if opargs is None: | ||
opargs = {} | ||||
|
r39589 | |||
# We allow the narrow patterns to be passed in explicitly to provide more | ||||
# flexibility for API consumers. | ||||
if includepats or excludepats: | ||||
includepats = includepats or set() | ||||
excludepats = excludepats or set() | ||||
else: | ||||
includepats, excludepats = repo.narrowpats | ||||
narrowspec.validatepatterns(includepats) | ||||
narrowspec.validatepatterns(excludepats) | ||||
|
r43346 | pullop = pulloperation( | ||
repo, | ||||
remote, | ||||
heads, | ||||
force, | ||||
bookmarks=bookmarks, | ||||
streamclonerequested=streamclonerequested, | ||||
includepats=includepats, | ||||
excludepats=excludepats, | ||||
depth=depth, | ||||
**pycompat.strkwargs(opargs) | ||||
) | ||||
|
r33668 | |||
peerlocal = pullop.remote.local() | ||||
if peerlocal: | ||||
missing = set(peerlocal.requirements) - pullop.repo.supported | ||||
|
r20469 | if missing: | ||
|
r43346 | msg = _( | ||
|
r43347 | b"required features are not" | ||
b" supported in the destination:" | ||||
b" %s" | ||||
) % (b', '.join(sorted(missing))) | ||||
|
r26587 | raise error.Abort(msg) | ||
|
r20469 | |||
|
r47447 | for category in repo._wanted_sidedata: | ||
# Check that a computer is registered for that category for at least | ||||
# one revlog kind. | ||||
for kind, computers in repo._sidedata_computers.items(): | ||||
if computers.get(category): | ||||
break | ||||
else: | ||||
# This should never happen since repos are supposed to be able to | ||||
# generate the sidedata they require. | ||||
raise error.ProgrammingError( | ||||
_( | ||||
b'sidedata category requested by local side without local' | ||||
b"support: '%s'" | ||||
) | ||||
% pycompat.bytestr(category) | ||||
) | ||||
|
r43347 | pullop.trmanager = transactionmanager(repo, b'pull', remote.url()) | ||
|
r42513 | wlock = util.nullcontextmanager() | ||
if not bookmod.bookmarksinstore(repo): | ||||
wlock = repo.wlock() | ||||
with wlock, repo.lock(), pullop.trmanager: | ||||
|
r45033 | if confirm or ( | ||
repo.ui.configbool(b"pull", b"confirm") and not repo.ui.plain() | ||||
): | ||||
add_confirm_callback(repo, pullop) | ||||
|
r39665 | # Use the modern wire protocol, if available. | ||
|
r43347 | if remote.capable(b'command-changesetdata'): | ||
|
r39665 | exchangev2.pull(pullop) | ||
else: | ||||
# This should ideally be in _pullbundle2(). However, it needs to run | ||||
# before discovery to avoid extra work. | ||||
_maybeapplyclonebundle(pullop) | ||||
streamclone.maybeperformlegacystreamclone(pullop) | ||||
_pulldiscovery(pullop) | ||||
if pullop.canusebundle2: | ||||
_fullpullbundle2(repo, pullop) | ||||
_pullchangeset(pullop) | ||||
_pullphase(pullop) | ||||
_pullbookmarks(pullop) | ||||
_pullobsolete(pullop) | ||||
|
r20469 | |||
|
r35236 | # storing remotenames | ||
|
r43347 | if repo.ui.configbool(b'experimental', b'remotenames'): | ||
|
r35348 | logexchange.pullremotenames(repo, remote) | ||
|
r35236 | |||
|
r22693 | return pullop | ||
|
r20476 | |||
|
r43346 | |||
|
r22936 | # list of steps to perform discovery before pull | ||
pulldiscoveryorder = [] | ||||
# Mapping between step name and function | ||||
# | ||||
# This exists to help extensions wrap steps if necessary | ||||
pulldiscoverymapping = {} | ||||
|
r43346 | |||
|
r22936 | def pulldiscovery(stepname): | ||
"""decorator for function performing discovery before pull | ||||
The function is added to the step -> function mapping and appended to the | ||||
list of steps. Beware that decorated function will be added in order (this | ||||
may matter). | ||||
You can only use this decorator for a new step, if you want to wrap a step | ||||
from an extension, change the pulldiscovery dictionary directly.""" | ||||
|
r43346 | |||
|
r22936 | def dec(func): | ||
assert stepname not in pulldiscoverymapping | ||||
pulldiscoverymapping[stepname] = func | ||||
pulldiscoveryorder.append(stepname) | ||||
return func | ||||
|
r43346 | |||
|
r22936 | return dec | ||
|
r43346 | |||
|
r20900 | def _pulldiscovery(pullop): | ||
|
r22936 | """Run all discovery steps""" | ||
for stepname in pulldiscoveryorder: | ||||
step = pulldiscoverymapping[stepname] | ||||
step(pullop) | ||||
|
r43346 | |||
|
r43347 | @pulldiscovery(b'b1:bookmarks') | ||
|
r25369 | def _pullbookmarkbundle1(pullop): | ||
"""fetch bookmark data in bundle1 case | ||||
If not using bundle2, we have to fetch bookmarks before changeset | ||||
discovery to reduce the chance and impact of race conditions.""" | ||||
|
r25443 | if pullop.remotebookmarks is not None: | ||
return | ||||
|
r43347 | if pullop.canusebundle2 and b'listkeys' in pullop.remotebundle2caps: | ||
|
r25479 | # all known bundle2 servers now support listkeys, but lets be nice with | ||
# new implementation. | ||||
return | ||||
|
r43347 | books = listkeys(pullop.remote, b'bookmarks') | ||
|
r35030 | pullop.remotebookmarks = bookmod.unhexlifybookmarks(books) | ||
|
r25369 | |||
|
r43347 | @pulldiscovery(b'changegroup') | ||
|
r22936 | def _pulldiscoverychangegroup(pullop): | ||
|
r20900 | """discovery phase for the pull | ||
Current handle changeset discovery only, will change handle all discovery | ||||
at some point.""" | ||||
|
r43346 | tmp = discovery.findcommonincoming( | ||
pullop.repo, pullop.remote, heads=pullop.heads, force=pullop.force | ||||
) | ||||
|
r23848 | common, fetch, rheads = tmp | ||
r43945 | has_node = pullop.repo.unfiltered().changelog.index.has_node | |||
|
r23848 | if fetch and rheads: | ||
|
r34318 | # If a remote heads is filtered locally, put in back in common. | ||
|
r23848 | # | ||
# This is a hackish solution to catch most of "common but locally | ||||
# hidden situation". We do not performs discovery on unfiltered | ||||
# repository because it end up doing a pathological amount of round | ||||
# trip for w huge amount of changeset we do not care about. | ||||
# | ||||
# If a set of such "common but filtered" changeset exist on the server | ||||
# but are not including a remote heads, we'll not be able to detect it, | ||||
scommon = set(common) | ||||
for n in rheads: | ||||
r43945 | if has_node(n): | |||
|
r23975 | if n not in scommon: | ||
common.append(n) | ||||
|
r34318 | if set(rheads).issubset(set(common)): | ||
|
r23848 | fetch = [] | ||
pullop.common = common | ||||
pullop.fetch = fetch | ||||
pullop.rheads = rheads | ||||
|
r20900 | |||
|
r43346 | |||
|
r20955 | def _pullbundle2(pullop): | ||
"""pull data using bundle2 | ||||
For now, the only supported data are changegroup.""" | ||||
|
r43347 | kwargs = {b'bundlecaps': caps20to10(pullop.repo, role=b'client')} | ||
|
r26471 | |||
|
r35779 | # make ui easier to access | ||
ui = pullop.repo.ui | ||||
|
r32257 | # At the moment we don't do stream clones over bundle2. If that is | ||
# implemented then here's where the check for that will go. | ||||
|
r35781 | streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0] | ||
|
r26471 | |||
|
r35779 | # declare pull perimeters | ||
|
r43347 | kwargs[b'common'] = pullop.common | ||
kwargs[b'heads'] = pullop.heads or pullop.rheads | ||||
|
r35779 | |||
|
r40527 | # check server supports narrow and then adding includepats and excludepats | ||
servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP) | ||||
if servernarrow and pullop.includepats: | ||||
|
r43347 | kwargs[b'includepats'] = pullop.includepats | ||
|
r40527 | if servernarrow and pullop.excludepats: | ||
|
r43347 | kwargs[b'excludepats'] = pullop.excludepats | ||
|
r40527 | |||
|
r35781 | if streaming: | ||
|
r43347 | kwargs[b'cg'] = False | ||
kwargs[b'stream'] = True | ||||
pullop.stepsdone.add(b'changegroup') | ||||
pullop.stepsdone.add(b'phases') | ||||
|
r35781 | |||
else: | ||||
|
r35780 | # pulling changegroup | ||
|
r43347 | pullop.stepsdone.add(b'changegroup') | ||
kwargs[b'cg'] = pullop.fetch | ||||
legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange') | ||||
hasbinaryphase = b'heads' in pullop.remotebundle2caps.get(b'phases', ()) | ||||
|
r43346 | if not legacyphase and hasbinaryphase: | ||
|
r43347 | kwargs[b'phases'] = True | ||
pullop.stepsdone.add(b'phases') | ||||
if b'listkeys' in pullop.remotebundle2caps: | ||||
if b'phases' not in pullop.stepsdone: | ||||
kwargs[b'listkeys'] = [b'phases'] | ||||
|
r35779 | |||
|
r35269 | bookmarksrequested = False | ||
|
r43347 | legacybookmark = b'bookmarks' in ui.configlist(b'devel', b'legacy.exchange') | ||
hasbinarybook = b'bookmarks' in pullop.remotebundle2caps | ||||
|
r35269 | |||
if pullop.remotebookmarks is not None: | ||||
|
r43347 | pullop.stepsdone.add(b'request-bookmarks') | ||
|
r35269 | |||
|
r43346 | if ( | ||
|
r43347 | b'request-bookmarks' not in pullop.stepsdone | ||
|
r35269 | and pullop.remotebookmarks is None | ||
|
r43346 | and not legacybookmark | ||
and hasbinarybook | ||||
): | ||||
|
r43347 | kwargs[b'bookmarks'] = True | ||
|
r35269 | bookmarksrequested = True | ||
|
r43347 | if b'listkeys' in pullop.remotebundle2caps: | ||
if b'request-bookmarks' not in pullop.stepsdone: | ||||
|
r25444 | # make sure to always includes bookmark data when migrating | ||
# `hg incoming --bundle` to using this function. | ||||
|
r43347 | pullop.stepsdone.add(b'request-bookmarks') | ||
kwargs.setdefault(b'listkeys', []).append(b'bookmarks') | ||||
|
r26690 | |||
# If this is a full pull / clone and the server supports the clone bundles | ||||
# feature, tell the server whether we attempted a clone bundle. The | ||||
# presence of this flag indicates the client supports clone bundles. This | ||||
# will enable the server to treat clients that support clone bundles | ||||
# differently from those that don't. | ||||
|
r43346 | if ( | ||
|
r43347 | pullop.remote.capable(b'clonebundles') | ||
|
r43346 | and pullop.heads is None | ||
|
r47771 | and list(pullop.common) == [pullop.repo.nullid] | ||
|
r43346 | ): | ||
|
r43347 | kwargs[b'cbattempted'] = pullop.clonebundleattempted | ||
|
r26690 | |||
|
r26471 | if streaming: | ||
|
r43347 | pullop.repo.ui.status(_(b'streaming all changes\n')) | ||
|
r26471 | elif not pullop.fetch: | ||
|
r43347 | pullop.repo.ui.status(_(b"no changes found\n")) | ||
|
r21258 | pullop.cgresult = 0 | ||
|
r20955 | else: | ||
|
r47771 | if pullop.heads is None and list(pullop.common) == [pullop.repo.nullid]: | ||
|
r43347 | pullop.repo.ui.status(_(b"requesting all changes\n")) | ||
|
r22953 | if obsolete.isenabled(pullop.repo, obsolete.exchangeopt): | ||
|
r26464 | remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps) | ||
|
r22354 | if obsolete.commonversion(remoteversions) is not None: | ||
|
r43347 | kwargs[b'obsmarkers'] = True | ||
pullop.stepsdone.add(b'obsmarkers') | ||||
|
r21159 | _pullbundle2extraprepare(pullop, kwargs) | ||
|
r37666 | |||
|
r47447 | remote_sidedata = bundle2.read_remote_wanted_sidedata(pullop.remote) | ||
if remote_sidedata: | ||||
kwargs[b'remote_sidedata'] = remote_sidedata | ||||
|
r37666 | with pullop.remote.commandexecutor() as e: | ||
args = dict(kwargs) | ||||
|
r43347 | args[b'source'] = b'pull' | ||
bundle = e.callcommand(b'getbundle', args).result() | ||||
|
r37666 | |||
try: | ||||
|
r43346 | op = bundle2.bundleoperation( | ||
|
r43347 | pullop.repo, pullop.gettransaction, source=b'pull' | ||
|
r43346 | ) | ||
|
r43347 | op.modes[b'bookmarks'] = b'records' | ||
|
r37666 | bundle2.processbundle(pullop.repo, bundle, op=op) | ||
except bundle2.AbortFromPart as exc: | ||||
|
r47207 | pullop.repo.ui.error(_(b'remote: abort: %s\n') % exc) | ||
|
r47739 | raise error.RemoteError(_(b'pull failed on remote'), hint=exc.hint) | ||
|
r37666 | except error.BundleValueError as exc: | ||
|
r47739 | raise error.RemoteError(_(b'missing support for %s') % exc) | ||
|
r21259 | |||
if pullop.fetch: | ||||
|
r33037 | pullop.cgresult = bundle2.combinechangegroupresults(op) | ||
|
r20955 | |||
|
r21658 | # processing phases change | ||
|
r43347 | for namespace, value in op.records[b'listkeys']: | ||
if namespace == b'phases': | ||||
|
r21658 | _pullapplyphases(pullop, value) | ||
|
r22656 | # processing bookmark update | ||
|
r35269 | if bookmarksrequested: | ||
books = {} | ||||
|
r43347 | for record in op.records[b'bookmarks']: | ||
books[record[b'bookmark']] = record[b"node"] | ||||
|
r35269 | pullop.remotebookmarks = books | ||
else: | ||||
|
r43347 | for namespace, value in op.records[b'listkeys']: | ||
if namespace == b'bookmarks': | ||||
|
r35269 | pullop.remotebookmarks = bookmod.unhexlifybookmarks(value) | ||
|
r25444 | |||
# bookmark data were either already there or pulled in the bundle | ||||
if pullop.remotebookmarks is not None: | ||||
_pullbookmarks(pullop) | ||||
|
r22656 | |||
|
r43346 | |||
|
r21159 | def _pullbundle2extraprepare(pullop, kwargs): | ||
"""hook function so that extensions can extend the getbundle call""" | ||||
|
r43346 | |||
|
r20489 | def _pullchangeset(pullop): | ||
"""pull changeset from unbundle into the local repo""" | ||||
# We delay the open of the transaction as late as possible so we | ||||
# don't open transaction for nothing or you break future useful | ||||
# rollback call | ||||
|
r43347 | if b'changegroup' in pullop.stepsdone: | ||
|
r22653 | return | ||
|
r43347 | pullop.stepsdone.add(b'changegroup') | ||
|
r20899 | if not pullop.fetch: | ||
|
r43347 | pullop.repo.ui.status(_(b"no changes found\n")) | ||
|
r23217 | pullop.cgresult = 0 | ||
return | ||||
|
r32930 | tr = pullop.gettransaction() | ||
|
r47771 | if pullop.heads is None and list(pullop.common) == [pullop.repo.nullid]: | ||
|
r43347 | pullop.repo.ui.status(_(b"requesting all changes\n")) | ||
elif pullop.heads is None and pullop.remote.capable(b'changegroupsubset'): | ||||
|
r20489 | # issue1320, avoid a race if remote changed after discovery | ||
pullop.heads = pullop.rheads | ||||
|
r43347 | if pullop.remote.capable(b'getbundle'): | ||
|
r20489 | # TODO: get bundlecaps from remote | ||
|
r43346 | cg = pullop.remote.getbundle( | ||
|
r43347 | b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads | ||
|
r43346 | ) | ||
|
r20489 | elif pullop.heads is None: | ||
|
r37653 | with pullop.remote.commandexecutor() as e: | ||
|
r43346 | cg = e.callcommand( | ||
|
r46554 | b'changegroup', | ||
{ | ||||
b'nodes': pullop.fetch, | ||||
b'source': b'pull', | ||||
}, | ||||
|
r43346 | ).result() | ||
|
r37653 | |||
|
r43347 | elif not pullop.remote.capable(b'changegroupsubset'): | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b"partial pull cannot be done because " | ||
b"other repository doesn't support " | ||||
b"changegroupsubset." | ||||
|
r43346 | ) | ||
) | ||||
|
r20489 | else: | ||
|
r37653 | with pullop.remote.commandexecutor() as e: | ||
|
r43346 | cg = e.callcommand( | ||
|
r43347 | b'changegroupsubset', | ||
|
r43346 | { | ||
|
r43347 | b'bases': pullop.fetch, | ||
b'heads': pullop.heads, | ||||
b'source': b'pull', | ||||
|
r43346 | }, | ||
).result() | ||||
bundleop = bundle2.applybundle( | ||||
|
r43347 | pullop.repo, cg, tr, b'pull', pullop.remote.url() | ||
|
r43346 | ) | ||
|
r33040 | pullop.cgresult = bundle2.combinechangegroupresults(bundleop) | ||
|
r20489 | |||
|
r43346 | |||
|
r20486 | def _pullphase(pullop): | ||
# Get remote phases data from remote | ||||
|
r43347 | if b'phases' in pullop.stepsdone: | ||
|
r22653 | return | ||
|
r43347 | remotephases = listkeys(pullop.remote, b'phases') | ||
|
r21654 | _pullapplyphases(pullop, remotephases) | ||
|
r43346 | |||
|
r21654 | def _pullapplyphases(pullop, remotephases): | ||
"""apply phase movement from observed remote state""" | ||||
|
r43347 | if b'phases' in pullop.stepsdone: | ||
|
r22937 | return | ||
|
r43347 | pullop.stepsdone.add(b'phases') | ||
publishing = bool(remotephases.get(b'publishing', False)) | ||||
|
r20486 | if remotephases and not publishing: | ||
|
r30332 | # remote is new and non-publishing | ||
|
r43346 | pheads, _dr = phases.analyzeremotephases( | ||
pullop.repo, pullop.pulledsubset, remotephases | ||||
) | ||||
|
r22068 | dheads = pullop.pulledsubset | ||
|
r20486 | else: | ||
# Remote is old or publishing all common changesets | ||||
# should be seen as public | ||||
|
r22068 | pheads = pullop.pulledsubset | ||
dheads = [] | ||||
unfi = pullop.repo.unfiltered() | ||||
phase = unfi._phasecache.phase | ||||
r43963 | rev = unfi.changelog.index.get_rev | |||
|
r22068 | public = phases.public | ||
draft = phases.draft | ||||
# exclude changesets already public locally and update the others | ||||
pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public] | ||||
if pheads: | ||||
|
r22069 | tr = pullop.gettransaction() | ||
phases.advanceboundary(pullop.repo, tr, public, pheads) | ||||
|
r22068 | |||
# exclude changesets already draft locally and update the others | ||||
dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft] | ||||
if dheads: | ||||
|
r22069 | tr = pullop.gettransaction() | ||
phases.advanceboundary(pullop.repo, tr, draft, dheads) | ||||
|
r20486 | |||
|
r43346 | |||
|
r22654 | def _pullbookmarks(pullop): | ||
"""process the remote bookmark information to update the local one""" | ||||
|
r43347 | if b'bookmarks' in pullop.stepsdone: | ||
|
r22654 | return | ||
|
r43347 | pullop.stepsdone.add(b'bookmarks') | ||
|
r22654 | repo = pullop.repo | ||
remotebookmarks = pullop.remotebookmarks | ||||
|
r43346 | bookmod.updatefromremote( | ||
repo.ui, | ||||
repo, | ||||
remotebookmarks, | ||||
pullop.remote.url(), | ||||
pullop.gettransaction, | ||||
explicit=pullop.explicitbookmarks, | ||||
) | ||||
|
r22654 | |||
|
r20478 | def _pullobsolete(pullop): | ||
|
r20476 | """utility function to pull obsolete markers from a remote | ||
The `gettransaction` is function that return the pull transaction, creating | ||||
one if necessary. We return the transaction to inform the calling code that | ||||
a new transaction have been created (when applicable). | ||||
Exists mostly to allow overriding for experimentation purpose""" | ||||
|
r43347 | if b'obsmarkers' in pullop.stepsdone: | ||
|
r22653 | return | ||
|
r43347 | pullop.stepsdone.add(b'obsmarkers') | ||
|
r20476 | tr = None | ||
|
r22953 | if obsolete.isenabled(pullop.repo, obsolete.exchangeopt): | ||
|
r43347 | pullop.repo.ui.debug(b'fetching remote obsolete markers\n') | ||
remoteobs = listkeys(pullop.remote, b'obsolete') | ||||
if b'dump0' in remoteobs: | ||||
|
r20478 | tr = pullop.gettransaction() | ||
|
r27558 | markers = [] | ||
|
r20476 | for key in sorted(remoteobs, reverse=True): | ||
|
r43347 | if key.startswith(b'dump'): | ||
|
r32200 | data = util.b85decode(remoteobs[key]) | ||
|
r27558 | version, newmarks = obsolete._readmarkers(data) | ||
markers += newmarks | ||||
if markers: | ||||
pullop.repo.obsstore.add(tr, markers) | ||||
|
r20478 | pullop.repo.invalidatevolatilesets() | ||
|
r20476 | return tr | ||
|
r43346 | |||
|
r38826 | def applynarrowacl(repo, kwargs): | ||
"""Apply narrow fetch access control. | ||||
This massages the named arguments for getbundle wire protocol commands | ||||
so requested data is filtered through access control rules. | ||||
""" | ||||
ui = repo.ui | ||||
# TODO this assumes existence of HTTP and is a layering violation. | ||||
|
r43347 | username = ui.shortuser(ui.environ.get(b'REMOTE_USER') or ui.username()) | ||
|
r38826 | user_includes = ui.configlist( | ||
|
r43346 | _NARROWACL_SECTION, | ||
|
r43347 | username + b'.includes', | ||
ui.configlist(_NARROWACL_SECTION, b'default.includes'), | ||||
|
r43346 | ) | ||
|
r38826 | user_excludes = ui.configlist( | ||
|
r43346 | _NARROWACL_SECTION, | ||
|
r43347 | username + b'.excludes', | ||
ui.configlist(_NARROWACL_SECTION, b'default.excludes'), | ||||
|
r43346 | ) | ||
|
r38826 | if not user_includes: | ||
|
r43346 | raise error.Abort( | ||
|
r44208 | _(b"%s configuration for user %s is empty") | ||
% (_NARROWACL_SECTION, username) | ||||
|
r43346 | ) | ||
|
r38826 | |||
user_includes = [ | ||||
|
r43347 | b'path:.' if p == b'*' else b'path:' + p for p in user_includes | ||
|
r43346 | ] | ||
|
r38826 | user_excludes = [ | ||
|
r43347 | b'path:.' if p == b'*' else b'path:' + p for p in user_excludes | ||
|
r43346 | ] | ||
|
r38826 | |||
|
r43906 | req_includes = set(kwargs.get('includepats', [])) | ||
req_excludes = set(kwargs.get('excludepats', [])) | ||||
|
r38826 | |||
req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns( | ||||
|
r43346 | req_includes, req_excludes, user_includes, user_excludes | ||
) | ||||
|
r38826 | |||
if invalid_includes: | ||||
raise error.Abort( | ||||
|
r44208 | _(b"The following includes are not accessible for %s: %s") | ||
|
r44273 | % (username, stringutil.pprint(invalid_includes)) | ||
|
r43346 | ) | ||
|
r38826 | |||
new_args = {} | ||||
new_args.update(kwargs) | ||||
|
r43906 | new_args['narrow'] = True | ||
new_args['narrow_acl'] = True | ||||
new_args['includepats'] = req_includes | ||||
|
r38826 | if req_excludes: | ||
|
r43906 | new_args['excludepats'] = req_excludes | ||
|
r38843 | |||
|
r38826 | return new_args | ||
|
r43346 | |||
|
r38827 | def _computeellipsis(repo, common, heads, known, match, depth=None): | ||
"""Compute the shape of a narrowed DAG. | ||||
Args: | ||||
repo: The repository we're transferring. | ||||
common: The roots of the DAG range we're transferring. | ||||
May be just [nullid], which means all ancestors of heads. | ||||
heads: The heads of the DAG range we're transferring. | ||||
match: The narrowmatcher that allows us to identify relevant changes. | ||||
depth: If not None, only consider nodes to be full nodes if they are at | ||||
most depth changesets away from one of heads. | ||||
Returns: | ||||
A tuple of (visitnodes, relevant_nodes, ellipsisroots) where: | ||||
visitnodes: The list of nodes (either full or ellipsis) which | ||||
need to be sent to the client. | ||||
relevant_nodes: The set of changelog nodes which change a file inside | ||||
the narrowspec. The client needs these as non-ellipsis nodes. | ||||
ellipsisroots: A dict of {rev: parents} that is used in | ||||
narrowchangegroup to produce ellipsis nodes with the | ||||
correct parents. | ||||
""" | ||||
cl = repo.changelog | ||||
mfl = repo.manifestlog | ||||
|
r39194 | clrev = cl.rev | ||
commonrevs = {clrev(n) for n in common} | {nullrev} | ||||
headsrevs = {clrev(n) for n in heads} | ||||
|
r38827 | if depth: | ||
revdepth = {h: 0 for h in headsrevs} | ||||
ellipsisheads = collections.defaultdict(set) | ||||
ellipsisroots = collections.defaultdict(set) | ||||
def addroot(head, curchange): | ||||
"""Add a root to an ellipsis head, splitting heads with 3 roots.""" | ||||
ellipsisroots[head].add(curchange) | ||||
# Recursively split ellipsis heads with 3 roots by finding the | ||||
# roots' youngest common descendant which is an elided merge commit. | ||||
# That descendant takes 2 of the 3 roots as its own, and becomes a | ||||
# root of the head. | ||||
while len(ellipsisroots[head]) > 2: | ||||
child, roots = splithead(head) | ||||
splitroots(head, child, roots) | ||||
head = child # Recurse in case we just added a 3rd root | ||||
def splitroots(head, child, roots): | ||||
ellipsisroots[head].difference_update(roots) | ||||
ellipsisroots[head].add(child) | ||||
ellipsisroots[child].update(roots) | ||||
ellipsisroots[child].discard(child) | ||||
def splithead(head): | ||||
r1, r2, r3 = sorted(ellipsisroots[head]) | ||||
for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)): | ||||
|
r43346 | mid = repo.revs( | ||
|
r43347 | b'sort(merge() & %d::%d & %d::%d, -rev)', nr1, head, nr2, head | ||
|
r43346 | ) | ||
|
r38827 | for j in mid: | ||
if j == nr2: | ||||
return nr2, (nr1, nr2) | ||||
if j not in ellipsisroots or len(ellipsisroots[j]) < 2: | ||||
return j, (nr1, nr2) | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _( | ||
b'Failed to split up ellipsis node! head: %d, ' | ||||
b'roots: %d %d %d' | ||||
) | ||||
|
r43346 | % (head, r1, r2, r3) | ||
) | ||||
|
r38827 | |||
missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs)) | ||||
visit = reversed(missing) | ||||
relevant_nodes = set() | ||||
visitnodes = [cl.node(m) for m in missing] | ||||
required = set(headsrevs) | known | ||||
for rev in visit: | ||||
clrev = cl.changelogrevision(rev) | ||||
|
r39194 | ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev] | ||
|
r38827 | if depth is not None: | ||
curdepth = revdepth[rev] | ||||
for p in ps: | ||||
revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1)) | ||||
needed = False | ||||
shallow_enough = depth is None or revdepth[rev] <= depth | ||||
if shallow_enough: | ||||
curmf = mfl[clrev.manifest].read() | ||||
if ps: | ||||
# We choose to not trust the changed files list in | ||||
# changesets because it's not always correct. TODO: could | ||||
# we trust it for the non-merge case? | ||||
p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read() | ||||
needed = bool(curmf.diff(p1mf, match)) | ||||
if not needed and len(ps) > 1: | ||||
# For merge changes, the list of changed files is not | ||||
# helpful, since we need to emit the merge if a file | ||||
# in the narrow spec has changed on either side of the | ||||
# merge. As a result, we do a manifest diff to check. | ||||
p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read() | ||||
needed = bool(curmf.diff(p2mf, match)) | ||||
else: | ||||
# For a root node, we need to include the node if any | ||||
# files in the node match the narrowspec. | ||||
needed = any(curmf.walk(match)) | ||||
if needed: | ||||
for head in ellipsisheads[rev]: | ||||
addroot(head, rev) | ||||
for p in ps: | ||||
required.add(p) | ||||
relevant_nodes.add(cl.node(rev)) | ||||
else: | ||||
if not ps: | ||||
ps = [nullrev] | ||||
if rev in required: | ||||
for head in ellipsisheads[rev]: | ||||
addroot(head, rev) | ||||
for p in ps: | ||||
ellipsisheads[p].add(rev) | ||||
else: | ||||
for p in ps: | ||||
ellipsisheads[p] |= ellipsisheads[rev] | ||||
# add common changesets as roots of their reachable ellipsis heads | ||||
for c in commonrevs: | ||||
for head in ellipsisheads[c]: | ||||
addroot(head, c) | ||||
return visitnodes, relevant_nodes, ellipsisroots | ||||
|
r43346 | |||
|
r35801 | def caps20to10(repo, role): | ||
|
r21645 | """return a set with appropriate options to use bundle20 during getbundle""" | ||
|
r43347 | caps = {b'HG20'} | ||
|
r35801 | capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role)) | ||
|
r43347 | caps.add(b'bundle2=' + urlreq.quote(capsblob)) | ||
|
r21645 | return caps | ||
|
r43346 | |||
|
r22542 | # List of names of steps to perform for a bundle2 for getbundle, order matters. | ||
getbundle2partsorder = [] | ||||
# Mapping between step name and function | ||||
# | ||||
# This exists to help extensions wrap steps if necessary | ||||
getbundle2partsmapping = {} | ||||
|
r43346 | |||
|
r24732 | def getbundle2partsgenerator(stepname, idx=None): | ||
|
r22542 | """decorator for function generating bundle2 part for getbundle | ||
The function is added to the step -> function mapping and appended to the | ||||
list of steps. Beware that decorated functions will be added in order | ||||
(this may matter). | ||||
You can only use this decorator for new steps, if you want to wrap a step | ||||
from an extension, attack the getbundle2partsmapping dictionary directly.""" | ||||
|
r43346 | |||
|
r22542 | def dec(func): | ||
assert stepname not in getbundle2partsmapping | ||||
getbundle2partsmapping[stepname] = func | ||||
|
r24732 | if idx is None: | ||
getbundle2partsorder.append(stepname) | ||||
else: | ||||
getbundle2partsorder.insert(idx, stepname) | ||||
|
r22542 | return func | ||
|
r43346 | |||
|
r22542 | return dec | ||
|
r43346 | |||
|
r27244 | def bundle2requested(bundlecaps): | ||
if bundlecaps is not None: | ||||
|
r43347 | return any(cap.startswith(b'HG2') for cap in bundlecaps) | ||
|
r27244 | return False | ||
|
r43346 | |||
def getbundlechunks( | ||||
|
r47445 | repo, | ||
source, | ||||
heads=None, | ||||
common=None, | ||||
bundlecaps=None, | ||||
remote_sidedata=None, | ||||
**kwargs | ||||
|
r43346 | ): | ||
|
r30187 | """Return chunks constituting a bundle's raw data. | ||
|
r20954 | |||
|
r24686 | Could be a bundle HG10 or a bundle HG20 depending on bundlecaps | ||
|
r30187 | passed. | ||
|
r20954 | |||
|
r35803 | Returns a 2-tuple of a dict with metadata about the generated bundle | ||
and an iterator over raw chunks (of varying sizes). | ||||
|
r20954 | """ | ||
|
r33016 | kwargs = pycompat.byteskwargs(kwargs) | ||
|
r35803 | info = {} | ||
|
r27244 | usebundle2 = bundle2requested(bundlecaps) | ||
|
r22542 | # bundle10 case | ||
|
r24649 | if not usebundle2: | ||
|
r43347 | if bundlecaps and not kwargs.get(b'cg', True): | ||
raise ValueError( | ||||
_(b'request for bundle10 must include changegroup') | ||||
) | ||||
|
r22542 | |||
|
r21656 | if kwargs: | ||
|
r43346 | raise ValueError( | ||
|
r43347 | _(b'unsupported getbundle arguments: %s') | ||
% b', '.join(sorted(kwargs.keys())) | ||||
|
r43346 | ) | ||
|
r29808 | outgoing = _computeoutgoing(repo, heads, common) | ||
|
r43347 | info[b'bundleversion'] = 1 | ||
|
r43346 | return ( | ||
info, | ||||
changegroup.makestream( | ||||
|
r47445 | repo, | ||
outgoing, | ||||
b'01', | ||||
source, | ||||
bundlecaps=bundlecaps, | ||||
remote_sidedata=remote_sidedata, | ||||
|
r43346 | ), | ||
) | ||||
|
r22542 | |||
# bundle20 case | ||||
|
r43347 | info[b'bundleversion'] = 2 | ||
|
r21143 | b2caps = {} | ||
for bcaps in bundlecaps: | ||||
|
r43347 | if bcaps.startswith(b'bundle2='): | ||
blob = urlreq.unquote(bcaps[len(b'bundle2=') :]) | ||||
|
r21143 | b2caps.update(bundle2.decodecaps(blob)) | ||
bundler = bundle2.bundle20(repo.ui, b2caps) | ||||
|
r22542 | |||
|
r43347 | kwargs[b'heads'] = heads | ||
kwargs[b'common'] = common | ||||
|
r23218 | |||
|
r22542 | for name in getbundle2partsorder: | ||
func = getbundle2partsmapping[name] | ||||
|
r43346 | func( | ||
bundler, | ||||
repo, | ||||
source, | ||||
bundlecaps=bundlecaps, | ||||
b2caps=b2caps, | ||||
|
r47445 | remote_sidedata=remote_sidedata, | ||
|
r43346 | **pycompat.strkwargs(kwargs) | ||
) | ||||
|
r22542 | |||
|
r43347 | info[b'prefercompressed'] = bundler.prefercompressed | ||
|
r35805 | |||
|
r35803 | return info, bundler.getchunks() | ||
|
r22542 | |||
|
r43346 | |||
|
r43347 | @getbundle2partsgenerator(b'stream2') | ||
|
r37184 | def _getbundlestream2(bundler, repo, *args, **kwargs): | ||
return bundle2.addpartbundlestream2(bundler, repo, **kwargs) | ||||
|
r35777 | |||
|
r43346 | |||
|
r43347 | @getbundle2partsgenerator(b'changegroup') | ||
|
r43346 | def _getbundlechangegrouppart( | ||
bundler, | ||||
repo, | ||||
source, | ||||
bundlecaps=None, | ||||
b2caps=None, | ||||
heads=None, | ||||
common=None, | ||||
|
r47445 | remote_sidedata=None, | ||
|
r43346 | **kwargs | ||
): | ||||
|
r22542 | """add a changegroup part to the requested bundle""" | ||
|
r44209 | if not kwargs.get('cg', True) or not b2caps: | ||
|
r38828 | return | ||
|
r43347 | version = b'01' | ||
cgversions = b2caps.get(b'changegroup') | ||||
|
r38828 | if cgversions: # 3.1 and 3.2 ship with an empty value | ||
|
r43346 | cgversions = [ | ||
v | ||||
for v in cgversions | ||||
if v in changegroup.supportedoutgoingversions(repo) | ||||
] | ||||
|
r38828 | if not cgversions: | ||
|
r43347 | raise error.Abort(_(b'no common changegroup version')) | ||
|
r38828 | version = max(cgversions) | ||
|
r22542 | |||
|
r38828 | outgoing = _computeoutgoing(repo, heads, common) | ||
if not outgoing.missing: | ||||
return | ||||
|
r43906 | if kwargs.get('narrow', False): | ||
include = sorted(filter(bool, kwargs.get('includepats', []))) | ||||
exclude = sorted(filter(bool, kwargs.get('excludepats', []))) | ||||
|
r40380 | matcher = narrowspec.match(repo.root, include=include, exclude=exclude) | ||
|
r38844 | else: | ||
|
r40380 | matcher = None | ||
|
r38844 | |||
|
r43346 | cgstream = changegroup.makestream( | ||
|
r47445 | repo, | ||
outgoing, | ||||
version, | ||||
source, | ||||
bundlecaps=bundlecaps, | ||||
matcher=matcher, | ||||
remote_sidedata=remote_sidedata, | ||||
|
r43346 | ) | ||
|
r38828 | |||
|
r43347 | part = bundler.newpart(b'changegroup', data=cgstream) | ||
|
r38828 | if cgversions: | ||
|
r43347 | part.addparam(b'version', version) | ||
part.addparam(b'nbchanges', b'%d' % len(outgoing.missing), mandatory=False) | ||||
|
r46129 | if scmutil.istreemanifest(repo): | ||
|
r43347 | part.addparam(b'treemanifest', b'1') | ||
|
r22542 | |||
r48000 | if repository.REPO_FEATURE_SIDE_DATA in repo.features: | |||
r43401 | part.addparam(b'exp-sidedata', b'1') | |||
|
r47447 | sidedata = bundle2.format_remote_wanted_sidedata(repo) | ||
part.addparam(b'exp-wanted-sidedata', sidedata) | ||||
r43401 | ||||
|
r43346 | if ( | ||
|
r43906 | kwargs.get('narrow', False) | ||
and kwargs.get('narrow_acl', False) | ||||
|
r43346 | and (include or exclude) | ||
): | ||||
|
r42393 | # this is mandatory because otherwise ACL clients won't work | ||
|
r43347 | narrowspecpart = bundler.newpart(b'Narrow:responsespec') | ||
narrowspecpart.data = b'%s\0%s' % ( | ||||
b'\n'.join(include), | ||||
b'\n'.join(exclude), | ||||
|
r43346 | ) | ||
|
r38844 | |||
|
r43347 | @getbundle2partsgenerator(b'bookmarks') | ||
|
r43346 | def _getbundlebookmarkpart( | ||
bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs | ||||
): | ||||
|
r35268 | """add a bookmark part to the requested bundle""" | ||
|
r43906 | if not kwargs.get('bookmarks', False): | ||
|
r35268 | return | ||
|
r44209 | if not b2caps or b'bookmarks' not in b2caps: | ||
|
r43347 | raise error.Abort(_(b'no common bookmarks exchange method')) | ||
|
r43346 | books = bookmod.listbinbookmarks(repo) | ||
|
r47538 | data = bookmod.binaryencode(repo, books) | ||
|
r35268 | if data: | ||
|
r43347 | bundler.newpart(b'bookmarks', data=data) | ||
@getbundle2partsgenerator(b'listkeys') | ||||
|
r43346 | def _getbundlelistkeysparts( | ||
bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs | ||||
): | ||||
|
r22542 | """add parts containing listkeys namespaces to the requested bundle""" | ||
|
r43906 | listkeys = kwargs.get('listkeys', ()) | ||
|
r21657 | for namespace in listkeys: | ||
|
r43347 | part = bundler.newpart(b'listkeys') | ||
part.addparam(b'namespace', namespace) | ||||
|
r21657 | keys = repo.listkeys(namespace).items() | ||
part.data = pushkey.encodekeys(keys) | ||||
|
r20967 | |||
|
r43346 | |||
|
r43347 | @getbundle2partsgenerator(b'obsmarkers') | ||
|
r43346 | def _getbundleobsmarkerpart( | ||
bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs | ||||
): | ||||
|
r22541 | """add an obsolescence markers part to the requested bundle""" | ||
|
r43906 | if kwargs.get('obsmarkers', False): | ||
|
r22353 | if heads is None: | ||
heads = repo.heads() | ||||
|
r43347 | subset = [c.node() for c in repo.set(b'::%ln', heads)] | ||
|
r22353 | markers = repo.obsstore.relevantmarkers(subset) | ||
|
r43858 | markers = obsutil.sortedmarkers(markers) | ||
r32515 | bundle2.buildobsmarkerspart(bundler, markers) | |||
|
r22353 | |||
|
r43346 | |||
|
r43347 | @getbundle2partsgenerator(b'phases') | ||
|
r43346 | def _getbundlephasespart( | ||
bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs | ||||
): | ||||
|
r34323 | """add phase heads part to the requested bundle""" | ||
|
r43906 | if kwargs.get('phases', False): | ||
|
r44234 | if not b2caps or b'heads' not in b2caps.get(b'phases'): | ||
|
r43347 | raise error.Abort(_(b'no common phases exchange method')) | ||
|
r34323 | if heads is None: | ||
heads = repo.heads() | ||||
headsbyphase = collections.defaultdict(set) | ||||
if repo.publishing(): | ||||
headsbyphase[phases.public] = heads | ||||
else: | ||||
# find the appropriate heads to move | ||||
phase = repo._phasecache.phase | ||||
node = repo.changelog.node | ||||
rev = repo.changelog.rev | ||||
for h in heads: | ||||
headsbyphase[phase(repo, rev(h))].add(h) | ||||
seenphases = list(headsbyphase.keys()) | ||||
# We do not handle anything but public and draft phase for now) | ||||
if seenphases: | ||||
assert max(seenphases) <= phases.draft | ||||
# if client is pulling non-public changesets, we need to find | ||||
# intermediate public heads. | ||||
draftheads = headsbyphase.get(phases.draft, set()) | ||||
if draftheads: | ||||
publicheads = headsbyphase.get(phases.public, set()) | ||||
|
r43347 | revset = b'heads(only(%ln, %ln) and public())' | ||
|
r34323 | extraheads = repo.revs(revset, draftheads, publicheads) | ||
for r in extraheads: | ||||
headsbyphase[phases.public].add(node(r)) | ||||
# transform data in a format used by the encoding function | ||||
|
r45677 | phasemapping = { | ||
phase: sorted(headsbyphase[phase]) for phase in phases.allphases | ||||
} | ||||
|
r34323 | |||
# generate the actual part | ||||
phasedata = phases.binaryencode(phasemapping) | ||||
|
r43347 | bundler.newpart(b'phase-heads', data=phasedata) | ||
@getbundle2partsgenerator(b'hgtagsfnodes') | ||||
|
r43346 | def _getbundletagsfnodes( | ||
bundler, | ||||
repo, | ||||
source, | ||||
bundlecaps=None, | ||||
b2caps=None, | ||||
heads=None, | ||||
common=None, | ||||
**kwargs | ||||
): | ||||
|
r25402 | """Transfer the .hgtags filenodes mapping. | ||
Only values for heads in this bundle will be transferred. | ||||
The part data consists of pairs of 20 byte changeset node and .hgtags | ||||
filenodes raw values. | ||||
""" | ||||
# Don't send unless: | ||||
# - changeset are being exchanged, | ||||
# - the client supports it. | ||||
|
r44209 | if not b2caps or not (kwargs.get('cg', True) and b'hgtagsfnodes' in b2caps): | ||
|
r25402 | return | ||
|
r29808 | outgoing = _computeoutgoing(repo, heads, common) | ||
r32217 | bundle2.addparttagsfnodescache(repo, bundler, outgoing) | |||
|
r25402 | |||
|
r43346 | |||
|
r43347 | @getbundle2partsgenerator(b'cache:rev-branch-cache') | ||
|
r43346 | def _getbundlerevbranchcache( | ||
bundler, | ||||
repo, | ||||
source, | ||||
bundlecaps=None, | ||||
b2caps=None, | ||||
heads=None, | ||||
common=None, | ||||
**kwargs | ||||
): | ||||
|
r36984 | """Transfer the rev-branch-cache mapping | ||
The payload is a series of data related to each branch | ||||
1) branch name length | ||||
2) number of open heads | ||||
3) number of closed heads | ||||
4) open heads nodes | ||||
5) closed heads nodes | ||||
""" | ||||
# Don't send unless: | ||||
# - changeset are being exchanged, | ||||
# - the client supports it. | ||||
|
r38825 | # - narrow bundle isn't in play (not currently compatible). | ||
|
r43346 | if ( | ||
|
r43906 | not kwargs.get('cg', True) | ||
|
r44209 | or not b2caps | ||
|
r43347 | or b'rev-branch-cache' not in b2caps | ||
|
r43906 | or kwargs.get('narrow', False) | ||
|
r43346 | or repo.ui.has_section(_NARROWACL_SECTION) | ||
): | ||||
|
r36984 | return | ||
|
r38825 | |||
|
r36984 | outgoing = _computeoutgoing(repo, heads, common) | ||
bundle2.addpartrevbranchcache(repo, bundler, outgoing) | ||||
|
r43346 | |||
|
r20967 | def check_heads(repo, their_heads, context): | ||
"""check if the heads of a repo have been modified | ||||
Used by peer for unbundling. | ||||
""" | ||||
heads = repo.heads() | ||||
|
r44517 | heads_hash = hashutil.sha1(b''.join(sorted(heads))).digest() | ||
|
r43346 | if not ( | ||
|
r43347 | their_heads == [b'force'] | ||
|
r43346 | or their_heads == heads | ||
|
r43347 | or their_heads == [b'hashed', heads_hash] | ||
|
r43346 | ): | ||
|
r20967 | # someone else committed/pushed/unbundled while we | ||
# were transferring data | ||||
|
r43346 | raise error.PushRaced( | ||
|
r43387 | b'repository changed while %s - please try again' % context | ||
|
r43346 | ) | ||
|
r20968 | |||
def unbundle(repo, cg, heads, source, url): | ||||
"""Apply a bundle to a repo. | ||||
this function makes sure the repo is locked during the application and have | ||||
|
r21024 | mechanism to check that no push race occurred between the creation of the | ||
|
r20968 | bundle and its application. | ||
If the push was raced as PushRaced exception is raised.""" | ||||
r = 0 | ||||
|
r21061 | # need a transaction when processing a bundle2 stream | ||
|
r26566 | # [wlock, lock, tr] - needs to be an array so nested functions can modify it | ||
lockandtr = [None, None, None] | ||||
|
r24847 | recordout = None | ||
|
r24878 | # quick fix for output mismatch with bundle2 in 3.4 | ||
|
r43347 | captureoutput = repo.ui.configbool( | ||
b'experimental', b'bundle2-output-capture' | ||||
) | ||||
if url.startswith(b'remote:http:') or url.startswith(b'remote:https:'): | ||||
|
r24878 | captureoutput = True | ||
|
r20968 | try: | ||
|
r30868 | # note: outside bundle1, 'heads' is expected to be empty and this | ||
# 'check_heads' call wil be a no-op | ||||
|
r43347 | check_heads(repo, heads, b'uploading changes') | ||
|
r20968 | # push can proceed | ||
|
r32891 | if not isinstance(cg, bundle2.unbundle20): | ||
|
r30870 | # legacy case: bundle1 (changegroup 01) | ||
r47669 | txnname = b"\n".join([source, urlutil.hidepassword(url)]) | |||
|
r32930 | with repo.lock(), repo.transaction(txnname) as tr: | ||
|
r33043 | op = bundle2.applybundle(repo, cg, tr, source, url) | ||
|
r33040 | r = bundle2.combinechangegroupresults(op) | ||
|
r30870 | else: | ||
|
r24795 | r = None | ||
|
r21187 | try: | ||
|
r43346 | |||
|
r26566 | def gettransaction(): | ||
if not lockandtr[2]: | ||||
|
r42513 | if not bookmod.bookmarksinstore(repo): | ||
lockandtr[0] = repo.wlock() | ||||
|
r26566 | lockandtr[1] = repo.lock() | ||
lockandtr[2] = repo.transaction(source) | ||||
|
r43347 | lockandtr[2].hookargs[b'source'] = source | ||
lockandtr[2].hookargs[b'url'] = url | ||||
lockandtr[2].hookargs[b'bundle2'] = b'1' | ||||
|
r26566 | return lockandtr[2] | ||
# Do greedy locking by default until we're satisfied with lazy | ||||
# locking. | ||||
|
r43347 | if not repo.ui.configbool( | ||
b'experimental', b'bundle2lazylocking' | ||||
): | ||||
|
r26566 | gettransaction() | ||
|
r43346 | op = bundle2.bundleoperation( | ||
repo, | ||||
gettransaction, | ||||
captureoutput=captureoutput, | ||||
|
r43347 | source=b'push', | ||
|
r43346 | ) | ||
|
r24851 | try: | ||
|
r25896 | op = bundle2.processbundle(repo, cg, op=op) | ||
|
r24851 | finally: | ||
r = op.reply | ||||
|
r24878 | if captureoutput and r is not None: | ||
|
r24851 | repo.ui.pushbuffer(error=True, subproc=True) | ||
|
r43346 | |||
|
r24851 | def recordout(output): | ||
|
r43347 | r.newpart(b'output', data=output, mandatory=False) | ||
|
r43346 | |||
|
r26566 | if lockandtr[2] is not None: | ||
lockandtr[2].close() | ||||
|
r25660 | except BaseException as exc: | ||
|
r21187 | exc.duringunbundle2 = True | ||
|
r24878 | if captureoutput and r is not None: | ||
|
r24847 | parts = exc._bundle2salvagedoutput = r.salvageoutput() | ||
|
r43346 | |||
|
r24847 | def recordout(output): | ||
|
r43346 | part = bundle2.bundlepart( | ||
|
r43347 | b'output', data=output, mandatory=False | ||
|
r43346 | ) | ||
|
r24847 | parts.append(part) | ||
|
r43346 | |||
|
r21187 | raise | ||
|
r20968 | finally: | ||
|
r26566 | lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0]) | ||
|
r24847 | if recordout is not None: | ||
recordout(repo.ui.popbuffer()) | ||||
|
r20968 | return r | ||
|
r26623 | |||
|
r43346 | |||
|
r26623 | def _maybeapplyclonebundle(pullop): | ||
"""Apply a clone bundle from a remote, if possible.""" | ||||
repo = pullop.repo | ||||
remote = pullop.remote | ||||
|
r43347 | if not repo.ui.configbool(b'ui', b'clonebundles'): | ||
|
r26623 | return | ||
|
r26855 | # Only run if local repo is empty. | ||
if len(repo): | ||||
return | ||||
|
r26623 | if pullop.heads: | ||
return | ||||
|
r43347 | if not remote.capable(b'clonebundles'): | ||
|
r26623 | return | ||
|
r37667 | with remote.commandexecutor() as e: | ||
|
r43347 | res = e.callcommand(b'clonebundles', {}).result() | ||
|
r26689 | |||
# If we call the wire protocol command, that's good enough to record the | ||||
# attempt. | ||||
pullop.clonebundleattempted = True | ||||
r46369 | entries = bundlecaches.parseclonebundlesmanifest(repo, res) | |||
|
r26623 | if not entries: | ||
|
r43346 | repo.ui.note( | ||
_( | ||||
|
r43347 | b'no clone bundles available on remote; ' | ||
b'falling back to regular clone\n' | ||||
|
r43346 | ) | ||
) | ||||
|
r26623 | return | ||
r46369 | entries = bundlecaches.filterclonebundleentries( | |||
|
r43346 | repo, entries, streamclonerequested=pullop.streamclonerequested | ||
) | ||||
|
r34360 | |||
|
r26644 | if not entries: | ||
# There is a thundering herd concern here. However, if a server | ||||
# operator doesn't advertise bundles appropriate for its clients, | ||||
# they deserve what's coming. Furthermore, from a client's | ||||
# perspective, no automatic fallback would mean not being able to | ||||
# clone! | ||||
|
r43346 | repo.ui.warn( | ||
_( | ||||
|
r43347 | b'no compatible clone bundles available on server; ' | ||
b'falling back to regular clone\n' | ||||
|
r43346 | ) | ||
) | ||||
repo.ui.warn( | ||||
|
r43387 | _(b'(you may want to report this to the server operator)\n') | ||
|
r43346 | ) | ||
|
r26644 | return | ||
r46369 | entries = bundlecaches.sortclonebundleentries(repo.ui, entries) | |||
|
r26644 | |||
|
r43347 | url = entries[0][b'URL'] | ||
repo.ui.status(_(b'applying clone bundle from %s\n') % url) | ||||
|
r26623 | if trypullbundlefromurl(repo.ui, repo, url): | ||
|
r43347 | repo.ui.status(_(b'finished applying clone bundle\n')) | ||
|
r26623 | # Bundle failed. | ||
# | ||||
# We abort by default to avoid the thundering herd of | ||||
# clients flooding a server that was expecting expensive | ||||
# clone load to be offloaded. | ||||
|
r43347 | elif repo.ui.configbool(b'ui', b'clonebundlefallback'): | ||
repo.ui.warn(_(b'falling back to normal clone\n')) | ||||
|
r26623 | else: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'error applying bundle'), | ||
|
r43346 | hint=_( | ||
|
r43347 | b'if this error persists, consider contacting ' | ||
b'the server operator or disable clone ' | ||||
b'bundles via ' | ||||
b'"--config ui.clonebundles=false"' | ||||
|
r43346 | ), | ||
) | ||||
|
r26623 | |||
def trypullbundlefromurl(ui, repo, url): | ||||
"""Attempt to apply a bundle from a URL.""" | ||||
|
r43347 | with repo.lock(), repo.transaction(b'bundleurl') as tr: | ||
|
r26623 | try: | ||
|
r32843 | fh = urlmod.open(ui, url) | ||
|
r43347 | cg = readbundle(ui, fh, b'stream') | ||
|
r26643 | |||
|
r33043 | if isinstance(cg, streamclone.streamcloneapplier): | ||
|
r32843 | cg.apply(repo) | ||
else: | ||||
|
r43347 | bundle2.applybundle(repo, cg, tr, b'clonebundles', url) | ||
|
r32843 | return True | ||
except urlerr.httperror as e: | ||||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'HTTP error fetching bundle: %s\n') | ||
|
r43346 | % stringutil.forcebytestr(e) | ||
) | ||||
|
r32843 | except urlerr.urlerror as e: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'error fetching bundle: %s\n') | ||
|
r43346 | % stringutil.forcebytestr(e.reason) | ||
) | ||||
|
r26623 | |||
|
r32843 | return False | ||