This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 14:00 UTC+2 (a few minutes of down time)

exchange.py 91.9 KB
Newer Older
1
# exchange.py - utility to exchange data between repos.
2 3 4 5 6 7
#
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

Gregory Szorc's avatar
Gregory Szorc committed
8 9
from __future__ import absolute_import

10
import collections
11
import weakref
Gregory Szorc's avatar
Gregory Szorc committed
12 13 14 15 16

from .i18n import _
from .node import (
    hex,
    nullid,
17
    nullrev,
Gregory Szorc's avatar
Gregory Szorc committed
18 19 20 21
)
from . import (
    bookmarks as bookmod,
    bundle2,
22
    bundlecaches,
Gregory Szorc's avatar
Gregory Szorc committed
23 24 25
    changegroup,
    discovery,
    error,
26
    exchangev2,
Gregory Szorc's avatar
Gregory Szorc committed
27
    lock as lockmod,
28
    logexchange,
29
    narrowspec,
Gregory Szorc's avatar
Gregory Szorc committed
30
    obsolete,
31
    obsutil,
Gregory Szorc's avatar
Gregory Szorc committed
32 33
    phases,
    pushkey,
34
    pycompat,
35
    requirements,
Gregory Szorc's avatar
Gregory Szorc committed
36 37 38 39
    scmutil,
    streamclone,
    url as urlmod,
    util,
40
    wireprototypes,
Gregory Szorc's avatar
Gregory Szorc committed
41
)
42 43 44 45
from .utils import (
    hashutil,
    stringutil,
)
46

47 48 49
urlerr = util.urlerr
urlreq = util.urlreq

50
_NARROWACL_SECTION = b'narrowacl'
51

52

53
def readbundle(ui, fh, fname, vfs=None):
54
    header = changegroup.readexactly(fh, 4)
55

56
    alg = None
57
    if not fname:
58 59
        fname = b"stream"
        if not header.startswith(b'HG') and header.startswith(b'\0'):
60
            fh = changegroup.headerlessfixup(fh, header)
61 62
            header = b"HG10"
            alg = b'UN'
63 64 65
    elif vfs:
        fname = vfs.join(fname)

66
    magic, version = header[0:2], header[2:4]
67

68 69 70
    if magic != b'HG':
        raise error.Abort(_(b'%s: not a Mercurial bundle') % fname)
    if version == b'10':
71 72
        if alg is None:
            alg = changegroup.readexactly(fh, 2)
73
        return changegroup.cg1unpacker(fh, alg)
74
    elif version.startswith(b'2'):
75
        return bundle2.getunbundler(ui, fh, magicstring=magic + version)
76
    elif version == b'S1':
77
        return streamclone.streamcloneapplier(fh)
78
    else:
79 80 81
        raise error.Abort(
            _(b'%s: unknown bundle version %s') % (fname, version)
        )
82

83

