# HG changeset patch # User Patrick Mezard # Date 1230933245 21600 # Node ID 2412800b12588582d082aecaf03bb7d582b7e598 # Parent f80132c5fea574f3ec63e79062231023ecb69cd3 Support svn:externals changes via .hgsvnexternals updates diff --git a/push_cmd.py b/push_cmd.py --- a/push_cmd.py +++ b/push_cmd.py @@ -5,6 +5,7 @@ from svn import core import util import hg_delta_editor +import svnexternals import svnwrap import fetch_command import utility_commands @@ -98,9 +99,10 @@ def _isdir(svn, branchpath, svndir): except core.SubversionException: return False -def _getdirchanges(svn, branchpath, parentctx, ctx, changedfiles): +def _getdirchanges(svn, branchpath, parentctx, ctx, changedfiles, extchanges): """Compute directories to add or delete when moving from parentctx - to ctx, assuming only 'changedfiles' files changed. + to ctx, assuming only 'changedfiles' files changed, and 'extchanges' + external references changed (as returned by svnexternals.diff()). Return (added, deleted) where 'added' is the list of all added directories and 'deleted' the list of deleted directories. @@ -109,13 +111,15 @@ def _getdirchanges(svn, branchpath, pare deleted directories are also listed, but item order of undefined in either list. """ - def finddirs(path): + def finddirs(path, includeself=False): + if includeself: + yield path pos = path.rfind('/') while pos != -1: yield path[:pos] pos = path.rfind('/', 0, pos) - def getctxdirs(ctx, keptdirs): + def getctxdirs(ctx, keptdirs, extdirs): dirs = {} for f in ctx.manifest(): for d in finddirs(f): @@ -123,6 +127,9 @@ def _getdirchanges(svn, branchpath, pare break if d in keptdirs: dirs[d] = 1 + for extdir in extdirs: + for d in finddirs(extdir, True): + dirs[d] = 1 return dirs deleted, added = [], [] @@ -134,10 +141,16 @@ def _getdirchanges(svn, branchpath, pare continue for d in finddirs(f): changeddirs[d] = 1 + for e in extchanges: + if not e[1] or not e[2]: + for d in finddirs(e[0], True): + changeddirs[d] = 1 if not changeddirs: return added, deleted - olddirs = getctxdirs(parentctx, changeddirs) - newdirs = getctxdirs(ctx, changeddirs) + olddirs = getctxdirs(parentctx, changeddirs, + [e[0] for e in extchanges if e[1]]) + newdirs = getctxdirs(ctx, changeddirs, + [e[0] for e in extchanges if e[2]]) for d in newdirs: if d not in olddirs and not _isdir(svn, branchpath, d): @@ -149,6 +162,11 @@ def _getdirchanges(svn, branchpath, pare return added, deleted +def _externals(ctx): + ext = svnexternals.externalsfile() + if '.hgsvnexternals' in ctx: + ext.read(ctx['.hgsvnexternals'].data()) + return ext def commit_from_rev(ui, repo, rev_ctx, hg_editor, svn_url, base_revision): """Build and send a commit from Mercurial to Subversion. @@ -162,13 +180,17 @@ def commit_from_rev(ui, repo, rev_ctx, h if parent_branch and parent_branch != 'default': branch_path = 'branches/%s' % parent_branch - addeddirs, deleteddirs = _getdirchanges(svn, branch_path, parent, - rev_ctx, rev_ctx.files()) + extchanges = list(svnexternals.diff(_externals(parent), + _externals(rev_ctx))) + addeddirs, deleteddirs = _getdirchanges(svn, branch_path, parent, rev_ctx, + rev_ctx.files(), extchanges) deleteddirs = set(deleteddirs) props = {} copies = {} for file in rev_ctx.files(): + if file == '.hgsvnexternals': + continue new_data = base_data = '' action = '' if file in rev_ctx: @@ -208,6 +230,15 @@ def commit_from_rev(ui, repo, rev_ctx, h action = 'delete' file_data[file] = base_data, new_data, action + def svnpath(p): + return '%s/%s' % (branch_path, p) + + changeddirs = [] + for d, v1, v2 in extchanges: + props.setdefault(svnpath(d), {})['svn:externals'] = v2 + if d not in deleteddirs and d not in addeddirs: + changeddirs.append(svnpath(d)) + # Now we are done with files, we can prune deleted directories # against themselves: ignore a/b if a/ is already removed deleteddirs2 = list(deleteddirs) @@ -217,9 +248,6 @@ def commit_from_rev(ui, repo, rev_ctx, h if pos >= 0 and d[:pos] in deleteddirs: deleteddirs.remove(d[:pos]) - def svnpath(p): - return '%s/%s' % (branch_path, p) - newcopies = {} for source, dest in copies.iteritems(): newcopies[svnpath(source)] = (svnpath(dest), base_revision) @@ -238,7 +266,7 @@ def commit_from_rev(ui, repo, rev_ctx, h addeddirs = [svnpath(d) for d in addeddirs] deleteddirs = [svnpath(d) for d in deleteddirs] - new_target_files += addeddirs + deleteddirs + new_target_files += addeddirs + deleteddirs + changeddirs try: svn.commit(new_target_files, rev_ctx.description(), file_data, base_revision, set(addeddirs), set(deleteddirs), diff --git a/svnexternals.py b/svnexternals.py --- a/svnexternals.py +++ b/svnexternals.py @@ -57,3 +57,16 @@ class externalsfile(dict): continue self.setdefault(target, []).append(line[1:]) +def diff(ext1, ext2): + """Compare 2 externalsfile and yield tuples like (dir, value1, value2) + where value1 is the external value in ext1 for dir or None, and + value2 the same in ext2. + """ + for d in ext1: + if d not in ext2: + yield d, '\n'.join(ext1[d]), None + elif ext1[d] != ext2[d]: + yield d, '\n'.join(ext1[d]), '\n'.join(ext2[d]) + for d in ext2: + if d not in ext1: + yield d, None, '\n'.join(ext2[d]) 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 @@ -319,14 +319,21 @@ class SubversionRepo(object): bat = editor.open_root(edit_baton, base_revision, self.pool) batons.append(bat) return bat - if path in addeddirs: - bat = editor.add_directory(path, parent, None, -1, pool) - batons.append(bat) - return bat if path in deleteddirs: bat = editor.delete_entry(path, base_revision, parent, pool) batons.append(bat) return bat + if path not in file_data: + if path in addeddirs: + bat = editor.add_directory(path, parent, None, -1, pool) + else: + bat = editor.open_directory(path, parent, base_revision, pool) + batons.append(bat) + props = properties.get(path, {}) + if 'svn:externals' in props: + value = props['svn:externals'] + editor.change_dir_prop(bat, 'svn:externals', value, pool) + return bat base_text, new_text, action = file_data[path] compute_delta = True if action == 'modify': diff --git a/tests/fixtures/pushexternals.sh b/tests/fixtures/pushexternals.sh new file mode 100755 --- /dev/null +++ b/tests/fixtures/pushexternals.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# +# Generate pushexternals.svndump +# + +mkdir temp +cd temp + +mkdir project-orig +cd project-orig +mkdir trunk +mkdir externals +cd .. + +svnadmin create testrepo +svnurl=file://`pwd`/testrepo +svn import project-orig $svnurl -m "init project" + +svn co $svnurl project +cd project/externals +mkdir project1 +echo a > project1/a +svn add project1 +mkdir project2 +echo a > project2/b +svn add project2 +svn ci -m "configure externals projects" +cd ../trunk +echo a > a +# dir is used to set svn:externals on an already existing directory +mkdir dir +svn add a dir +svn ci -m "add a and dir" +svn rm a +svn ci -m "remove a" +cd ../.. + +svnadmin dump testrepo > ../pushexternals.svndump diff --git a/tests/fixtures/pushexternals.svndump b/tests/fixtures/pushexternals.svndump new file mode 100644 --- /dev/null +++ b/tests/fixtures/pushexternals.svndump @@ -0,0 +1,171 @@ +SVN-fs-dump-format-version: 2 + +UUID: ce6cbbbe-6533-4ba7-91e1-cc165717826f + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2008-12-27T19:48:52.687312Z +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-12-27T19:48:52.751303Z +PROPS-END + +Node-path: externals +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: 130 +Content-length: 130 + +K 7 +svn:log +V 28 +configure externals projects +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2008-12-27T19:48:53.230452Z +PROPS-END + +Node-path: externals/project1 +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: externals/project1/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: externals/project2 +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: externals/project2/b +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3 +Content-length: 12 + +PROPS-END +a + + +Revision-number: 3 +Prop-content-length: 115 +Content-length: 115 + +K 7 +svn:log +V 13 +add a and dir +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2008-12-27T19:48:54.187575Z +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/dir +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Revision-number: 4 +Prop-content-length: 109 +Content-length: 109 + +K 7 +svn:log +V 8 +remove a +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2008-12-27T19:48:55.175595Z +PROPS-END + +Node-path: trunk/a +Node-action: delete + + diff --git a/tests/test_externals.py b/tests/test_externals.py --- a/tests/test_externals.py +++ b/tests/test_externals.py @@ -81,7 +81,58 @@ class TestFetchExternals(test_util.TestB def test_externals_stupid(self): self.test_externals(True) + +class TestPushExternals(test_util.TestBase): + def setUp(self): + test_util.TestBase.setUp(self) + test_util.load_fixture_and_fetch('pushexternals.svndump', + self.repo_path, + self.wc_path) + + def test_push_externals(self, stupid=False): + # Add a new reference on an existing and non-existing directory + changes = [ + ('.hgsvnexternals', '.hgsvnexternals', + """\ +[dir] + ../externals/project2 deps/project2 +[subdir1] + ../externals/project1 deps/project1 +[subdir2] + ../externals/project2 deps/project2 +"""), + ('subdir1/a', 'subdir1/a', 'a'), + ('subdir2/a', 'subdir2/a', 'a'), + ] + self.commitchanges(changes) + self.pushrevisions(stupid) + self.assertchanges(changes, self.repo['tip']) + + # Remove all references from one directory, add a new one + # to the other (test multiline entries) + changes = [ + ('.hgsvnexternals', '.hgsvnexternals', + """\ +[subdir1] + ../externals/project1 deps/project1 + ../externals/project2 deps/project2 +"""), + # This removal used to trigger the parent directory removal + ('subdir1/a', None, None), + ] + self.commitchanges(changes) + self.pushrevisions(stupid) + self.assertchanges(changes, self.repo['tip']) + # Check subdir2/a is still there even if the externals were removed + self.assertTrue('subdir2/a' in self.repo['tip']) + self.assertTrue('subdir1/a' not in self.repo['tip']) + + def test_push_externals_stupid(self): + self.test_push_externals(True) + + def suite(): all = [unittest.TestLoader().loadTestsFromTestCase(TestFetchExternals), + unittest.TestLoader().loadTestsFromTestCase(TestPushExternals), ] return unittest.TestSuite(all)