changeset 133:2242dd1163c6

hg_delta_editor: fix bad parent revision calculation in the case of a branch recycling a name. Also implemented marking branches as closed in both replay and stupid paths.
author Augie Fackler <durin42@gmail.com>
date Wed, 10 Dec 2008 14:29:05 -0600
parents 3a9d6cd18332
children 22248b34b15a
files fetch_command.py hg_delta_editor.py tests/fixtures/branch_rename_to_trunk.sh tests/fixtures/branch_rename_to_trunk.svndump tests/test_fetch_branches.py tests/test_tags.py
diffstat 6 files changed, 408 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/fetch_command.py
+++ b/fetch_command.py
@@ -32,8 +32,8 @@ def fetch_revisions(ui, svn_url, hg_repo
     merc_util._encoding = 'UTF-8'
     skipto_rev=int(skipto_rev)
     have_replay = not stupid
-    if have_replay and not callable(delta.svn_txdelta_apply(None, None,
-                                                            None)[0]): #pragma: no cover
+    if have_replay and not callable(
+        delta.svn_txdelta_apply(None, None, None)[0]): #pragma: no cover
         ui.status('You are using old Subversion SWIG bindings. Replay will not'
                   ' work until you upgrade to 1.5.0 or newer. Falling back to'
                   ' a slower method that may be buggier. Please upgrade, or'
@@ -124,7 +124,7 @@ def replay_convert_rev(hg_editor, svn, r
             if p[-1] == '/':
                 dirpath = p[len(rootpath):]
                 files_to_grab.update((dirpath + f for f,k in
-                                      svn.list_files(dirpath, r.revnum) 
+                                      svn.list_files(dirpath, r.revnum)
                                       if k == 'f'))
             else:
                 files_to_grab.add(p[len(rootpath):])
@@ -176,7 +176,7 @@ def mempatchproxy(parentctx, files):
     class mempatch(patchfile):
         def __init__(self, ui, fname, opener, missing=False):
             patchfile.__init__(self, ui, fname, None, False)
-            
+
         def readlines(self, fname):
             if fname not in parentctx:
                 raise IOError('Cannot find %r to patch' % fname)
@@ -328,7 +328,7 @@ def stupid_diff_branchrev(ui, svn, hg_ed
                     data = data[len('link '):]
             elif path in parentctx:
                 data = parentctx[path].data()
-            
+
         copied = copies.get(path)
         return context.memfilectx(path=path, data=data, islink=islink,
                                   isexec=isexe, copied=copied)
@@ -477,17 +477,19 @@ def stupid_fetch_branchrev(svn, hg_edito
 def stupid_svn_server_pull_rev(ui, svn, hg_editor, r):
     # this server fails at replay
     branches = hg_editor.branches_in_paths(r.paths)
+    deleted_branches = {}
+    date = r.date.replace('T', ' ').replace('Z', '').split('.')[0]
+    date += ' -0000'
     for b in branches:
         parentctx = hg_editor.repo[hg_editor.get_parent_revision(r.revnum, b)]
         kind = svn.checkpath(branches[b], r.revnum)
         if kind != 'd':
             # Branch does not exist at this revision. Get parent revision and
             # remove everything.
-            files_touched = parentctx.manifest().keys()
-            def filectxfn(repo, memctx, path):
-                raise IOError()
+            deleted_branches[b] = parentctx.node()
+            continue
         else:
-            try:            
+            try:
                 files_touched, filectxfn = stupid_diff_branchrev(
                     ui, svn, hg_editor, b, r, parentctx)
             except BadPatchApply, e:
@@ -496,8 +498,6 @@ def stupid_svn_server_pull_rev(ui, svn, 
                 files_touched, filectxfn = stupid_fetch_branchrev(
                     svn, hg_editor, b, branches[b], r, parentctx)
 
-        date = r.date.replace('T', ' ').replace('Z', '').split('.')[0]
-        date += ' -0000'
         extra = {}
         if b:
             extra['branch'] = b
@@ -522,6 +522,31 @@ def stupid_svn_server_pull_rev(ui, svn, 
             hg_editor._save_metadata()
             ui.status('committed as %s on branch %s\n' %
                       (node.hex(ha),  b or 'default'))
+    for b, parent in deleted_branches.iteritems():
+        if parent == node.nullid:
+            continue
+        parentctx = hg_editor.repo[parent]
+        if parentctx.children():
+            continue
+        files_touched = parentctx.manifest().keys()
+        def filectxfn(repo, memctx, path):
+            raise IOError()
+        closed = node.nullid
+        if 'closed-branches' in hg_editor.repo.branchtags():
+            closed = hg_editor.repo['closed-branches'].node()
+        parents = (parent, closed)
+        current_ctx = context.memctx(hg_editor.repo,
+                                     parents,
+                                     r.message or '...',
+                                     files_touched,
+                                     filectxfn,
+                                     '%s%s' % (r.author,
+                                               hg_editor.author_host),
+                                     date,
+                                     {'branch': 'closed-branches'})
+        ha = hg_editor.repo.commitctx(current_ctx)
+        ui.status('Marked branch %s as closed.' % (b or 'default'))
+        hg_editor._save_metadata()
 
 class BadPatchApply(Exception):
     pass
--- a/hg_delta_editor.py
+++ b/hg_delta_editor.py
@@ -107,11 +107,11 @@ class HgChangeReceiver(delta.Editor):
         self.author_host = author_host
 
     def __setup_repo(self, repo_path):
-        '''Verify the repo is going to work out for us.
+        """Verify the repo is going to work out for us.
 
         This method will fail an assertion if the repo exists but doesn't have
         the Subversion metadata.
-        '''
+        """
         if os.path.isdir(repo_path) and len(os.listdir(repo_path)):
             self.repo = hg.repository(self.ui, repo_path)
             assert os.path.isfile(self.revmap_file)
@@ -141,6 +141,7 @@ class HgChangeReceiver(delta.Editor):
         self.missing_plaintexts = set()
         self.commit_branches_empty = {}
         self.base_revision = None
+        self.branches_to_delete = set()
 
     def _save_metadata(self):
         '''Save the Subversion metadata. This should really be called after
@@ -164,11 +165,11 @@ class HgChangeReceiver(delta.Editor):
         return self._split_branch_path(path)[:2]
 
     def _split_branch_path(self, path):
-        '''Figure out which branch inside our repo this path represents, and
+        """Figure out which branch inside our repo this path represents, and
         also figure out which path inside that branch it is.
 
         Raises an exception if it can't perform its job.
-        '''
+        """
         path = self._normalize_path(path)
         if path.startswith('trunk'):
             p = path[len('trunk'):]
@@ -186,8 +187,8 @@ class HgChangeReceiver(delta.Editor):
         return None, None, None
 
     def set_current_rev(self, rev):
-        '''Set the revision we're currently converting.
-        '''
+        """Set the revision we're currently converting.
+        """
         self.current_rev = rev
 
     def set_file(self, path, data, isexec=False, islink=False):
@@ -236,19 +237,23 @@ class HgChangeReceiver(delta.Editor):
                 continue
             if num <= number and num > real_num:
                 real_num = num
-        if real_num == 0:
-            if branch in self.branches:
-                parent_branch = self.branches[branch][0]
-                parent_branch_rev = self.branches[branch][1]
-                if parent_branch_rev <= 0:
-                    return None, None
-                branch_created_rev = self.branches[branch][2]
-                if parent_branch == 'trunk':
-                    parent_branch = None
-                if branch_created_rev <= number+1 and branch != parent_branch:
-                    return self.get_parent_svn_branch_and_rev(
-                                                    parent_branch_rev+1,
-                                                    parent_branch)
+        if branch in self.branches:
+            parent_branch = self.branches[branch][0]
+            parent_branch_rev = self.branches[branch][1]
+            # check to see if this branch already existed and is the same
+            if parent_branch_rev < real_num:
+                return real_num, branch
+            # if that wasn't true, then this is the a new branch with the
+            # same name as some old deleted branch
+            if parent_branch_rev <= 0 and real_num == 0:
+                return None, None
+            branch_created_rev = self.branches[branch][2]
+            if parent_branch == 'trunk':
+                parent_branch = None
+            if branch_created_rev <= number+1 and branch != parent_branch:
+                return self.get_parent_svn_branch_and_rev(
+                                                parent_branch_rev+1,
+                                                parent_branch)
         if real_num != 0:
             return real_num, branch
         return None, None
@@ -265,12 +270,12 @@ class HgChangeReceiver(delta.Editor):
         paths = revision.paths
         added_branches = {}
         added_tags = {}
+        self.branches_to_delete = set()
         tags_to_delete = set()
-        branches_to_delete = set()
         for p in sorted(paths):
             fi, br = self._path_and_branch_for_path(p)
             if fi is not None:
-                if fi == '' and br not in self.branches:
+                if fi == '' and paths[p].action != 'D':
                     src_p = paths[p].copyfrom_path
                     src_rev = paths[p].copyfrom_rev
                     src_tag = self._is_path_tag(src_p)
@@ -294,7 +299,7 @@ class HgChangeReceiver(delta.Editor):
                 elif fi == '' and br in self.branches:
                     br2 = br or 'default'
                     if br2 not in self.repo.branchtags() and paths[p].action == 'D':
-                        branches_to_delete.add(br)
+                        self.branches_to_delete.add(br)
             else:
                 t_name = self._is_path_tag(p)
                 if t_name == False:
@@ -320,7 +325,7 @@ class HgChangeReceiver(delta.Editor):
                         tags_to_delete.add(t_name)
         for t in tags_to_delete:
             del self.tags[t]
-        for br in branches_to_delete:
+        for br in self.branches_to_delete:
             del self.branches[br]
         self.tags.update(added_tags)
         self.branches.update(added_branches)
@@ -359,9 +364,12 @@ class HgChangeReceiver(delta.Editor):
             parents = (self.get_parent_revision(rev.revnum, branch),
                        revlog.nullid)
             if branch is not None:
-                if branch not in self.branches and branch not in self.repo.branchtags():
+                if (branch not in self.branches
+                    and branch not in self.repo.branchtags()):
                     continue
                 extra['branch'] = branch
+            if (branch in self.branches_to_delete):
+                continue
             parent_ctx = self.repo.changectx(parents[0])
             def filectxfn(repo, memctx, path):
                 current_file = files[path]
@@ -396,6 +404,29 @@ class HgChangeReceiver(delta.Editor):
             if (rev.revnum, branch) not in self.revmap:
                 self.add_to_revmap(rev.revnum, branch, new_hash)
         # now we handle branches that need to be committed without any files
+        for branch in self.branches_to_delete:
+            closed = revlog.nullid
+            if 'closed-branches' in self.repo.branchtags():
+                closed = self.repo['closed-branches'].node()
+            ha = self.get_parent_revision(rev.revnum, branch)
+            parentctx = self.repo.changectx(ha)
+            if parentctx.children():
+                continue
+            parents = (ha, closed)
+            def del_all_files(*args):
+                raise IOError
+            files = parentctx.manifest().keys()
+            current_ctx = context.memctx(self.repo,
+                                         parents,
+                                         rev.message or ' ',
+                                         files,
+                                         del_all_files,
+                                         '%s%s' % (rev.author,
+                                                   self.author_host),
+                                         date,
+                                         {'branch': 'closed-branches'})
+            new_hash = self.repo.commitctx(current_ctx)
+            self.ui.status('Marked branch %s as closed.' % (branch or 'default'))
         for branch in self.commit_branches_empty:
             ha = self.get_parent_revision(rev.revnum, branch)
             if ha == node.nullid:
@@ -404,8 +435,11 @@ class HgChangeReceiver(delta.Editor):
             def del_all_files(*args):
                 raise IOError
             extra = {}
-            if branch:
-                extra['branch'] = branch
+            if parent_ctx.children():
+                # Target isn't an active head, no need to do things to it.
+                continue
+            if branch in self.branches_to_delete:
+                extra['branch'] = 'closed-branch'
             # True here means nuke all files
             files = []
             if self.commit_branches_empty[branch]:
@@ -468,6 +502,8 @@ class HgChangeReceiver(delta.Editor):
     @stash_exception_on_self
     def delete_entry(self, path, revision_bogus, parent_baton, pool=None):
         br_path, branch = self._path_and_branch_for_path(path)
+        if br_path == '':
+            self.branches_to_delete.add(branch)
         if br_path is not None:
             ha = self.get_parent_revision(self.current_rev.revnum, branch)
             if ha == revlog.nullid:
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/branch_rename_to_trunk.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+mkdir temp
+cd temp
+svnadmin create repo
+svn co file://`pwd`/repo wc
+cd wc
+mkdir branches trunk tags
+svn add *
+svn ci -m 'btt'
+cd trunk
+for a in alpha beta gamma delta ; do
+    echo $a > $a
+    svn add $a
+done
+svn ci -m 'Add files.'
+cd ..
+svn up
+svn cp trunk branches/dev_branch
+svn ci -m 'branch'
+cd branches/dev_branch
+svn rm delta
+echo narf > alpha
+echo iota > iota
+svn add iota
+svn ci -m 'branch changes'
+cd ../..
+svn up
+svn mv trunk branches/old_trunk
+svn ci -m 'move trunk to a branch'
+svn up
+svn mv branches/dev_branch trunk
+svn ci -m 'move dev to trunk'
+cd ..
+cd ..
+svnadmin dump temp/repo > branch_rename_to_trunk.svndump
+echo
+echo 'Complete.'
+echo 'You probably want to clean up temp now.'
+echo 'Dump in branch_rename_to_trunk.svndump'
+exit 0
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/branch_rename_to_trunk.svndump
@@ -0,0 +1,252 @@
+SVN-fs-dump-format-version: 2
+
+UUID: f6ca240b-44b4-4753-9ebe-569095e6ee32
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2008-12-10T21:37:23.126889Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 102
+Content-length: 102
+
+K 7
+svn:log
+V 3
+btt
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2008-12-10T21:37:24.068831Z
+PROPS-END
+
+Node-path: branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 110
+Content-length: 110
+
+K 7
+svn:log
+V 10
+Add files.
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2008-12-10T21:37:25.115468Z
+PROPS-END
+
+Node-path: trunk/alpha
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 6
+Text-content-md5: 9f9f90dbe3e5ee1218c86b8839db1995
+Content-length: 16
+
+PROPS-END
+alpha
+
+
+Node-path: trunk/beta
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 5
+Text-content-md5: f0cf2a92516045024a0c99147b28f05b
+Content-length: 15
+
+PROPS-END
+beta
+
+
+Node-path: trunk/delta
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 6
+Text-content-md5: d2840cc81bc032bd1141b56687d0f93c
+Content-length: 16
+
+PROPS-END
+delta
+
+
+Node-path: trunk/gamma
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 6
+Text-content-md5: 303febb9068384eca46b5b6516843b35
+Content-length: 16
+
+PROPS-END
+gamma
+
+
+Revision-number: 3
+Prop-content-length: 105
+Content-length: 105
+
+K 7
+svn:log
+V 6
+branch
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2008-12-10T21:37:28.044281Z
+PROPS-END
+
+Node-path: branches/dev_branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk
+
+
+Revision-number: 4
+Prop-content-length: 114
+Content-length: 114
+
+K 7
+svn:log
+V 14
+branch changes
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2008-12-10T21:37:29.085784Z
+PROPS-END
+
+Node-path: branches/dev_branch/alpha
+Node-kind: file
+Node-action: change
+Text-content-length: 5
+Text-content-md5: 5e723ed52db2000686425ca28bc5ba4a
+Content-length: 5
+
+narf
+
+
+Node-path: branches/dev_branch/iota
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 5
+Text-content-md5: ebcf3971120220589f1dfbf8d56e25b9
+Content-length: 15
+
+PROPS-END
+iota
+
+
+Node-path: branches/dev_branch/delta
+Node-action: delete
+
+
+Revision-number: 5
+Prop-content-length: 122
+Content-length: 122
+
+K 7
+svn:log
+V 22
+move trunk to a branch
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2008-12-10T21:37:32.056153Z
+PROPS-END
+
+Node-path: branches/old_trunk
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 4
+Node-copyfrom-path: trunk
+
+
+Node-path: trunk
+Node-action: delete
+
+
+Revision-number: 6
+Prop-content-length: 117
+Content-length: 117
+
+K 7
+svn:log
+V 17
+move dev to trunk
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2008-12-10T21:37:35.046793Z
+PROPS-END
+
+Node-path: branches/dev_branch
+Node-action: delete
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 5
+Node-copyfrom-path: branches/dev_branch
+
+
--- a/tests/test_fetch_branches.py
+++ b/tests/test_fetch_branches.py
@@ -1,6 +1,8 @@
 import sys
 import unittest
 
+from mercurial import node
+
 import test_util
 
 
@@ -28,6 +30,17 @@ class TestFetchBranches(test_util.TestBa
 
     def test_unorderedbranch_stupid(self):
         self.test_unorderedbranch(True)
+        
+    def test_renamed_branch_to_trunk(self, stupid=False):
+        repo = self._load_fixture_and_fetch('branch_rename_to_trunk.svndump', 
+                                            stupid)
+        self.assertEqual(node.hex(repo['tip'].node()),
+                         'b479347c1f56d1fafe5e32a7ce0d1b7099637784')
+        self.assertEqual(repo['tip'].parents()[0].branch(), 'dev_branch')
+        self.assertEqual(repo['old_trunk'].parents()[0].branch(), 'default')
+
+    def test_renamed_branch_to_trunk_stupid(self):
+        self.test_renamed_branch_to_trunk(stupid=True)
 
 def suite():
     all = [unittest.TestLoader().loadTestsFromTestCase(TestFetchBranches),
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -77,8 +77,12 @@ class TestTags(test_util.TestBase):
         repo = self._load_fixture_and_fetch('tag_by_rename_branch.svndump',
                                             stupid=stupid)
         repo = self.getrepo()
+        self.assertEqual(repo['tip'], repo['closed-branches'])
         self.assertEqual(node.hex(repo['tip'].node()),
-                         '1b941f92acc343939274bd8bbf25984fa9706bb9')
+                         'dd2dccc3180631192f058468ec7215899223a6d8')
+        taggedrev = repo['tip'].parents()[0]
+        self.assertEqual(node.hex(taggedrev.node()),
+                         '68f5f7d82b00a2efe3aca28b615ebab98235d55f')
         self.assertEqual(node.hex(repo['tag/dummy'].node()),
                          '68f5f7d82b00a2efe3aca28b615ebab98235d55f')