84 85 86 87 88 89
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.
    """
90

91
    def speccompression(alg):
92 93 94 95
        try:
            return util.compengines.forbundletype(alg).bundletype()[0]
        except KeyError:
            return None
96 97 98 99

    b = readbundle(ui, fh, None)
    if isinstance(b, changegroup.cg1unpacker):
        alg = b._type
100 101
        if alg == b'_truncatedBZ':
            alg = b'BZ'
102 103
        comp = speccompression(alg)
        if not comp:
104 105
            raise error.Abort(_(b'unknown compression algorithm: %s') % alg)
        return b'%s-v1' % comp
106
    elif isinstance(b, bundle2.unbundle20):
107 108
        if b'Compression' in b.params:
            comp = speccompression(b.params[b'Compression'])
109
            if not comp:
110 111 112
                raise error.Abort(
                    _(b'unknown compression algorithm: %s') % comp
                )
113
        else:
114
            comp = b'none'
115 116 117

        version = None
        for part in b.iterparts():
118 119 120 121
            if part.type == b'changegroup':
                version = part.params[b'version']
                if version in (b'01', b'02'):
                    version = b'v2'
122
                else:
123 124
                    raise error.Abort(
                        _(
125 126
                            b'changegroup version %s does not have '
                            b'a known bundlespec'
127 128
                        )
                        % version,
129
                        hint=_(b'try upgrading your Mercurial client'),
130
                    )
131
            elif part.type == b'stream2' and version is None:
132
                # A stream2 part requires to be part of a v2 bundle
133
                requirements = urlreq.unquote(part.params[b'requirements'])
134 135
                splitted = requirements.split()
                params = bundle2._formatrequirementsparams(splitted)
136
                return b'none-v2;stream=v2;%s' % params
137 138

        if not version:
139
            raise error.Abort(
140
                _(b'could not identify changegroup version in bundle')
141
            )
142

143
        return b'%s-%s' % (comp, version)
144 145
    elif isinstance(b, streamclone.streamcloneapplier):
        requirements = streamclone.readbundle1header(fh)[2]
146
        formatted = bundle2._formatrequirementsparams(requirements)
147
        return b'none-packed1;%s' % formatted
148
    else:
149
        raise error.Abort(_(b'unknown bundle type: %s') % b)
150

151

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
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:
        common = [nullid]
    if not heads:
        heads = cl.heads()
    return discovery.outgoing(repo, common, heads)

171

172 173 174
def _checkpublish(pushop):
    repo = pushop.repo
    ui = repo.ui
175 176
    behavior = ui.config(b'experimental', b'auto-publish')
    if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
177
        return
178 179
    remotephases = listkeys(pushop.remote, b'phases')
    if not remotephases.get(b'publishing', False):
180 181 182
        return

    if pushop.revs is None:
183
        published = repo.filtered(b'served').revs(b'not public()')
184
    else:
185
        published = repo.revs(b'::%ln - public()', pushop.revs)
186
    if published:
187 188 189 190 191
        if behavior == b'warn':
            ui.warn(
                _(b'%i changesets about to be published\n') % len(published)
            )
        elif behavior == b'confirm':
192
            if ui.promptchoice(
193
                _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
194 195
                % len(published)
            ):
196
                raise error.CanceledError(_(b'user quit'))
197 198
        elif behavior == b'abort':
            msg = _(b'push would publish %i changesets') % len(published)
199
            hint = _(
200 201
                b"use --publish or adjust 'experimental.auto-publish'"
                b" config"
202
            )
203 204
            raise error.Abort(msg, hint=hint)

205

206 207
def _forcebundle1(op):
    """return true if a pull/push must use bundle1
208

209 210
    This function is used to allow testing of the older bundle version"""
    ui = op.repo.ui
211
    # The goal is this config is to allow developer to choose the bundle
212 213 214 215 216
    # 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
217 218 219
    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')
220

221

222 223 224
class pushoperation(object):
    """A object that represent a single push operation

225
    Its purpose is to carry push related state and very common operations.
226

227 228
    A new pushoperation should be created at the beginning of each push and
    discarded afterward.
