changeset 7233:9f0e52e1df77

fix pull racing with push/commit (issue1320) changegroup() has a problem when nodes which does not descend from a node in <bases> are added to remote after the discovery phase. If that happens, changegroup() won't send the correct set of nodes, ie. some nodes will be missing. To correct it we have to find the set of nodes that both remote and self have (called <common>), and send all the nodes not in <common>. This fix has some overhead, in the worst case it will re-send a whole branch. A proper fix to avoid this overhead might be to change the protocol so that the <common> nodes are sent (instead of the <bases> of the missing nodes).
author Benoit Boissinot <benoit.boissinot@ens-lyon.org>
date Tue, 21 Oct 2008 17:00:35 +0200
parents c2ac09f81ec9
children ae70fe6143fc
files mercurial/localrepo.py mercurial/revlog.py tests/test-clone-cgi.out tests/test-fetch.out tests/test-hgweb-commands.out tests/test-push-warn.out
diffstat 6 files changed, 65 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/localrepo.py	Thu Oct 23 23:03:09 2008 +0200
+++ b/mercurial/localrepo.py	Tue Oct 21 17:00:35 2008 +0200
@@ -1578,6 +1578,20 @@
         the linkrev.
         """
 
+        if extranodes is None:
+            # can we go through the fast path ?
+            heads.sort()
+            allheads = self.heads()
+            allheads.sort()
+            if heads == allheads:
+                common = []
+                # parents of bases are known from both sides
+                for n in bases:
+                    for p in self.changelog.parents(n):
+                        if p != nullid:
+                            common.append(p)
+                return self._changegroup(common, source)
+
         self.hook('preoutgoing', throw=True, source=source)
 
         # Set up some initial variables
@@ -1854,16 +1868,22 @@
         return util.chunkbuffer(gengroup())
 
     def changegroup(self, basenodes, source):
+        # to avoid a race we use changegroupsubset() (issue1320)
+        return self.changegroupsubset(basenodes, self.heads(), source)
+
+    def _changegroup(self, common, source):
         """Generate a changegroup of all nodes that we have that a recipient
         doesn't.
 
         This is much easier than the previous function as we can assume that
-        the recipient has any changenode we aren't sending them."""
+        the recipient has any changenode we aren't sending them.
+
+        common is the set of common nodes between remote and self"""
 
         self.hook('preoutgoing', throw=True, source=source)
 
         cl = self.changelog
-        nodes = cl.nodesbetween(basenodes, None)[0]
+        nodes = cl.findmissing(common)
         revset = dict.fromkeys([cl.rev(n) for n in nodes])
         self.changegroupinfo(nodes, source)
 
--- a/mercurial/revlog.py	Thu Oct 23 23:03:09 2008 +0200
+++ b/mercurial/revlog.py	Tue Oct 21 17:00:35 2008 +0200
@@ -590,6 +590,46 @@
                     yield i
                     break
 
+    def findmissing(self, common=None, heads=None):
+        '''
+        returns the topologically sorted list of nodes from the set:
+        missing = (ancestors(heads) \ ancestors(common))
+
+        where ancestors() is the set of ancestors from heads, heads included
+
+        if heads is None, the heads of the revlog are used
+        if common is None, nullid is assumed to be a common node
+        '''
+        if common is None:
+            common = [nullid]
+        if heads is None:
+            heads = self.heads()
+
+        common = [self.rev(n) for n in common]
+        heads = [self.rev(n) for n in heads]
+
+        # we want the ancestors, but inclusive
+        has = dict.fromkeys(self.ancestors(*common))
+        has[nullrev] = None
+        for r in common:
+            has[r] = None
+
+        # take all ancestors from heads that aren't in has
+        missing = {}
+        visit = [r for r in heads if r not in has]
+        while visit:
+            r = visit.pop(0)
+            if r in missing:
+                continue
+            else:
+                missing[r] = None
+                for p in self.parentrevs(r):
+                    if p not in has:
+                        visit.append(p)
+        missing = missing.keys()
+        missing.sort()
+        return [self.node(r) for r in missing]
+
     def nodesbetween(self, roots=None, heads=None):
         """Return a tuple containing three elements. Elements 1 and 2 contain
         a final list bases and heads after all the unreachable ones have been
--- a/tests/test-clone-cgi.out	Thu Oct 23 23:03:09 2008 +0200
+++ b/tests/test-clone-cgi.out	Tue Oct 21 17:00:35 2008 +0200
@@ -2,4 +2,4 @@
 adding a
 % try hgweb request
 0
-54086fe9a47b47d83204f38bda0b90c2  page1
+1f424bb22ec05c3c6bc866b6e67efe43  page1
--- a/tests/test-fetch.out	Thu Oct 23 23:03:09 2008 +0200
+++ b/tests/test-fetch.out	Tue Oct 21 17:00:35 2008 +0200
@@ -100,7 +100,7 @@
 adding changesets
 adding manifests
 adding file changes
-added 1 changesets with 1 changes to 1 files
+added 1 changesets with 1 changes to 2 files
 % parent should be 2 (no automatic update)
 2
 
Binary file tests/test-hgweb-commands.out has changed
--- a/tests/test-push-warn.out	Thu Oct 23 23:03:09 2008 +0200
+++ b/tests/test-push-warn.out	Tue Oct 21 17:00:35 2008 +0200
@@ -22,7 +22,7 @@
 adding changesets
 adding manifests
 adding file changes
-added 2 changesets with 1 changes to 1 files
+added 2 changesets with 1 changes to 2 files
 adding foo
 updating working directory
 1 files updated, 0 files merged, 0 files removed, 0 files unresolved