changeset 175:2412800b1258

Support svn:externals changes via .hgsvnexternals updates
author Patrick Mezard <pmezard@gmail.com>
date Fri, 02 Jan 2009 15:54:05 -0600
parents f80132c5fea5
children c4115b3918e9
files push_cmd.py svnexternals.py svnwrap/svn_swig_wrapper.py tests/fixtures/pushexternals.sh tests/fixtures/pushexternals.svndump tests/test_externals.py
diffstat 6 files changed, 324 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- 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),
--- 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])
--- 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':
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
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
+
+
--- 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)