changeset 691:a45365f1492a

push: fix case where we get >1 revs back after svn commit This hinges on looking not only at children(), but at descendants() to find things.
author Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
date Wed, 08 Sep 2010 10:43:02 +0200
parents 4b55fb6d6847
children 42879fddf727
files hgsubversion/wrappers.py tests/test_single_dir_clone.py
diffstat 2 files changed, 65 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -141,41 +141,52 @@ def push(repo, dest, force, revs):
         ui.status('no changes found\n')
         return 1 # so we get a sane exit status, see hg's commands.push
     while outgoing:
+
+        # 2. Commit oldest revision that needs to be pushed
         oldest = outgoing.pop(-1)
         old_ctx = repo[oldest]
-        if len(old_ctx.parents()) != 1:
+        old_pars = old_ctx.parents()
+        if len(old_pars) != 1:
             ui.status('Found a branch merge, this needs discussion and '
                       'implementation.\n')
             return 0 # results in nonzero exit status, see hg's commands.py
-        base_n = old_ctx.parents()[0].node()
-        old_children = repo[base_n].children()
-        svnbranch = repo[base_n].branch()
-        oldtip = base_n
-        samebranchchildren = [c for c in repo[oldtip].children() if c.branch() == svnbranch
+        # We will commit to svn against this node's parent rev. Any file-level
+        # conflicts here will result in an error reported by svn.
+        base_ctx = old_pars[0]
+        base_revision = hashes[base_ctx.node()][0]
+        svnbranch = base_ctx.branch()
+        # Find most recent svn commit we have on this branch.
+        # This node will become the nearest known ancestor of the pushed rev.
+        oldtipctx = base_ctx
+        old_children = oldtipctx.descendants()
+        seen = set(c.node() for c in old_children)
+        samebranchchildren = [c for c in old_children if c.branch() == svnbranch
                               and c.node() in hashes]
-        while samebranchchildren:
-            oldtip = samebranchchildren[0].node()
-            samebranchchildren = [c for c in repo[oldtip].children() if c.branch() == svnbranch
-                                  and c.node() in hashes]
-        # 2. Commit oldest revision that needs to be pushed
-        base_revision = hashes[base_n][0]
+        if samebranchchildren:
+            # The following relies on descendants being sorted by rev.
+            oldtipctx = samebranchchildren[-1]
+        # All set, so commit now.
         try:
             pushmod.commit(ui, repo, old_ctx, meta, base_revision, svn)
         except pushmod.NoFilesException:
             ui.warn("Could not push revision %s because it had no changes in svn.\n" %
                      old_ctx)
             return 1
+
         # 3. Fetch revisions from svn
         # TODO: this probably should pass in the source explicitly - rev too?
         r = repo.pull(dest, force=force)
         assert not r or r == 0
+
         # 4. Find the new head of the target branch
-        oldtipctx = repo[oldtip]
-        replacement = [c for c in oldtipctx.children() if c not in old_children
-                       and c.branch() == oldtipctx.branch()]
-        assert len(replacement) == 1, 'Replacement node came back as: %r' % replacement
-        replacement = replacement[0]
-        # 5. Rebase all children of the currently-pushing rev to the new branch
+        # We expect to get our own new commit back, but we might also get other
+        # commits that happened since our last pull, or even right after our own
+        # commit (race).
+        for c in oldtipctx.descendants():
+            if c.node() not in seen and c.branch() == svnbranch:
+                newtipctx = c
+
+        # 5. Rebase all children of the currently-pushing rev to the new head
         heads = repo.heads(old_ctx.node())
         for needs_transplant in heads:
             def extrafn(ctx, extra):
@@ -185,8 +196,12 @@ def push(repo, dest, force, revs):
             # TODO: can we avoid calling our own rebase wrapper here?
             rebase(hgrebase.rebase, ui, repo, svn=True, svnextrafn=extrafn,
                    svnsourcerev=needs_transplant)
+            # Reload the repo after the rebase. Do not reuse contexts across this.
+            newtip = newtipctx.node()
             repo = hg.repository(ui, meta.path)
-            for child in repo[replacement.node()].children():
+            newtipctx = repo[newtip]
+            # Rewrite the node ids in outgoing to their rebased versions.
+            for child in newtipctx.children():
                 rebasesrc = node.bin(child.extra().get('rebase_source', node.hex(node.nullid)))
                 if rebasesrc in outgoing:
                     while rebasesrc in outgoing:
--- a/tests/test_single_dir_clone.py
+++ b/tests/test_single_dir_clone.py
@@ -134,6 +134,37 @@ class TestSingleDir(test_util.TestBase):
         self.assertEqual(self.repo['tip']['bogus'].data(),
                          'contents of bogus')
 
+    def test_push_single_dir_one_incoming_and_two_outgoing(self):
+        # Tests simple pushing from default branch to a single dir repo
+        # Pushes two outgoing over one incoming svn rev
+        # (used to cause an "unknown revision")
+        # This can happen if someone committed to svn since our last pull (race).
+        repo = self._load_fixture_and_fetch('branch_from_tag.svndump',
+                                            stupid=False,
+                                            layout='single',
+                                            subdir='trunk')
+        self._add_svn_rev({'trunk/alpha': 'Changed'})
+        def file_callback(repo, memctx, path):
+            return context.memfilectx(path=path,
+                                      data='data of %s' % path,
+                                      islink=False,
+                                      isexec=False,
+                                      copied=False)
+        for fn in ['one', 'two']:
+            ctx = context.memctx(repo,
+                                 (repo['tip'].node(), node.nullid),
+                                 'automated test',
+                                 [fn],
+                                 file_callback,
+                                 'an_author',
+                                 '2009-10-19 18:49:30 -0500',
+                                 {'branch': 'default',})
+            repo.commitctx(ctx)
+        hg.update(repo, repo['tip'].node())
+        self.pushrevisions(expected_extra_back=1)
+        self.assertTrue('trunk/one' in self.svnls(''))
+        self.assertTrue('trunk/two' in self.svnls(''))
+
     def test_push_single_dir_branch(self):
         # Tests local branches pushing to a single dir repo. Creates a fork at
         # tip. The default branch adds a file called default, while branch foo