Index: mercurial/bundle2.py =================================================================== --- mercurial/bundle2.py +++ mercurial/bundle2.py @@ -299,7 +299,7 @@ * a way to construct a bundle response when applicable. """ - def __init__(self, repo, transactiongetter, captureoutput=True, source=''): + def __init__(self, repo, transactiongetter, captureoutput=True, source='', force=False): self.repo = repo self.ui = repo.ui self.records = unbundlerecords() @@ -310,6 +310,7 @@ # carries value that can modify part behavior self.modes = {} self.source = source + self.force = force def gettransaction(self): transaction = self._gettransaction() @@ -2135,17 +2136,23 @@ if bookmarksmode == 'apply': tr = op.gettransaction() bookstore = op.repo._bookmarks + allhooks = [] + for book, node in changes: + old_node = bookstore.get(book, '') + if not op.force and old_node != '' and node is not None: + if not bookmarks.validdest(op.repo, op.repo[old_node], op.repo[node]): + raise error.Abort(_('push rejected: bookmark "%s" has changed') % book, + hint=_("run 'hg pull', resolve conflicts, and push again")) + hookargs = tr.hookargs.copy() + hookargs['pushkeycompat'] = '1' + hookargs['namespace'] = 'bookmarks' + hookargs['key'] = book + hookargs['old'] = nodemod.hex(old_node) + hookargs['new'] = nodemod.hex(node if node is not None else '') + hookargs['force'] = op.force + allhooks.append(hookargs) + if pushkeycompat: - allhooks = [] - for book, node in changes: - hookargs = tr.hookargs.copy() - hookargs['pushkeycompat'] = '1' - hookargs['namespace'] = 'bookmarks' - hookargs['key'] = book - hookargs['old'] = nodemod.hex(bookstore.get(book, '')) - hookargs['new'] = nodemod.hex(node if node is not None else '') - allhooks.append(hookargs) - for hookargs in allhooks: op.repo.hook('prepushkey', throw=True, **pycompat.strkwargs(hookargs)) Index: mercurial/exchange.py =================================================================== --- mercurial/exchange.py +++ mercurial/exchange.py @@ -1159,6 +1159,7 @@ 'bundle': stream, 'heads': ['force'], 'url': pushop.remote.url(), + 'force': pushop.force, }).result() except error.BundleValueError as exc: raise error.Abort(_('missing support for %s') % exc) @@ -2375,7 +2376,7 @@ raise error.PushRaced('repository changed while %s - ' 'please try again' % context) -def unbundle(repo, cg, heads, source, url): +def unbundle(repo, cg, heads, source, url, force=None): """Apply a bundle to a repo. this function makes sure the repo is locked during the application and have @@ -2424,7 +2425,8 @@ op = bundle2.bundleoperation(repo, gettransaction, captureoutput=captureoutput, - source='push') + source='push', + force=force) try: op = bundle2.processbundle(repo, cg, op=op) finally: Index: mercurial/localrepo.py =================================================================== --- mercurial/localrepo.py +++ mercurial/localrepo.py @@ -306,14 +306,14 @@ raise error.Abort(_('cannot perform stream clone against local ' 'peer')) - def unbundle(self, bundle, heads, url): + def unbundle(self, bundle, heads, url, force=None): """apply a bundle on a repo This function handles the repo locking itself.""" try: try: bundle = exchange.readbundle(self.ui, bundle, None) - ret = exchange.unbundle(self._repo, bundle, heads, 'push', url) + ret = exchange.unbundle(self._repo, bundle, heads, 'push', url, force=force) if util.safehasattr(ret, 'getchunks'): # This is a bundle20 object, turn it into an unbundler. # This little dance should be dropped eventually when the Index: mercurial/repository.py =================================================================== --- mercurial/repository.py +++ mercurial/repository.py @@ -191,7 +191,7 @@ Successful result should be a generator of data chunks. """ - def unbundle(bundle, heads, url): + def unbundle(bundle, heads, url, force=None): """Transfer repository data to the peer. This is how the bulk of data during a push is transferred. Index: mercurial/wireprotov1peer.py =================================================================== --- mercurial/wireprotov1peer.py +++ mercurial/wireprotov1peer.py @@ -445,7 +445,7 @@ else: return changegroupmod.cg1unpacker(f, 'UN') - def unbundle(self, bundle, heads, url): + def unbundle(self, bundle, heads, url, force=None): '''Send cg (a readable file-like object representing the changegroup to push, typically a chunkbuffer object) to the remote server as a bundle. @@ -464,10 +464,12 @@ ['hashed', hashlib.sha1(''.join(sorted(heads))).digest()]) else: heads = wireprototypes.encodelist(heads) - + args = {'heads': heads} + if force: + args['force'] = 1 if util.safehasattr(bundle, 'deltaheader'): # this a bundle10, do the old style call sequence - ret, output = self._callpush("unbundle", bundle, heads=heads) + ret, output = self._callpush("unbundle", bundle, **args) if ret == "": raise error.ResponseError( _('push failed:'), output) @@ -481,7 +483,7 @@ self.ui.status(_('remote: '), l) else: # bundle2 push. Send a stream, fetch a stream. - stream = self._calltwowaystream('unbundle', bundle, heads=heads) + stream = self._calltwowaystream('unbundle', bundle, **args) ret = bundle2.getunbundler(self.ui, stream) return ret Index: mercurial/wireprotov1server.py =================================================================== --- mercurial/wireprotov1server.py +++ mercurial/wireprotov1server.py @@ -548,8 +548,8 @@ return wireprototypes.streamreslegacy( streamclone.generatev1wireproto(repo)) -@wireprotocommand('unbundle', 'heads', permission='push') -def unbundle(repo, proto, heads): +@wireprotocommand('unbundle', 'heads *', permission='push') +def unbundle(repo, proto, heads, others): their_heads = wireprototypes.decodelist(heads) with proto.mayberedirectstdio() as output: @@ -594,7 +594,7 @@ hint=bundle2requiredhint) r = exchange.unbundle(repo, gen, their_heads, 'serve', - proto.client()) + proto.client(), others.get('force') == '1') if util.safehasattr(r, 'addpart'): # The return looks streamable, we are in the bundle2 case # and should return a stream. Index: tests/test-bookmarks-conflict.t =================================================================== --- /dev/null +++ tests/test-bookmarks-conflict.t @@ -0,0 +1,91 @@ +initialize + $ make_changes() { + > d=`pwd` + > [ ! -z $1 ] && cd $1 + > echo "test `basename \`pwd\``" >> test + > hg commit -Am"${2:-test}" + > r=$? + > cd $d + > return $r + > } + $ ls -1a + . + .. + $ hg init a + $ cd a + $ echo 'test' > test; hg commit -Am'test' + adding test + $ hg book @ + +clone to b + + $ mkdir ../b + $ cd ../b + $ hg clone ../a . + updating to bookmark @ + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ make_changes + $ hg book bk_b + +clone to c + $ mkdir ../c + $ cd ../c + $ hg clone ../a . + updating to bookmark @ + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ make_changes + $ hg book bk_c + +push from b + $ cd ../b + $ hg push -B . + pushing to $TESTTMP/a + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + updating bookmark @ + exporting bookmark bk_b + $ hg -R ../a id -r @ + e11a942451be tip @/bk_b + +push from c + $ cd ../c + $ hg push -B . + pushing to $TESTTMP/a + searching for changes + remote has heads on branch 'default' that are not known locally: e11a942451be + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + exporting bookmark bk_c + $ hg push -B @ + pushing to $TESTTMP/a + searching for changes + no changes found + abort: push rejected: bookmark "@" has changed + (run 'hg pull', resolve conflicts, and push again) + [255] + $ hg -R ../a log -G -T '{rev} {bookmarks}' + o 2 bk_c + | + | o 1 @ bk_b + |/ + @ 0 + + + $ hg push -B @ --force + pushing to $TESTTMP/a + searching for changes + no changes found + updating bookmark @ + [1] + $ hg -R ../a log -G -T '{rev} {bookmarks}' + o 2 @ bk_c + | + | o 1 bk_b + |/ + @ 0 + Index: tests/test-bookmarks-pushpull.t =================================================================== --- tests/test-bookmarks-pushpull.t +++ tests/test-bookmarks-pushpull.t @@ -813,7 +813,7 @@ Z 0d2164f0ce0d foo foobar - $ hg push -B Z http://localhost:$HGPORT/ + $ hg push -B Z http://localhost:$HGPORT/ --force pushing to http://localhost:$HGPORT/ searching for changes no changes found Index: tests/test-hook.t =================================================================== --- tests/test-hook.t +++ tests/test-hook.t @@ -545,6 +545,7 @@ HG_URL=file:$TESTTMP/a pushkey hook: HG_BUNDLE2=1 + HG_FORCE=0 HG_HOOKNAME=pushkey HG_HOOKTYPE=pushkey HG_KEY=foo @@ -632,6 +633,7 @@ HG_TXNNAME=push prepushkey.forbid hook: HG_BUNDLE2=1 + HG_FORCE=0 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=baz