Commits

Patrick Mézard committed 2412800

Support svn:externals changes via .hgsvnexternals updates

  • Participants
  • Parent commits f80132c

Comments (0)

Files changed (6)

 
 import util
 import hg_delta_editor
+import svnexternals
 import svnwrap
 import fetch_command
 import utility_commands
     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.
     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):
                     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 = [], []
             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):
 
     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.
     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:
             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)
         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)
 
     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),

File svnexternals.py

                     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])

File svnwrap/svn_swig_wrapper.py

                 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':

File tests/fixtures/pushexternals.sh

+#!/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

File tests/fixtures/pushexternals.svndump

+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
+
+

File tests/test_externals.py

     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)