checkheads: handle partial obsolescence
authorPierre-Yves David <pierre-yves.david@ens-lyon.org>
Wed, 29 Mar 2017 15:48:27 +0200
changeset 2249 0ecb9fba6364
parent 2248 c2817eac11e5
child 2250 2b4e2e93b7db
checkheads: handle partial obsolescence We now properly detects situations were only parts of the remote branch is obsoleted. To do so, we process children in the branch recursively to see if they will be obsolete. The current code has some trouble when the remote branch in unknown locally, or when the prune happened on a successors that is not relevant to the push. These case will be handled later. The processing code is becoming more and more complex, a lighter approach would be to check for the obsolescence markers that are relevant to the pushed set, but I prefer to stick with the current approach until more test cases are written.
hgext3rd/evolve/checkheads.py
tests/test-checkheads-partial-C1.t
tests/test-checkheads-partial-C2.t
tests/test-checkheads-partial-C3.t
tests/test-checkheads-partial-C4.t
--- a/hgext3rd/evolve/checkheads.py	Wed Mar 29 16:41:42 2017 +0200
+++ b/hgext3rd/evolve/checkheads.py	Wed Mar 29 15:48:27 2017 +0200
@@ -191,35 +191,108 @@
     # pushed element:
     #
     # XXX as above, There are several cases this code does not handle
-    # XXX properly
+    # XXX properly. some below:
     #
     # * We "silently" skip processing on all changeset unknown locally
     #
-    # (1) if <nh> is public, it won't be affected by obsolete marker
-    #     and a new is created
+    # * if <nh> is public on the remote, it won't be affected by obsolete
+    #     marker and a new is created
     #
-    # (2) if the new heads have ancestors which are not obsolete and
-    #     not ancestors of any other heads we will have a new head too.
+    # * if a successors is pruned, but the prune marker will not be exchanged,
+    #   we currently badely detect the situation.
     #
     # These two cases will be easy to handle for known changeset but
     # much more tricky for unsynced changes.
     repo = pushop.repo
     unfi = repo.unfiltered()
+    cl = unfi.changelog
     newhs = set()
     discarded = set()
-    for nh in candidate:
-        if nh not in unfi:
+    # I leave the print in the code because they are so handy at debugging
+    # and I keep getting back to this piece of code.
+    #
+    # from mercurial.node import short
+    localcandidate = set()
+    unknownheads = set()
+    for h in candidate:
+        if h in unfi:
+            localcandidate.add(h)
+        else:
+            unknownheads.add(h)
+    if len(localcandidate) == 1:
+        return unknownheads | set(candidate), set()
+    while localcandidate:
+        nh = localcandidate.pop()
+        # print 'newhead:', short(nh)
+        currentbranch = unfi.revs('only(%n, (%ln+%ln))',
+                                  nh, localcandidate, newhs)
+        if nh in futurecommon:
+            # The heads will still be alive in the future set
+            # print ' >> head is common'
             newhs.add(nh)
             continue
-        if unfi[nh].phase() <= phases.public:
-            newhs.add(nh)
-        else:
-            # check if the head is pruned
-            # XXX we need some "stop at" argument to prevent iterating too far.
-            # XXX but pushing obsolete changeset will requires --force anyway.
-            sucsets = obsolete.successorssets(repo, nh)
+        stack = [nh]
+        seen = set(stack)
+        while stack:
+            current = stack.pop()
+            # print ' current:', short(current)
+            if current in futurecommon:
+                if cl.rev(current) not in currentbranch:
+                    # we reach the bottom of the branch, do not go further
+                    # print '  > futurecommon'
+                    continue
+            # XXX we do not know the phase on the remote side
+            if unfi[current].phase() <= phases.public:
+                # public changeset cannot be rewritten. This head will remain.
+                newhs.add(nh)
+                break
+            # We check is the changeset will disappear on push. It happens when
+            #
+            # * the changeset is pruned,
+            # * the changeset is superceed by a one in future common set.
+            #
+            # XXX note: maybe we can just check is the changeset is affected by a
+            # markers relevant to the 'push-set' but that. It might be simpler
+
+            # XXX we need some "stop at" argument to 'successorssets' to
+            # XXX prevent iterating too far. But pushing obsolete changeset
+            # XXX will requires --force anyway.
+            sucsets = obsolete.successorssets(unfi, current)
+            # print '  =', short(current), [tuple(short(s) for s in ss) for ss in sucsets]
             if sucsets == []: # pruned
