changeset 447:0d3b5acb1d51

tags: handle edits to tags as gracefully as possible
author Augie Fackler <durin42@gmail.com>
date Tue, 23 Jun 2009 21:38:27 -0500 (2009-06-24)
parents cbd230043379
children fbc7cf4fd701
files hgsubversion/editor.py hgsubversion/replay.py hgsubversion/stupid.py hgsubversion/svncommands.py hgsubversion/svnmeta.py tests/fixtures/commit-to-tag.sh tests/fixtures/commit-to-tag.svndump tests/test_rebuildmeta.py tests/test_tags.py
diffstat 9 files changed, 495 insertions(+), 22 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/editor.py
+++ b/hgsubversion/editor.py
@@ -191,7 +191,8 @@ class HgEditor(delta.Editor):
         fpath, branch = self.meta.split_branch_path(path, existing=False)[:2]
         if not fpath:
             return
-        if branch not in self.meta.branches:
+        if (branch not in self.meta.branches and
+            not self.meta.is_path_tag(self.meta.remotename(branch))):
             # we know this branch will exist now, because it has at least one file. Rock.
             self.meta.branches[branch] = None, 0, self.current.rev.revnum
         self.current.file = path
--- a/hgsubversion/replay.py
+++ b/hgsubversion/replay.py
@@ -111,13 +111,22 @@ def convert_rev(ui, meta, svn, r, tbdelt
             continue
 
         extra = meta.genextra(rev.revnum, branch)
+        tag = False
         if branch is not None:
-            if (branch not in meta.branches
-                and branch not in meta.repo.branchtags()):
+            tag = meta.is_path_tag(meta.remotename(branch))
+            if (not (tag and tag in meta.tags) and
+                (branch not in meta.branches
+                and branch not in meta.repo.branchtags())):
                 continue
 
-        parent_ctx = meta.repo.changectx(parents[0])
-        if '.hgsvnexternals' not in parent_ctx and '.hgsvnexternals' in files:
+        parentctx = meta.repo.changectx(parents[0])
+        if tag:
+            if parentctx.node() == node.nullid:
+                continue
+            extra.update({'branch': parentctx.extra().get('branch', None),
+                          'close': 1})
+
+        if '.hgsvnexternals' not in parentctx and '.hgsvnexternals' in files:
             # Do not register empty externals files
             if (files['.hgsvnexternals'] in current.files
                 and not current.files[files['.hgsvnexternals']]):
@@ -128,7 +137,7 @@ def convert_rev(ui, meta, svn, r, tbdelt
             if current_file in current.deleted:
                 raise IOError()
             copied = current.copies.get(current_file)
-            flags = parent_ctx.flags(path)
+            flags = parentctx.flags(path)
             is_exec = current.execfiles.get(current_file, 'x' in flags)
             is_link = current.symlinks.get(current_file, 'l' in flags)
             if current_file in current.files:
@@ -139,13 +148,13 @@ def convert_rev(ui, meta, svn, r, tbdelt
                     ui.warn('file marked as link, but contains data: '
                             '%s (%r)\n' % (current_file, flags))
             else:
-                data = parent_ctx.filectx(path).data()
+                data = parentctx.filectx(path).data()
             return context.memfilectx(path=path,
                                       data=data,
                                       islink=is_link, isexec=is_exec,
                                       copied=copied)
 
-        if not meta.usebranchnames:
+        if not meta.usebranchnames or extra.get('branch', None) == 'default':
             extra.pop('branch', None)
         current_ctx = context.memctx(meta.repo,
                                      parents,
@@ -158,8 +167,10 @@ def convert_rev(ui, meta, svn, r, tbdelt
 
         new_hash = meta.repo.commitctx(current_ctx)
         util.describe_commit(ui, new_hash, branch)
-        if (rev.revnum, branch) not in meta.revmap:
+        if (rev.revnum, branch) not in meta.revmap and not tag:
             meta.revmap[rev.revnum, branch] = new_hash
+        if tag:
+            meta.movetag(tag, new_hash, parentctx.extra().get('branch', None), rev, date)
 
     # 2. handle branches that need to be committed without any files
     for branch in current.emptybranches:
@@ -178,6 +189,7 @@ def convert_rev(ui, meta, svn, r, tbdelt
                                'Please report this issue.')
 
         extra = meta.genextra(rev.revnum, branch)
+
         if not meta.usebranchnames:
             extra.pop('branch', None)
 
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -601,9 +601,18 @@ def convert_rev(ui, meta, svn, r, tbdelt
                 assert f[0] != '/'
 
         extra = meta.genextra(r.revnum, b)
-        if not meta.usebranchnames:
-            extra.pop('branch', None)
 
+        tag = False
+        tag = meta.is_path_tag(meta.remotename(b))
+
+        if tag:
+            if parentctx.node() == node.nullid:
+                continue
+            extra.update({'branch': parentctx.extra().get('branch', None),
+                          'close': 1})
+
+        if not meta.usebranchnames or extra.get('branch', None) == 'default':
+            extra.pop('branch', None)
         current_ctx = context.memctx(meta.repo,
                                      [parentctx.node(), revlog.nullid],
                                      r.message or util.default_commit_msg,
@@ -615,10 +624,14 @@ def convert_rev(ui, meta, svn, r, tbdelt
         ha = meta.repo.commitctx(current_ctx)
 
         branch = extra.get('branch', None)
-        if not branch in meta.branches:
+        if (not branch in meta.branches
+            and not meta.is_path_tag(meta.remotename(branch))):
             meta.branches[branch] = None, 0, r.revnum
-        meta.revmap[r.revnum, b] = ha
+        if not tag:
+            meta.revmap[r.revnum, b] = ha
         util.describe_commit(ui, ha, b)
+        if tag:
+            meta.movetag(tag, ha, parentctx.extra().get('branch', None), r, date)
 
     # These are branches with an 'R' status in svn log. This means they were
     # replaced by some other branch, so we need to verify they get marked as closed.
--- a/hgsubversion/svncommands.py
+++ b/hgsubversion/svncommands.py
@@ -120,6 +120,10 @@ def rebuildmeta(ui, repo, hg_repo_path, 
             commitpath = commitpath[len('branches/'):]
         elif commitpath == 'trunk':
             commitpath = ''
+        elif commitpath.startswith('tags'):
+            if ctx.extra().get('close'):
+                continue
+            commitpath = '../' + commitpath
         else:
             assert False, 'Unhandled case in rebuildmeta'
         revmap.write('%s %s %s\n' % (revision, ctx.hex(), commitpath))
--- a/hgsubversion/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -145,7 +145,6 @@ class SVNMeta(object):
     def localname(self, path):
         """Compute the local name for a branch located at path.
         """
-        assert not path.startswith('tags/')
         if path == 'trunk':
             return None
         elif path.startswith('branches/'):
@@ -164,7 +163,10 @@ class SVNMeta(object):
         branchpath = 'trunk'
         if branch:
             extra['branch'] = branch
-            branchpath = 'branches/%s' % branch
+            if branch.startswith('../'):
+                branchpath = branch[3:]
+            else:
+                branchpath = 'branches/%s' % branch
 
         subdir = self.subdir
         if subdir and subdir[-1] == '/':
@@ -218,8 +220,16 @@ class SVNMeta(object):
         known.
         """
         path = self.normalize(path)
-        if path.startswith('tags/'):
-            return None, None, None
+        if self.is_path_tag(path):
+            tag = self.is_path_tag(path)
+            matched = [t for t in self.tags.iterkeys() if tag.startswith(t+'/')]
+            if not matched:
+                return None, None, None
+            matched.sort(cmp=lambda x,y: cmp(len(x),len(y)), reverse=True)
+            brpath = tag[len(matched[0])+1:]
+            svrpath = path[:-(len(brpath)+1)]
+            ln = self.localname(svrpath)
+            return brpath, ln, svrpath
         test = ''
         path_comps = path.split('/')
         while self.localname(test) not in self.branches and len(path_comps):
@@ -299,7 +309,11 @@ class SVNMeta(object):
     def get_parent_revision(self, number, branch):
         '''Get the parent revision hash for a commit on a specific branch.
         '''
-        r, br = self.get_parent_svn_branch_and_rev(number, branch)
+        tag = self.is_path_tag(self.remotename(branch))
+        if tag and tag in self.tags:
+            br, r = self.tags[tag]
+        else:
+            r, br = self.get_parent_svn_branch_and_rev(number, branch)
         if r is not None:
             return self.revmap[r, br]
         return revlog.nullid
@@ -398,6 +412,39 @@ class SVNMeta(object):
         self.tags.update(tbdelta['tags'][0])
         self.branches.update(tbdelta['branches'][0])
 
+    def movetag(self, tag, hash, branch, rev, date):
+        if branch == 'default':
+            branch = None
+        parentctx = self.repo[self.get_parent_revision(rev.revnum, branch)]
+        if '.hgtags' in parentctx:
+            tagdata = parentctx.filectx('.hgtags').data()
+        else:
+            tagdata = ''
+        tagdata += '%s %s\n' % (node.hex(hash), tag, )
+        def hgtagsfn(repo, memctx, path):
+            assert path == '.hgtags'
+            return context.memfilectx(path=path,
+                                      data=tagdata,
+                                      islink=False,
+                                      isexec=False,
+                                      copied=False)
+        pextra = parentctx.extra()
+        ctx = context.memctx(self.repo,
+                             (parentctx.node(), node.nullid),
+                             rev.message or '...',
+                             ['.hgtags', ],
+                             hgtagsfn,
+                             self.authors[rev.author],
+                             date,
+                             pextra)
+        new_hash = self.repo.commitctx(ctx)
+        prefix, revnum = pextra['convert_revision'].rsplit('@', 1)
+        branch = self.localname(self.normalize('/' + prefix.split('/', 1)[1]))
+        assert self.revmap[int(revnum), branch] == parentctx.node()
+        self.revmap[int(revnum), branch] = new_hash
+
+        util.describe_commit(self.ui, new_hash, branch)
+
     def committags(self, delta, rev, endbranches):
 
         date = self.fixdate(rev.date)
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/commit-to-tag.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+mkdir temp
+cd temp
+svnadmin create repo
+REPOPATH="file://`pwd`/repo"
+svn co $REPOPATH wc
+cd wc
+mkdir -p branches/magic trunk tags
+svn add *
+svn ci -m 'btt'
+cd branches/magic
+for a in alpha beta gamma; do
+    echo $a > $a
+    svn add $a
+    svn ci -m "Add file $a"
+done
+cd ../..
+svn up
+svn cp $REPOPATH/branches/magic $REPOPATH/tags/will-edit -m 'Make tag to edit'
+svn up
+
+cd branches/magic
+for a in delta iota lambda; do
+    echo $a > $a
+    svn add $a
+    svn ci -m "Add file $a"
+done
+cd ../..
+
+cd tags/will-edit
+svn rm alpha
+svn ci -m 'removed alpha on a tag. Moves tag, implicit branch.'
+cd ../..
+
+cd branches/magic
+for a in omega; do
+    echo $a > $a
+    svn add $a
+    svn ci -m "Add file $a"
+done
+cd ../..
+
+cd ../..
+svnadmin dump temp/repo > commit-to-tag.svndump
+echo
+echo 'Complete.'
+echo 'You probably want to clean up temp now.'
+echo 'Dump in commit-to-tag.svndump'
+exit 0
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/commit-to-tag.svndump
@@ -0,0 +1,325 @@
+SVN-fs-dump-format-version: 2
+
+UUID: af82cc90-c2d2-43cd-b1aa-c8a78449440a
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2009-06-21T18:31:52.697525Z
+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
+2009-06-21T18:31:53.092074Z
+PROPS-END
+
+Node-path: branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: branches/magic
+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: 114
+Content-length: 114
+
+K 7
+svn:log
+V 14
+Add file alpha
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:31:54.113073Z
+PROPS-END
+
+Node-path: branches/magic/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
+
+
+Revision-number: 3
+Prop-content-length: 113
+Content-length: 113
+
+K 7
+svn:log
+V 13
+Add file beta
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:31:55.071743Z
+PROPS-END
+
+Node-path: branches/magic/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
+
+
+Revision-number: 4
+Prop-content-length: 114
+Content-length: 114
+
+K 7
+svn:log
+V 14
+Add file gamma
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:31:56.073236Z
+PROPS-END
+
+Node-path: branches/magic/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: 5
+Prop-content-length: 116
+Content-length: 116
+
+K 7
+svn:log
+V 16
+Make tag to edit
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:31:58.056126Z
+PROPS-END
+
+Node-path: tags/will-edit
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 4
+Node-copyfrom-path: branches/magic
+
+
+Revision-number: 6
+Prop-content-length: 114
+Content-length: 114
+
+K 7
+svn:log
+V 14
+Add file delta
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:31:59.064850Z
+PROPS-END
+
+Node-path: branches/magic/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
+
+
+Revision-number: 7
+Prop-content-length: 113
+Content-length: 113
+
+K 7
+svn:log
+V 13
+Add file iota
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:32:00.080479Z
+PROPS-END
+
+Node-path: branches/magic/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
+
+
+Revision-number: 8
+Prop-content-length: 115
+Content-length: 115
+
+K 7
+svn:log
+V 15
+Add file lambda
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:32:01.079819Z
+PROPS-END
+
+Node-path: branches/magic/lambda
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 7
+Text-content-md5: 8c8a4646591ee0d9a43d3149320ed577
+Content-length: 17
+
+PROPS-END
+lambda
+
+
+Revision-number: 9
+Prop-content-length: 151
+Content-length: 151
+
+K 7
+svn:log
+V 51
+removed alpha on a tag. Moves tag, implicit branch.
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:32:02.140778Z
+PROPS-END
+
+Node-path: tags/will-edit/alpha
+Node-action: delete
+
+
+Revision-number: 10
+Prop-content-length: 114
+Content-length: 114
+
+K 7
+svn:log
+V 14
+Add file omega
+K 10
+svn:author
+V 5
+durin
+K 8
+svn:date
+V 27
+2009-06-21T18:32:03.083676Z
+PROPS-END
+
+Node-path: branches/magic/omega
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 6
+Text-content-md5: 14723c69541ee556d75c581b787dc217
+Content-length: 16
+
+PROPS-END
+omega
+
+
--- a/tests/test_rebuildmeta.py
+++ b/tests/test_rebuildmeta.py
@@ -32,9 +32,9 @@ def _do_case(self, name, stupid):
         self.assertTrue(os.path.isfile(stf), '%r is missing!' % stf)
         dtf = os.path.join(dest.path, 'svn', tf)
         self.assertTrue(os.path.isfile(dtf), '%r is missing!' % tf)
-        self.assertEqual(open(stf).read(),
-                         open(dtf).read())
-    self.assertEqual(src.branchtags(), dest.branchtags())
+        old, new = open(stf).read(), open(dtf).read()
+        self.assertEqual(old, new)
+        self.assertEqual(src.branchtags(), dest.branchtags())
     srcbi = pickle.load(open(os.path.join(src.path, 'svn', 'branch_info')))
     destbi = pickle.load(open(os.path.join(dest.path, 'svn', 'branch_info')))
     self.assertEqual(sorted(srcbi.keys()), sorted(destbi.keys()))
@@ -42,7 +42,7 @@ def _do_case(self, name, stupid):
     for branch in destbi:
         srcinfo = srcbi[branch]
         destinfo = destbi[branch]
-        if destinfo[:2] == (None, 0):
+        if srcinfo[:2] == (None, 0) or destinfo[:2] == (None, 0):
             self.assert_(srcinfo[2] <= destinfo[2])
             self.assertEqual(srcinfo[0], destinfo[0])
         else:
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -86,6 +86,28 @@ class TestTags(test_util.TestBase):
              'versions/branch_version': 'I\x89\x1c>z#\xfc._K#@:\xd6\x1f\x96\xd6\x83\x1b|',
              })
 
+    def test_edited_tag_stupid(self):
+        self.test_edited_tag(True)
+
+    def test_edited_tag(self, stupid=False):
+       repo = self._load_fixture_and_fetch('commit-to-tag.svndump',
+                                           stupid=stupid)
+       self.assertEqual(len(repo.heads()), 2)
+       heads = repo.heads()
+       openheads = [h for h in heads if not repo[h].extra().get('close', False)]
+       closedheads = set(heads) - set(openheads)
+       self.assertEqual(len(openheads), 1)
+       self.assertEqual(len(closedheads), 1)
+       tagged = list(closedheads)[0]
+       self.assertEqual(
+           repo[tagged].extra(),
+           {'close': '1',
+            'branch': 'magic',
+            'convert_revision': 'svn:af82cc90-c2d2-43cd-b1aa-c8a78449440a/tags/will-edit@9'})
+       self.assertEqual(tagged, repo.tags()['will-edit'])
+       self.assertEqual(repo['will-edit'].manifest().keys(), ['beta',
+                                                              'gamma',
+                                                              ])
 
     def test_tags_in_unusual_location(self):
         repo = self._load_fixture_and_fetch('tag_name_same_as_branch.svndump')