changeset 70:49b7cbe4c8e3

push_cmd: handle copies at file level Mercurial store knows only file-level copies, directory copies are handle with heuristics. Implement the former one in svn backends.
author Patrick Mezard <pmezard@gmail.com>
date Wed, 05 Nov 2008 13:37:08 +0100
parents 63ece4ea25c9
children bf1e8b8ed452
files push_cmd.py svnwrap/svn_swig_wrapper.py tests/fixtures/pushrenames.sh tests/fixtures/pushrenames.svndump tests/test_push_renames.py
diffstat 5 files changed, 315 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/push_cmd.py
+++ b/push_cmd.py
@@ -101,18 +101,27 @@ def commit_from_rev(ui, repo, rev_ctx, h
 
     added_dirs = []
     props = {}
+    copies = {}
     for file in rev_ctx.files():
         new_data = base_data = ''
         action = ''
         if file in rev_ctx:
-            new_data = rev_ctx.filectx(file).data()
+            fctx = rev_ctx.filectx(file)
+            new_data = fctx.data()
 
-            if 'x' in rev_ctx.filectx(file).flags():
+            if 'x' in fctx.flags():
                 props.setdefault(file, {})['svn:executable'] = '*'
-            if 'l' in rev_ctx.filectx(file).flags():
+            if 'l' in fctx.flags():
                 props.setdefault(file, {})['svn:special'] = '*'
 
             if file not in parent:
+                renamed = fctx.renamed()
+                if renamed:
+                    # TODO current model (and perhaps svn model) does not support
+                    # this kind of renames: a -> b, b -> c
+                    copies[file] = renamed[0]
+                    base_data = parent[renamed[0]].data()
+
                 action = 'add'
                 dirname = '/'.join(file.split('/')[:-1] + [''])
                 # check for new directories
@@ -133,7 +142,14 @@ def commit_from_rev(ui, repo, rev_ctx, h
         file_data[file] = base_data, new_data, action
 
     # TODO check for directory deletes here
-    new_target_files = ['%s/%s' % (branch_path, f) for f in rev_ctx.files()]
+    def svnpath(p):
+        return '%s/%s' % (branch_path, p)
+
+    newcopies = {}
+    for source, dest in copies.iteritems():
+        newcopies[svnpath(source)] = (svnpath(dest), base_revision)
+
+    new_target_files = [svnpath(f) for f in rev_ctx.files()]
     for tf, ntf in zip(rev_ctx.files(), new_target_files):
         if tf in file_data:
             file_data[ntf] = file_data[tf]
@@ -149,7 +165,7 @@ def commit_from_rev(ui, repo, rev_ctx, h
     new_target_files += added_dirs
     try:
         svn.commit(new_target_files, rev_ctx.description(), file_data,
-                   base_revision, set(added_dirs), props)
+                   base_revision, set(added_dirs), props, newcopies)
     except core.SubversionException, e:
         if hasattr(e, 'apr_err') and e.apr_err == 160028:
             raise merc_util.Abort('Base text was out of date, maybe rebase?')
--- a/svnwrap/svn_swig_wrapper.py
+++ b/svnwrap/svn_swig_wrapper.py
@@ -264,7 +264,7 @@ class SubversionRepo(object):
                 revisions.pop(0)
 
     def commit(self, paths, message, file_data, base_revision, dirs,
-               properties):
+               properties, copies):
         """Commits the appropriate targets from revision in editor's store.
         """
         self.init_ra_and_client()
@@ -296,7 +296,10 @@ class SubversionRepo(object):
                 baton = editor.open_file(path, parent, base_revision, pool)
             elif action == 'add':
                 try:
-                    baton = editor.add_file(path, parent, None, -1, pool)
+                    frompath, fromrev = copies.get(path, (None, -1))
+                    if frompath:
+                        frompath = self.svn_url + '/' + frompath
+                    baton = editor.add_file(path, parent, frompath, fromrev, pool)
                 except (core.SubversionException, TypeError), e: #pragma: no cover
                     print e.message
                     raise
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/pushrenames.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+#
+# Generate pushrenames.svndump
+#
+
+mkdir temp
+cd temp
+
+mkdir project-orig
+cd project-orig
+mkdir trunk
+mkdir branches
+cd ..
+
+svnadmin create testrepo
+svnurl=file://`pwd`/testrepo
+svn import project-orig $svnurl -m "init project"
+
+svn co $svnurl project
+cd project/trunk
+echo a > a
+echo b > b
+echo c > c
+echo d > d
+echo e > e
+svn add a b c d e
+svn ci -m "add files"
+cd ../..
+
+svnadmin dump testrepo > ../pushrenames.svndump
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/pushrenames.svndump
@@ -0,0 +1,128 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 7b554dd8-6cf3-486c-abe4-86e652fb70b5
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2008-11-02T21:17:17.157936Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 114
+Content-length: 114
+
+K 7
+svn:log
+V 12
+init project
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2008-11-02T21:17:17.217532Z
+PROPS-END
+
+Node-path: branches
+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 9
+add files
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2008-11-02T21:17:18.184945Z
+PROPS-END
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Content-length: 12
+
+PROPS-END
+a
+
+
+Node-path: trunk/b
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 3b5d5c3712955042212316173ccf37be
+Content-length: 12
+
+PROPS-END
+b
+
+
+Node-path: trunk/c
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 2cd6ee2c70b0bde53fbe6cac3c8b8bb1
+Content-length: 12
+
+PROPS-END
+c
+
+
+Node-path: trunk/d
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: e29311f6f1bf1af907f9ef9f44b8328b
+Content-length: 12
+
+PROPS-END
+d
+
+
+Node-path: trunk/e
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 9ffbf43126e33be52cd2bf7e01d627f9
+Content-length: 12
+
+PROPS-END
+e
+
+
new file mode 100644
--- /dev/null
+++ b/tests/test_push_renames.py
@@ -0,0 +1,131 @@
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+from mercurial import context
+from mercurial import commands
+from mercurial import hg
+from mercurial import node
+from mercurial import ui
+from mercurial import revlog
+
+import fetch_command
+import push_cmd
+import test_util
+
+class TestPushRenames(unittest.TestCase):
+    def setUp(self):
+        self.oldwd = os.getcwd()
+        self.tmpdir = tempfile.mkdtemp('svnwrap_test')
+        self.repo_path = '%s/testrepo' % self.tmpdir
+        self.wc_path = '%s/testrepo_wc' % self.tmpdir
+        test_util.load_svndump_fixture(self.repo_path, 'pushrenames.svndump')
+        fetch_command.fetch_revisions(ui.ui(),
+                                      svn_url='file://%s' % self.repo_path,
+                                      hg_repo_path=self.wc_path)
+
+    # define this as a property so that it reloads anytime we need it
+    @property
+    def repo(self):
+        return hg.repository(ui.ui(), self.wc_path)
+
+    def tearDown(self):
+        shutil.rmtree(self.tmpdir)
+        os.chdir(self.oldwd)
+
+    def _commitchanges(self, repo, changes):
+        parentctx = repo['tip']
+
+        changed, removed = [], []
+        for source, dest, newdata in changes:
+            if dest is None:
+                removed.append(source)
+            else:
+                changed.append(dest)
+
+        def filectxfn(repo, memctx, path):
+            if path in removed:
+                raise IOError()
+            entry = [e for e in changes if path == e[1]][0]
+            source, dest, newdata = entry
+            if newdata is None:
+                newdata = parentctx[source].data()
+            copied = None
+            if source != dest:
+                copied = source
+            return context.memfilectx(path=dest,
+                                      data=newdata,
+                                      islink=False,
+                                      isexec=False,
+                                      copied=copied)
+                
+        ctx = context.memctx(repo,
+                             (parentctx.node(), node.nullid),
+                             'automated test',
+                             changed + removed,
+                             filectxfn,
+                             'an_author',
+                             '2008-10-07 20:59:48 -0500')
+        return repo.commitctx(ctx)
+
+    def _debug_print_copies(self, ctx):
+        w = sys.stderr.write
+        for f in ctx.files():
+            if f not in ctx:
+                w('R %s\n' % f)
+            else:
+                w('U %s %r\n' % (f, ctx[f].data()))
+                if ctx[f].renamed():
+                    w('%s copied from %s\n' % (f, ctx[f].renamed()[0]))
+
+    def assertchanges(self, changes, ctx):
+        for source, dest, data in changes:
+            if dest is None:
+                self.assertTrue(source not in ctx)
+            else:
+                self.assertTrue(dest in ctx)
+                if data is None:
+                    data = ctx.parents()[0][source].data()
+                self.assertEqual(data, ctx[dest].data())
+                if dest != source:
+                    copy = ctx[dest].renamed()
+                    self.assertEqual(copy[0], source)
+
+    def test_push_renames(self, commit=True):
+        repo = self.repo
+
+        changes = [
+            # Regular copy of a single file
+            ('a', 'a2', None),
+            # Copy and update of target
+            ('a', 'a3', 'aa\n'),
+            # Regular move of a single file
+            ('b', 'b2', None),
+            ('b', None, None),
+            # Regular move and update of target
+            ('c', 'c2', 'c\nc\n'),
+            ('c', None, None),
+            # Copy and update of source and targets
+            ('d', 'd2', 'd\nd2\n'),
+            ('d', 'd', 'd\nd\n'),
+            # Double copy and removal (aka copy and move)
+            ('e', 'e2', 'e\ne2\n'),
+            ('e', 'e3', 'e\ne3\n'),
+            ('e', None, None),
+            ]
+        self._commitchanges(repo, changes)
+        
+        hg.update(repo, repo['tip'].node())
+        push_cmd.push_revisions_to_subversion(ui.ui(), repo=self.repo,
+                                              hg_repo_path=self.wc_path,
+                                              svn_url='file://'+self.repo_path)
+        tip = self.repo['tip']
+        # self._debug_print_copies(tip)
+        self.assertchanges(changes, tip)
+
+def suite():
+    all = [unittest.TestLoader().loadTestsFromTestCase(TestPushRenames),
+          ]
+    return unittest.TestSuite(all)