229 230
    """

231 232 233 234 235 236 237 238 239 240 241
    def __init__(
        self,
        repo,
        remote,
        force=False,
        revs=None,
        newbranch=False,
        bookmarks=(),
        publish=False,
        pushvars=None,
    ):
242 243
        # repo we push from
        self.repo = repo
244
        self.ui = repo.ui
245 246
        # repo we push to
        self.remote = remote
247 248
        # force option provided
        self.force = force
249 250
        # revs to be pushed (None is "all")
        self.revs = revs
251 252
        # bookmark explicitly pushed
        self.bookmarks = bookmarks
253 254
        # allow push of new branch
        self.newbranch = newbranch
255 256 257
        # step already performed
        # (used to check what steps have been already performed through bundle2)
        self.stepsdone = set()
258
        # Integer version of the changegroup push result
259 260 261 262 263
        # - 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()
264
        self.cgresult = None
265 266
        # Boolean value for the bookmark push
        self.bkresult = None
267
        # discover.outgoing object (contains common and outgoing data)
268
        self.outgoing = None
269
        # all remote topological heads before the push
270
        self.remoteheads = None
271 272 273 274 275 276 277 278 279 280 281 282 283
        # 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
284 285
        # testable as a boolean indicating if any nodes are missing locally.
        self.incoming = None
286 287
        # summary of the remote phase situation
        self.remotephases = None
288 289 290 291
        # 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
292
        # outgoing obsmarkers
293
        self.outobsmarkers = set()
294
        # outgoing bookmarks, list of (bm, oldnode | '', newnode | '')
295
        self.outbookmarks = []
296 297
        # transaction manager
        self.trmanager = None
298 299 300
        # map { pushkey partid -> callback handling failure}
        # used to handle exception from mandatory pushkey part failure
        self.pkfailcb = {}
Jun Wu's avatar
Jun Wu committed
301 302
        # an iterable of pushvars or None
        self.pushvars = pushvars
303 304
        # publish pushed changesets
        self.publish = publish
305

306 307 308
    @util.propertycache
    def futureheads(self):
        """future remote heads if the changeset push succeeds"""
309
        return self.outgoing.ancestorsof
310

311 312 313 314 315 316 317
    @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()
318 319
        # I want cheads = heads(::ancestorsof and ::commonheads)
        # (ancestorsof is revs with secret changeset filtered out)
320 321
        #
        # This can be expressed as:
322 323
        #     cheads = ( (ancestorsof and ::commonheads)
        #              + (commonheads and ::ancestorsof))"
324 325 326 327
        #              )
        #
        # while trying to push we already computed the following:
        #     common = (::commonheads)
328
        #     missing = ((commonheads::ancestorsof) - commonheads)
329 330
        #
        # We can pick:
331
        # * ancestorsof part of common (::commonheads)
332
        common = self.outgoing.common
333 334
        rev = self.repo.changelog.index.rev
        cheads = [node for node in self.revs if rev(node) in common]
335 336
        # and
        # * commonheads parents on missing
337
        revset = unfi.set(
338
            b'%ln and parents(roots(%ln))',
339 340 341
            self.outgoing.commonheads,
            self.outgoing.missing,
        )
342 343 344
        cheads.extend(c.node() for c in revset)
        return cheads

345 346 347
    @property
    def commonheads(self):
        """set of all common heads after changeset bundle push"""
348
        if self.cgresult:
349 350 351
            return self.futureheads
        else:
            return self.fallbackheads
352

353

354
# mapping of message used when pushing bookmark
355
bookmsgmap = {
356 357 358
    b'update': (
        _(b"updating bookmark %s\n"),
        _(b'updating bookmark %s failed!\n'),
359
    ),
360 361 362
    b'export': (
        _(b"exporting bookmark %s\n"),
        _(b'exporting bookmark %s failed!\n'),
363
    ),
364 365 366
    b'delete': (
        _(b"deleting remote bookmark %s\n"),
        _(b'deleting remote bookmark %s failed!\n'),
367 368
    ),
}
369 370


371 372 373 374 375 376 377 378 379 380
def push(
    repo,
    remote,
    force=False,
    revs=None,
    newbranch=False,
    bookmarks=(),
    publish=False,
    opargs=None,
):
381 382 383 384 385 386 387 388
    '''Push outgoing changesets (limited by revs) from a local
    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()
    '''
389 390
    if opargs is None:
        opargs = {}
391 392 393 394 395 396 397 398 399 400
    pushop = pushoperation(
        repo,
        remote,
        force,
        revs,
        newbranch,
        bookmarks,
        publish,
        **pycompat.strkwargs(opargs)
    )
401
    if pushop.remote.local():
402 403 404
        missing = (
            set(pushop.repo.requirements) - pushop.remote.local().supported
        )
405
        if missing:
406
            msg = _(
407 408 409 410
                b"required features are not"
                b" supported in the destination:"
                b" %s"
            ) % (b', '.join(sorted(missing)))
411
            raise error.Abort(msg)
412

413
    if not pushop.remote.canpush():
414
        raise error.Abort(_(b"destination does not support push"))
415

416
    if not pushop.remote.capable(b'unbundle'):
417 418
        raise error.Abort(
            _(
419 420
                b'cannot push: destination does not support the '
                b'unbundle wire protocol command'
421 422
            )
        )
423

424 425
    # get lock as we might write phase data
    wlock = lock = None
426
    try:
427 428
        # bundle2 push may receive a reply bundle touching bookmarks
        # requiring the wlock. Take it now to ensure proper ordering.
429
        maypushback = pushop.ui.configbool(b'experimental', b'bundle2.pushback')
430 431 432 433 434
        if (
            (not _forcebundle1(pushop))
            and maypushback
            and not bookmod.bookmarksinstore(repo)
        ):
435 436
            wlock = pushop.repo.wlock()
        lock = pushop.repo.lock()
437
        pushop.trmanager = transactionmanager(
438
            pushop.repo, b'push-response', pushop.remote.url()
439
        )
440
    except error.LockUnavailable as err:
441 442 443
        # source repo cannot be locked.
        # We do not abort the push, but just disable the local phase
        # synchronisation.
444
        msg = b'cannot lock source repository: %s\n' % stringutil.forcebytestr(
445 446
            err
        )
447
        pushop.ui.debug(msg)
448

449 450 451 452 453 454
    with wlock or util.nullcontextmanager():
        with lock or util.nullcontextmanager():
            with pushop.trmanager or util.nullcontextmanager():
                pushop.repo.checkpush(pushop)
                _checkpublish(pushop)
                _pushdiscovery(pushop)
455 456
                if not pushop.force:
                    _checksubrepostate(pushop)
457 458 459 460 461 462
                if not _forcebundle1(pushop):
                    _pushbundle2(pushop)
                _pushchangeset(pushop)
                _pushsyncphase(pushop)
                _pushobsolete(pushop)
                _pushbookmark(pushop)
463

464
    if repo.ui.configbool(b'experimental', b'remotenames'):
465 466
        logexchange.pullremotenames(repo, remote)

467
    return pushop
468

469

470 471 472 473 474 475 476 477
# 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 = {}

478

479 480 481 482 483 484 485 486 487
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."""
488

