# HG changeset patch # User Patrick Mezard # Date 1225888628 -3600 # Node ID 49b7cbe4c8e3aba06d9fa8f212451311c15447ac # Parent 63ece4ea25c9725cf0baac7498636aa21a4d0fa6 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. diff --git a/push_cmd.py b/push_cmd.py --- 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?') diff --git a/svnwrap/svn_swig_wrapper.py b/svnwrap/svn_swig_wrapper.py --- 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 diff --git a/tests/fixtures/pushrenames.sh b/tests/fixtures/pushrenames.sh 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 diff --git a/tests/fixtures/pushrenames.svndump b/tests/fixtures/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 + + diff --git a/tests/test_push_renames.py b/tests/test_push_renames.py 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)