-                discarded.add(nh)
+                # XXX possibly, the prune markers is further away and rooted on
+                # some changeset we do not push
+
+                # print '  - pruned'
+                # XXX if current is unknown, we should request a pull
+                if current in unfi:
+                    for p in unfi[current].parents():
+                        pnode = p.node()
+                        if pnode not in seen:
+                            seen.add(pnode)
+                            stack.append(pnode)
+                continue
+            for ss in sucsets:
+                if ss == (current,): # changeset is alive
+                    # print '  < alive'
+                    newhs.add(nh)
+                    break
+                for suc in ss:
+                    # XXX we should do something special for split
+                    # split -> 1 < len(ss)
+                    if suc in futurecommon:
+                        # print '  - superceeded (%s)' % (current in repo)
+                        if current in unfi:
+                            for p in unfi[current].parents():
+                                pnode = p.node()
+                                if pnode not in seen:
+                                    seen.add(pnode)
+                                    stack.append(pnode)
+                        # XXX if current is unknown, we should request a pull
+                        break
+                else:
+                    continue
+                break # propagate the break
             else:
                 for ss in sucsets:
                     for suc in ss:
@@ -231,4 +304,5 @@
                     break # propagate the break
                 else:
                     newhs.add(nh)
+    newhs |= unknownheads
     return newhs, discarded
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-partial-C1.t	Wed Mar 29 15:48:27 2017 +0200
@@ -0,0 +1,79 @@
+====================================
+Testing head checking code: Case C-1
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category C: checking case were the branch is only partially obsoleted.
+TestCase 1: 2 changeset branch, only the head is rewritten
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * 1 new changesets branches superceeding only the head of the old one
+.. * base of the old branch is still alive
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B'
+..     | |
+..   A ○ |
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  25c56d33e4c4 (draft): B1
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | o  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 25c56d33e4c4!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-partial-C2.t	Wed Mar 29 15:48:27 2017 +0200
@@ -0,0 +1,79 @@
+====================================
+Testing head checking code: Case C-2
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category C: checking case were the branch is only partially obsoleted.
+TestCase 2: 2 changeset branch, only the base is rewritten
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * 1 new changesets branches superceeding only the base of the old one
+.. * The old branch is still alive (base is obsolete, head is alive)
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   B ○
+..     |
+..   A ø⇠◔ A'
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg log -G --hidden
+  @  f6082bc4ffef (draft): A1
+  |
+  | o  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(A1)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head f6082bc4ffef!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-partial-C3.t	Wed Mar 29 15:48:27 2017 +0200
@@ -0,0 +1,78 @@
+====================================
+Testing head checking code: Case C-3
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category C: checking case were the branch is only partially obsoleted.
+TestCase 3: 2 changeset branch, only the head is pruned
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * old head is pruned
+.. * 1 new unrelated branch
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   B ⊗
+..     |
+..   A ◔ ◔ C
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ hg debugobsolete `getid "desc(B0)"`
+  $ hg log -G --hidden
+  @  0f88766e02d6 (draft): C0
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | o  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 0f88766e02d6!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-partial-C4.t	Wed Mar 29 15:48:27 2017 +0200
@@ -0,0 +1,78 @@
+====================================
+Testing head checking code: Case C-4
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category C: checking case were the branch is only partially obsoleted.
+TestCase 4: 2 changeset branch, only the base is pruned
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * old base is pruned
+.. * 1 new unrelated branch
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   B ◔
+..     |
+..   A ⊗ ◔ C
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ hg debugobsolete `getid "desc(A0)"`
+  $ hg log -G --hidden
+  @  0f88766e02d6 (draft): C0
+  |
+  | o  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(C0)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 0f88766e02d6!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]