489 490 491 492 493
    def dec(func):
        assert stepname not in pushdiscoverymapping
        pushdiscoverymapping[stepname] = func
        pushdiscoveryorder.append(stepname)
        return func
494

495 496
    return dec

497

498
def _pushdiscovery(pushop):
499 500 501 502 503
    """Run all discovery steps"""
    for stepname in pushdiscoveryorder:
        step = pushdiscoverymapping[stepname]
        step(pushop)

504

505 506 507 508 509 510 511 512 513 514 515
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)


516
@pushdiscovery(b'changeset')
517 518
def _pushdiscoverychangeset(pushop):
    """discover the changeset that need to be pushed"""
519
    fci = discovery.findcommonincoming
520
    if pushop.revs:
521 522 523 524 525 526
        commoninc = fci(
            pushop.repo,
            pushop.remote,
            force=pushop.force,
            ancestorsof=pushop.revs,
        )
527 528
    else:
        commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
529 530
    common, inc, remoteheads = commoninc
    fco = discovery.findcommonoutgoing
531 532 533 534 535 536 537
    outgoing = fco(
        pushop.repo,
        pushop.remote,
        onlyheads=pushop.revs,
        commoninc=commoninc,
        force=pushop.force,
    )
538 539 540 541
    pushop.outgoing = outgoing
    pushop.remoteheads = remoteheads
    pushop.incoming = inc

542

543
@pushdiscovery(b'phase')
544 545 546 547 548 549
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()
550
    remotephases = listkeys(pushop.remote, b'phases')
551

552
    if (
553
        pushop.ui.configbool(b'ui', b'_usedassubrepo')
554 555
        and remotephases  # server supports phases
        and not pushop.outgoing.missing  # no changesets to be pushed
556
        and remotephases.get(b'publishing', False)
557
    ):
