push: hackish handeling of new branch head from phase move
authorPierre-Yves David <pierre-yves.david@fb.com>
Sat, 12 Mar 2016 18:42:16 +0000
changeset 1887 68125d026b07
parent 1886 0504e76bfbd9
child 1888 dfaf0de6f4d8
push: hackish handeling of new branch head from phase move The current head checking mechanism is not expecting "head change" from phase movement. Topic allows that, changeset with a topic moving to public can create a new head. We introduce a hack to double check that no head were added at the transaction level to work around this.
src/topic/__init__.py
src/topic/discovery.py
tests/test-topic-push.t
--- a/src/topic/__init__.py	Sat Mar 12 18:19:27 2016 +0000
+++ b/src/topic/__init__.py	Sat Mar 12 18:42:16 2016 +0000
@@ -14,11 +14,13 @@
 
 from mercurial.i18n import _
 from mercurial import branchmap
+from mercurial import bundle2
 from mercurial import cmdutil
 from mercurial import commands
 from mercurial import context
 from mercurial import discovery as discoverymod
 from mercurial import error
+from mercurial import exchange
 from mercurial import extensions
 from mercurial import localrepo
 from mercurial import lock
@@ -295,6 +297,11 @@
 extensions.wrapfunction(merge, 'update', mergeupdatewrap)
 extensions.wrapfunction(discoverymod, '_headssummary', discovery._headssummary)
 extensions.wrapfunction(wireproto, 'branchmap', discovery.wireprotobranchmap)
+extensions.wrapfunction(bundle2, 'handlecheckheads', discovery.handlecheckheads)
+bundle2.handlecheckheads.params = frozenset() # we need a proper wrape b2 part stuff
+bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads
+extensions.wrapfunction(exchange, '_pushb2phases', discovery._pushb2phases)
+exchange.b2partsgenmapping['phase'] = exchange._pushb2phases
 topicrevset.modsetup()
 cmdutil.summaryhooks.add('topic', summaryhook)
 
--- a/src/topic/discovery.py	Sat Mar 12 18:19:27 2016 +0000
+++ b/src/topic/discovery.py	Sat Mar 12 18:42:16 2016 +0000
@@ -1,4 +1,8 @@
+import weakref
 from mercurial import branchmap
+from mercurial import error
+from mercurial import exchange
+from mercurial.i18n import _
 from . import topicmap
 
 def _headssummary(orig, repo, remote, outgoing):
@@ -49,3 +53,55 @@
         return orig(repo, proto)
     finally:
         repo.__class__ = oldrepo
+
+
+# Discovery have deficiency around phases, branch can get new heads with pure
+# phases change. This happened with a changeset was allowed to be pushed
+# because it had a topic, but it later become public and create a new branch
+# head.
+#
+# Handle this by doing an extra check for new head creation server side
+def _nbheads(repo):
+    data = {}
+    for b in repo.branchmap().iterbranches():
+        if ':' in b[0]:
+            continue
+        data[b[0]] = len(b[1])
+    return data
+
+def handlecheckheads(orig, op, inpart):
+    orig(op, inpart)
+    if op.repo.publishing():
+        return
+    tr = op.gettransaction()
+    if tr.hookargs['source'] not in ('push', 'serve'): # not a push
+        return
+    tr._prepushheads = _nbheads(op.repo)
+    reporef = weakref.ref(op.repo)
+    oldvalidator = tr.validator
+    def validator(tr):
+        repo = reporef()
+        if repo is not None:
+            repo.invalidatecaches()
+            finalheads = _nbheads(repo)
+            for branch, oldnb in tr._prepushheads.iteritems():
+                newnb = finalheads.pop(branch, 0)
+                if oldnb < newnb:
+                    msg = _('push create a new head on branch "%s"' % branch)
+                    raise error.Abort(msg)
+            for branch, newnb in finalheads.iteritems():
+                if 1 < newnb:
+                    msg = _('push create more than 1 head on new branch "%s"' % branch)
+                    raise error.Abort(msg)
+        return oldvalidator(tr)
+    tr.validator = validator
+handlecheckheads.params = frozenset()
+
+def _pushb2phases(orig, pushop, bundler):
+    hascheck =  any(p.type == 'check:heads' for p in  bundler._parts)
+    if pushop.outdatedphases and not hascheck:
+        exchange._pushb2ctxcheckheads(pushop, bundler)
+    return orig(pushop, bundler)
+
+
+
--- a/tests/test-topic-push.t	Sat Mar 12 18:19:27 2016 +0000
+++ b/tests/test-topic-push.t	Sat Mar 12 18:42:16 2016 +0000
@@ -328,3 +328,63 @@
 
   $ cd ..
 
+Test phase move
+==================================
+
+setup, two repo knowns about two small topic branch
+
+  $ hg init repoA
+  $ hg clone repoA repoB
+  updating to branch default
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat << EOF >> repoA/.hg/hgrc
+  > [phases]
+  > publish=False
+  > EOF
+  $ cat << EOF >> repoB/.hg/hgrc
+  > [phases]
+  > publish=False
+  > EOF
+  $ cd repoA
+  $ echo aaa > base
+  $ hg add base
+  $ hg commit -m 'CBASE'
+  $ echo aaa > aaa
+  $ hg add aaa
+  $ hg topic topicA
+  $ hg commit -m 'CA'
+  $ hg up 'desc(CBASE)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo aaa > bbb
+  $ hg add bbb
+  $ hg topic topicB
+  $ hg commit -m 'CB'
+  $ cd ..
+  $ hg push -R repoA repoB
+  pushing to repoB
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files (+1 heads)
+  $ hg log -G -R repoA
+  @  2 default topicB draft CB
+  |
+  | o  1 default topicA draft CA
+  |/
+  o  0 default  draft CBASE
+  
+
+We turn different topic to public on each side,
+
+  $ hg -R repoA phase --public topicA
+  $ hg -R repoB phase --public topicB
+
+Pushing should complain because it create to heads on default
+
+  $ hg push -R repoA repoB
+  pushing to repoB
+  searching for changes
+  no changes found
+  abort: push create a new head on branch "default"
+  [255]