exchange.py
255 lines
| 11.4 KiB
| text/x-python
|
PythonLexer
/ mercurial / exchange.py
Pierre-Yves David
|
r20345 | # exchange.py - utily to exchange data between repo. | ||
# | ||||
# 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. | ||||
from i18n import _ | ||||
from node import hex | ||||
import errno | ||||
import util, scmutil, changegroup | ||||
import discovery, phases, obsolete, bookmarks | ||||
Pierre-Yves David
|
r20346 | |||
class pushoperation(object): | ||||
"""A object that represent a single push operation | ||||
It purpose is to carry push related state and very common operation. | ||||
A new should be created at the begining of each push and discarded | ||||
afterward. | ||||
""" | ||||
Pierre-Yves David
|
r20349 | def __init__(self, repo, remote, force=False): | ||
Pierre-Yves David
|
r20346 | # repo we push from | ||
self.repo = repo | ||||
Pierre-Yves David
|
r20347 | self.ui = repo.ui | ||
Pierre-Yves David
|
r20348 | # repo we push to | ||
self.remote = remote | ||||
Pierre-Yves David
|
r20349 | # force option provided | ||
self.force = force | ||||
Pierre-Yves David
|
r20346 | |||
Pierre-Yves David
|
r20345 | def push(repo, remote, force=False, revs=None, newbranch=False): | ||
'''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() | ||||
''' | ||||
Pierre-Yves David
|
r20349 | pushop = pushoperation(repo, remote, force) | ||
Pierre-Yves David
|
r20348 | if pushop.remote.local(): | ||
missing = (set(pushop.repo.requirements) | ||||
- pushop.remote.local().supported) | ||||
Pierre-Yves David
|
r20345 | if missing: | ||
msg = _("required features are not" | ||||
" supported in the destination:" | ||||
" %s") % (', '.join(sorted(missing))) | ||||
raise util.Abort(msg) | ||||
# there are two ways to push to remote repo: | ||||
# | ||||
# addchangegroup assumes local user can lock remote | ||||
# repo (local filesystem, old ssh servers). | ||||
# | ||||
# unbundle assumes local user cannot lock remote repo (new ssh | ||||
# servers, http servers). | ||||
Pierre-Yves David
|
r20348 | if not pushop.remote.canpush(): | ||
Pierre-Yves David
|
r20345 | raise util.Abort(_("destination does not support push")) | ||
Pierre-Yves David
|
r20346 | unfi = pushop.repo.unfiltered() | ||
Pierre-Yves David
|
r20345 | def localphasemove(nodes, phase=phases.public): | ||
"""move <nodes> to <phase> in the local source repo""" | ||||
if locallock is not None: | ||||
Pierre-Yves David
|
r20346 | phases.advanceboundary(pushop.repo, phase, nodes) | ||
Pierre-Yves David
|
r20345 | else: | ||
# repo is not locked, do not change any phases! | ||||
# Informs the user that phases should have been moved when | ||||
# applicable. | ||||
Pierre-Yves David
|
r20346 | actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()] | ||
Pierre-Yves David
|
r20345 | phasestr = phases.phasenames[phase] | ||
if actualmoves: | ||||
Pierre-Yves David
|
r20347 | pushop.ui.status(_('cannot lock source repo, skipping ' | ||
'local %s phase update\n') % phasestr) | ||||
Pierre-Yves David
|
r20345 | # get local lock as we might write phase data | ||
locallock = None | ||||
try: | ||||
Pierre-Yves David
|
r20346 | locallock = pushop.repo.lock() | ||
Pierre-Yves David
|
r20345 | except IOError, err: | ||
if err.errno != errno.EACCES: | ||||
raise | ||||
# source repo cannot be locked. | ||||
# We do not abort the push, but just disable the local phase | ||||
# synchronisation. | ||||
msg = 'cannot lock source repository: %s\n' % err | ||||
Pierre-Yves David
|
r20347 | pushop.ui.debug(msg) | ||
Pierre-Yves David
|
r20345 | try: | ||
Pierre-Yves David
|
r20349 | pushop.repo.checkpush(pushop.force, revs) | ||
Pierre-Yves David
|
r20345 | lock = None | ||
Pierre-Yves David
|
r20348 | unbundle = pushop.remote.capable('unbundle') | ||
Pierre-Yves David
|
r20345 | if not unbundle: | ||
Pierre-Yves David
|
r20348 | lock = pushop.remote.lock() | ||
Pierre-Yves David
|
r20345 | try: | ||
# discovery | ||||
fci = discovery.findcommonincoming | ||||
Pierre-Yves David
|
r20349 | commoninc = fci(unfi, pushop.remote, force=pushop.force) | ||
Pierre-Yves David
|
r20345 | common, inc, remoteheads = commoninc | ||
fco = discovery.findcommonoutgoing | ||||
Pierre-Yves David
|
r20348 | outgoing = fco(unfi, pushop.remote, onlyheads=revs, | ||
Pierre-Yves David
|
r20349 | commoninc=commoninc, force=pushop.force) | ||
Pierre-Yves David
|
r20345 | |||
if not outgoing.missing: | ||||
# nothing to push | ||||
scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded) | ||||
ret = None | ||||
else: | ||||
# something to push | ||||
Pierre-Yves David
|
r20349 | if not pushop.force: | ||
Pierre-Yves David
|
r20345 | # if repo.obsstore == False --> no obsolete | ||
# then, save the iteration | ||||
if unfi.obsstore: | ||||
# this message are here for 80 char limit reason | ||||
mso = _("push includes obsolete changeset: %s!") | ||||
mst = "push includes %s changeset: %s!" | ||||
# plain versions for i18n tool to detect them | ||||
_("push includes unstable changeset: %s!") | ||||
_("push includes bumped changeset: %s!") | ||||
_("push includes divergent changeset: %s!") | ||||
# 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.missingheads: | ||||
ctx = unfi[node] | ||||
if ctx.obsolete(): | ||||
raise util.Abort(mso % ctx) | ||||
elif ctx.troubled(): | ||||
raise util.Abort(_(mst) | ||||
% (ctx.troubles()[0], | ||||
ctx)) | ||||
Pierre-Yves David
|
r20347 | newbm = pushop.ui.configlist('bookmarks', 'pushing') | ||
Pierre-Yves David
|
r20348 | discovery.checkheads(unfi, pushop.remote, outgoing, | ||
Pierre-Yves David
|
r20345 | remoteheads, newbranch, | ||
bool(inc), newbm) | ||||
# TODO: get bundlecaps from remote | ||||
bundlecaps = None | ||||
# create a changegroup from local | ||||
if revs is None and not (outgoing.excluded | ||||
Pierre-Yves David
|
r20346 | or pushop.repo.changelog.filteredrevs): | ||
Pierre-Yves David
|
r20345 | # push everything, | ||
# use the fast path, no race possible on push | ||||
Pierre-Yves David
|
r20346 | bundler = changegroup.bundle10(pushop.repo, bundlecaps) | ||
cg = pushop.repo._changegroupsubset(outgoing, | ||||
bundler, | ||||
'push', | ||||
fastpath=True) | ||||
Pierre-Yves David
|
r20345 | else: | ||
Pierre-Yves David
|
r20346 | cg = pushop.repo.getlocalbundle('push', outgoing, | ||
bundlecaps) | ||||
Pierre-Yves David
|
r20345 | |||
# apply changegroup to remote | ||||
if unbundle: | ||||
# 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. | ||||
Pierre-Yves David
|
r20349 | if pushop.force: | ||
Pierre-Yves David
|
r20345 | remoteheads = ['force'] | ||
# ssh: return remote's addchangegroup() | ||||
# http: return remote's addchangegroup() or 0 for error | ||||
Pierre-Yves David
|
r20348 | ret = pushop.remote.unbundle(cg, remoteheads, 'push') | ||
Pierre-Yves David
|
r20345 | else: | ||
# we return an integer indicating remote head count | ||||
# change | ||||
Pierre-Yves David
|
r20348 | ret = pushop.remote.addchangegroup(cg, 'push', | ||
pushop.repo.url()) | ||||
Pierre-Yves David
|
r20345 | |||
if ret: | ||||
# push succeed, synchronize target of the push | ||||
cheads = outgoing.missingheads | ||||
elif revs is None: | ||||
# All out push fails. synchronize all common | ||||
cheads = outgoing.commonheads | ||||
else: | ||||
# I want cheads = heads(::missingheads and ::commonheads) | ||||
# (missingheads is revs with secret changeset filtered out) | ||||
# | ||||
# This can be expressed as: | ||||
# cheads = ( (missingheads and ::commonheads) | ||||
# + (commonheads and ::missingheads))" | ||||
# ) | ||||
# | ||||
# while trying to push we already computed the following: | ||||
# common = (::commonheads) | ||||
# missing = ((commonheads::missingheads) - commonheads) | ||||
# | ||||
# We can pick: | ||||
# * missingheads part of common (::commonheads) | ||||
common = set(outgoing.common) | ||||
Pierre-Yves David
|
r20346 | nm = pushop.repo.changelog.nodemap | ||
Pierre-Yves David
|
r20345 | cheads = [node for node in revs if nm[node] in common] | ||
# and | ||||
# * commonheads parents on missing | ||||
revset = unfi.set('%ln and parents(roots(%ln))', | ||||
outgoing.commonheads, | ||||
outgoing.missing) | ||||
cheads.extend(c.node() for c in revset) | ||||
# even when we don't push, exchanging phase data is useful | ||||
Pierre-Yves David
|
r20348 | remotephases = pushop.remote.listkeys('phases') | ||
Pierre-Yves David
|
r20347 | if (pushop.ui.configbool('ui', '_usedassubrepo', False) | ||
Pierre-Yves David
|
r20345 | and remotephases # server supports phases | ||
and ret is None # nothing was pushed | ||||
and remotephases.get('publishing', False)): | ||||
# 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. | ||||
remotephases = {'publishing': 'True'} | ||||
if not remotephases: # old server or public only repo | ||||
localphasemove(cheads) | ||||
# don't push any phase data as there is nothing to push | ||||
else: | ||||
Pierre-Yves David
|
r20346 | ana = phases.analyzeremotephases(pushop.repo, cheads, | ||
remotephases) | ||||
Pierre-Yves David
|
r20345 | pheads, droots = ana | ||
### Apply remote phase on local | ||||
if remotephases.get('publishing', False): | ||||
localphasemove(cheads) | ||||
else: # publish = False | ||||
localphasemove(pheads) | ||||
localphasemove(cheads, phases.draft) | ||||
### Apply local phase on remote | ||||
# 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 | ||||
outdated = unfi.set('heads((%ln::%ln) and public())', | ||||
droots, cheads) | ||||
for newremotehead in outdated: | ||||
Pierre-Yves David
|
r20348 | r = pushop.remote.pushkey('phases', | ||
newremotehead.hex(), | ||||
str(phases.draft), | ||||
str(phases.public)) | ||||
Pierre-Yves David
|
r20345 | if not r: | ||
Pierre-Yves David
|
r20347 | pushop.ui.warn(_('updating %s to public failed!\n') | ||
% newremotehead) | ||||
pushop.ui.debug('try to push obsolete markers to remote\n') | ||||
Pierre-Yves David
|
r20348 | obsolete.syncpush(pushop.repo, pushop.remote) | ||
Pierre-Yves David
|
r20345 | finally: | ||
if lock is not None: | ||||
lock.release() | ||||
finally: | ||||
if locallock is not None: | ||||
locallock.release() | ||||
Pierre-Yves David
|
r20348 | bookmarks.updateremote(pushop.ui, unfi, pushop.remote, revs) | ||
Pierre-Yves David
|
r20345 | return ret | ||