558 559 560 561 562
        # When:
        # - this is a subrepo push
        # - and remote support phase
        # - and no changeset are to be pushed
        # - and remote is publishing
563
        # We may be in issue 3781 case!
564 565 566
        # We drop the possible phase synchronisation done by
        # courtesy to publish changesets possibly locally draft
        # on the remote.
567 568 569
        pushop.outdatedphases = []
        pushop.fallbackoutdatedphases = []
        return
570

571 572 573
    pushop.remotephases = phases.remotephasessummary(
        pushop.repo, pushop.fallbackheads, remotephases
    )
574 575
    droots = pushop.remotephases.draftroots

576
    extracond = b''
577
    if not pushop.remotephases.publishing:
578 579
        extracond = b' and public()'
    revset = b'heads((%%ln::%%ln) %s)' % extracond
580 581 582 583
    # 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))
584
    if not pushop.remotephases.publishing and pushop.publish:
585 586
        future = list(
            unfi.set(
587
                b'%ln and (not public() or %ln::)', pushop.futureheads, droots
588 589
            )
        )
590
    elif not outgoing.missing:
591 592 593 594
        future = fallback
    else:
        # adds changeset we are going to push as draft
        #
595
        # should not be necessary for publishing server, but because of an
596
        # issue fixed in xxxxx we have to do it anyway.
597
        fdroots = list(
598
            unfi.set(b'roots(%ln  + %ln::)', outgoing.missing, droots)
599
        )
600 601 602 603 604
        fdroots = [f.node() for f in fdroots]
        future = list(unfi.set(revset, fdroots, pushop.futureheads))
    pushop.outdatedphases = future
    pushop.fallbackoutdatedphases = fallback

605

606
@pushdiscovery(b'obsmarker')
607
def _pushdiscoveryobsmarkers(pushop):
608 609 610 611 612 613
    if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
        return

    if not pushop.repo.obsstore:
        return

614
    if b'obsolete' not in listkeys(pushop.remote, b'namespaces'):
615 616 617 618 619
        return

    repo = pushop.repo
    # very naive computation, that can be quite expensive on big repo.
    # However: evolution is currently slow on them anyway.
620
    nodes = (c.node() for c in repo.set(b'::%ln', pushop.futureheads))
621
    pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
622

623

624
@pushdiscovery(b'bookmarks')
625 626 627 628
def _pushdiscoverybookmarks(pushop):
    ui = pushop.ui
    repo = pushop.repo.unfiltered()
    remote = pushop.remote
629
    ui.debug(b"checking for updated bookmarks\n")
630 631
    ancestors = ()
    if pushop.revs:
632
        revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
633
        ancestors = repo.changelog.ancestors(revnums, inclusive=True)
634

635
    remotebookmark = bookmod.unhexlifybookmarks(listkeys(remote, b'bookmarks'))
636

637 638 639
    explicit = {
        repo._bookmarks.expandname(bookmark) for bookmark in pushop.bookmarks
    }
640

641
    comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
642 643
    return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)

644

645
def _processcompared(pushop, pushed, explicit, remotebms, comp):
646
    """take decision on bookmarks to push to the remote repo
647

648
    Exists to help extensions alter this behavior.
649
    """
650
    addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
651

652 653
    repo = pushop.repo

654
    for b, scid, dcid in advsrc:
655 656
        if b in explicit:
            explicit.remove(b)
657
        if not pushed or repo[scid].rev() in pushed:
658
            pushop.outbookmarks.append((b, dcid, scid))
659 660 661 662
    # search added bookmark
    for b, scid, dcid in addsrc:
        if b in explicit:
            explicit.remove(b)
663 664 665 666 667
            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))
668
    # search for overwritten bookmark
669
    for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
670 671 672 673 674 675 676 677
        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"
678
            pushop.outbookmarks.append((b, dcid, b''))
679 680 681 682
    # identical bookmarks shouldn't get reported
    for b, scid, dcid in same:
        if b in explicit:
            explicit.remove(b)
683 684 685 686

    if explicit:
        explicit = sorted(explicit)
        # we should probably list all of them
687 688
        pushop.ui.warn(
            _(
689 690
                b'bookmark %s does not exist on the local '
                b'or remote repository!\n'
691 692 693
            )
            % explicit[0]
        )
694 695 696
        pushop.bkresult = 2

    pushop.outbookmarks.sort()
697

698

699 700 701 702 703 704 705 706 707 708 709 710
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:
711 712 713 714 715 716 717 718 719 720 721 722 723 724
            # 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:
725 726
                ctx = unfi[node]
                if ctx.obsolete():
727
                    raise error.Abort(mso % ctx)
728
                elif ctx.isunstable():
729 730 731
                    # TODO print more than one instability in the abort
                    # message
                    raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
732

733
        discovery.checkheads(pushop)
734 735
    return True

736

737 738 739 740 741 742 743 744
# 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 = {}

745

746
def b2partsgenerator(stepname, idx=None):
747 748 749 750 751 752 753 754
    """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."""
755

756 757 758
    def dec(func):
        assert stepname not in b2partsgenmapping
        b2partsgenmapping[stepname] = func
759 760 761 762
        if idx is None:
            b2partsgenorder.append(stepname)
        else:
            b2partsgenorder.insert(idx, stepname)
763
        return func
764

765 766
    return dec

767

768 769 770
def _pushb2ctxcheckheads(pushop, bundler):
    """Generate race condition checking parts

771
    Exists as an independent function to aid extensions
772
    """
773 774
    # * 'force' do not check for push race,
    # * if we don't push anything, there are nothing to check.
775
    if not pushop.force and pushop.outgoing.ancestorsof:
776 777 778
        allowunrelated = b'related' in bundler.capabilities.get(
            b'checkheads', ()
        )
779 780
        emptyremote = pushop.pushbranchmap is None
        if not allowunrelated or emptyremote:
781
            bundler.newpart(b'check:heads', data=iter(pushop.remoteheads))
782 783
        else:
            affected = set()
784
            for branch, heads in pycompat.iteritems(pushop.pushbranchmap):
785 786 787 788 789 790 791
                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))
792
                bundler.newpart(b'check:updated-heads', data=data)
793

794

795 796
def _pushing(pushop):
    """return True if we are pushing anything"""
797 798 799 800 801 802 803
    return bool(
        pushop.outgoing.missing
        or pushop.outdatedphases
        or pushop.outobsmarkers
        or pushop.outbookmarks
    )

804

805
@b2partsgenerator(b'check-bookmarks')
806 807 808 809 810
def _pushb2checkbookmarks(pushop, bundler):
    """insert bookmark move checking"""
    if not _pushing(pushop) or pushop.force:
        return
    b2caps = bundle2.bundle2caps(pushop.remote)
811
    hasbookmarkcheck = b'bookmarks' in b2caps
812 813 814 815 816 817
    if not (pushop.outbookmarks and hasbookmarkcheck):
        return
    data = []
    for book, old, new in pushop.outbookmarks:
        data.append((book, old))
    checkdata = bookmod.binaryencode(data)
818
    bundler.newpart(b'check:bookmarks', data=checkdata)
819

820

821
@b2partsgenerator(b'check-phases')
822 823 824 825 826
def _pushb2checkphases(pushop, bundler):
    """insert phase move checking"""
    if not _pushing(pushop) or pushop.force:
        return
    b2caps = bundle2.bundle2caps(pushop.remote)
827
    hasphaseheads = b'heads' in b2caps.get(b'phases', ())
828 829
    if pushop.remotephases is not None and hasphaseheads:
        # check that the remote phase has not changed
830
        checks = {p: [] for p in phases.allphases}
831 832
        checks[phases.public].extend(pushop.remotephases.publicheads)
        checks[phases.draft].extend(pushop.remotephases.draftroots)
833 834 835
        if any(pycompat.itervalues(checks)):
            for phase in checks:
                checks[phase].sort()
836
            checkdata = phases.binaryencode(checks)