changeset 97:0d3a2a7cefa3

hg_delta_editor: fix symlink prefix confusion - SubversionRepo.get_file() strips the symlink prefix - Enforce that hg_delta_editor symlink data always contains the prefix. The alternative was seducing and more consistent with hg content but it makes the code more complicated since svn:special can be set before or after the content is set, and we need it in apply_textdelta() This issue fixes jQuery repository conversion at r3674.
author Patrick Mezard <pmezard@gmail.com>
date Thu, 20 Nov 2008 22:41:15 -0600
parents 9b5e528f67f8
children c7ac013cf7fd
files fetch_command.py hg_delta_editor.py svnwrap/svn_swig_wrapper.py tests/fixtures/symlinks.sh tests/fixtures/symlinks.svndump tests/test_fetch_symlinks.py
diffstat 6 files changed, 328 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/fetch_command.py
+++ b/fetch_command.py
@@ -154,9 +154,7 @@ def replay_convert_rev(hg_editor, svn, r
             cleanup_file_handles(svn, i)
             i += 1
             data, mode = svn.get_file(p2, r.revnum)
-            hg_editor.current_files[p] = data
-            hg_editor.current_files_exec[p] = 'x' in mode
-            hg_editor.current_files_symlink[p] = 'l' in mode
+            hg_editor.set_file(p, data, 'x' in mode, 'l' in mode)
         hg_editor.missing_plaintexts = set()
         hg_editor.ui.status('\n')
     hg_editor.commit_current_delta()
@@ -339,13 +337,10 @@ def stupid_fetch_branchrev(svn, hg_edito
 
     copies = getcopies(svn, hg_editor, branch, branchpath, r, files, parentid)
     
-    linkprefix = 'link '
     def filectxfn(repo, memctx, path):
         data, mode = svn.get_file(branchpath + '/' + path, r.revnum)
         isexec = 'x' in mode
         islink = 'l' in mode
-        if islink and data.startswith(linkprefix):
-            data = data[len(linkprefix):]
         copied = copies.get(path)
         return context.memfilectx(path=path, data=data, islink=islink,
                                   isexec=isexec, copied=copied)
--- a/hg_delta_editor.py
+++ b/hg_delta_editor.py
@@ -122,6 +122,7 @@ class HgChangeReceiver(delta.Editor):
         '''Clear the info relevant to a replayed revision so that the next
         revision can be replayed.
         '''
+        # Map files to raw svn data (symlink prefix is preserved)
         self.current_files = {}
         self.deleted_files = {}
         self.current_rev = None
@@ -181,6 +182,13 @@ class HgChangeReceiver(delta.Editor):
         '''
         self.current_rev = rev
 
+    def set_file(self, path, data, isexec=False, islink=False):
+        if islink:
+            data = 'link ' + data
+        self.current_files[path] = data
+        self.current_files_exec[path] = isexec
+        self.current_files_symlink[path] = islink
+
     def _normalize_path(self, path):
         '''Normalize a path to strip of leading slashes and our subdir if we
         have one.
@@ -362,10 +370,8 @@ class HgChangeReceiver(delta.Editor):
                     raise IOError()
                 copied = self.copies.get(current_file)
                 flags = parent_ctx.flags(path)
-                is_exec = self.current_files_exec.get(current_file,
-                                                      'x' in flags)
-                is_link = self.current_files_symlink.get(current_file,
-                                                         'l' in flags)
+                is_exec = self.current_files_exec.get(current_file, 'x' in flags)
+                is_link = self.current_files_symlink.get(current_file, 'l' in flags)
                 if current_file in self.current_files:
                     data = self.current_files[current_file]
                     if is_link:
@@ -551,10 +557,9 @@ class HgChangeReceiver(delta.Editor):
         ctx = self.repo.changectx(ha)
         if from_file in ctx:
             fctx = ctx.filectx(from_file)
+            flags = fctx.flags()
             cur_file = self.current_file
-            self.current_files[cur_file] = fctx.data()
-            self.current_files_symlink[cur_file] = 'l' in fctx.flags()
-            self.current_files_exec[cur_file] = 'x' in fctx.flags()
+            self.set_file(cur_file, fctx.data(), 'x' in flags, 'l' in flags)
         if from_branch == branch:
             parentid = self.get_parent_revision(self.current_rev.revnum,
                                                 branch)
@@ -605,9 +610,7 @@ class HgChangeReceiver(delta.Editor):
             f2 = f[len(cp_f):]
             fctx = cp_f_ctx.filectx(f)
             fp_c = path + '/' + f2
-            self.current_files[fp_c] = fctx.data()
-            self.current_files_exec[fp_c] = 'x' in fctx.flags()
-            self.current_files_symlink[fp_c] = 'l' in fctx.flags()
+            self.set_file(fp_c, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags())
             if fp_c in self.deleted_files:
                 del self.deleted_files[fp_c]
             if branch == source_branch:
@@ -656,7 +659,10 @@ class HgChangeReceiver(delta.Editor):
                         self.missing_plaintexts.add(self.current_file)
                         # short circuit exit since we can't do anything anyway
                         return lambda x: None
-                    base = ctx.filectx(p_).data()
+                    fctx = ctx[p_]
+                    base = fctx.data()
+                    if 'l' in fctx.flags():
+                        base = 'link ' + base
         source = cStringIO.StringIO(base)
         target = cStringIO.StringIO()
         self.stream = target
--- a/svnwrap/svn_swig_wrapper.py
+++ b/svnwrap/svn_swig_wrapper.py
@@ -431,10 +431,10 @@ class SubversionRepo(object):
     def get_file(self, path, revision):
         """Return content and mode of file at given path and revision.
 
-        Content is raw svn content, symlinks content is still prefixed
-        by 'link '. Mode is 'x' if file is executable, 'l' if a symlink,
-        the empty string otherwise. If the file does not exist at this
-        revision, raise IOError.
+        "link " prefix is dropped from symlink content. Mode is 'x' if
+        file is executable, 'l' if a symlink, the empty string
+        otherwise. If the file does not exist at this revision, raise
+        IOError.
         """
         mode = ''
         out = cStringIO.StringIO()
@@ -451,6 +451,10 @@ class SubversionRepo(object):
                 raise IOError()
             raise
         data = out.getvalue()
+        if mode  == 'l':
+            linkprefix = "link "
+            if data.startswith(linkprefix):
+                data = data[len(linkprefix):]
         return data, mode
 
     def proplist(self, path, revision, recurse=False):
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/symlinks.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# Generate symlinks.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
+ln -s a linka
+mkdir d
+ln -s a d/linka
+svn add a linka d
+svn ci -m "add symlinks"
+# Move symlinks
+svn mv linka linkaa
+svn mv d d2
+svn commit -m "moving symlinks"
+# Update symlinks (test "link " prefix vs applydelta)
+echo b > b
+rm linkaa
+ln -s b linkaa
+rm d2/linka
+ln -s b d2/linka
+svn ci -m "update symlinks"
+cd ../..
+
+svnadmin dump testrepo > ../symlinks.svndump
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/symlinks.svndump
@@ -0,0 +1,216 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 0155d33a-8628-44e5-a968-540cca8db82a
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2008-11-16T15:00:25.793049Z
+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-16T15:00:25.873999Z
+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: 114
+Content-length: 114
+
+K 7
+svn:log
+V 12
+add symlinks
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2008-11-16T15:00:26.214702Z
+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/d
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/d/linka
+Node-kind: file
+Node-action: add
+Prop-content-length: 33
+Text-content-length: 6
+Text-content-md5: c118dba188202a1efc975bef6064180b
+Content-length: 39
+
+K 11
+svn:special
+V 1
+*
+PROPS-END
+link a
+
+Node-path: trunk/linka
+Node-kind: file
+Node-action: add
+Prop-content-length: 33
+Text-content-length: 6
+Text-content-md5: c118dba188202a1efc975bef6064180b
+Content-length: 39
+
+K 11
+svn:special
+V 1
+*
+PROPS-END
+link a
+
+Revision-number: 3
+Prop-content-length: 117
+Content-length: 117
+
+K 7
+svn:log
+V 15
+moving symlinks
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2008-11-16T15:00:29.163300Z
+PROPS-END
+
+Node-path: trunk/d2
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/d
+Prop-content-length: 34
+Content-length: 34
+
+K 13
+svn:mergeinfo
+V 0
+
+PROPS-END
+
+
+Node-path: trunk/linkaa
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/linka
+Text-copy-source-md5: c118dba188202a1efc975bef6064180b
+Prop-content-length: 57
+Content-length: 57
+
+K 11
+svn:special
+V 1
+*
+K 13
+svn:mergeinfo
+V 0
+
+PROPS-END
+
+
+Node-path: trunk/linka
+Node-action: delete
+
+
+Node-path: trunk/d
+Node-action: delete
+
+
+Revision-number: 4
+Prop-content-length: 117
+Content-length: 117
+
+K 7
+svn:log
+V 15
+update symlinks
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2008-11-16T15:00:30.171883Z
+PROPS-END
+
+Node-path: trunk/d2/linka
+Node-kind: file
+Node-action: change
+Text-content-length: 6
+Text-content-md5: e9292b8c4fca95ac8c70b4ae040d0b79
+Content-length: 6
+
+link b
+
+Node-path: trunk/linkaa
+Node-kind: file
+Node-action: change
+Text-content-length: 6
+Text-content-md5: e9292b8c4fca95ac8c70b4ae040d0b79
+Content-length: 6
+
+link b
+
new file mode 100644
--- /dev/null
+++ b/tests/test_fetch_symlinks.py
@@ -0,0 +1,46 @@
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+from mercurial import hg
+from mercurial import ui
+from mercurial import node
+
+import fetch_command
+import test_util
+
+
+class TestFetchSymlinks(test_util.TestBase):
+    def _load_fixture_and_fetch(self, fixture_name, stupid):
+        return test_util.load_fixture_and_fetch(fixture_name, self.repo_path,
+                                                self.wc_path, stupid=stupid)
+
+    def test_symlinks(self, stupid=False):
+        repo = self._load_fixture_and_fetch('symlinks.svndump', stupid)
+        # Check no symlink contains the 'link ' prefix
+        for rev in repo:
+            r = repo[rev]
+            for f in r.manifest():
+                if 'l' not in r[f].flags():
+                    continue
+                self.assertFalse(r[f].data().startswith('link '))
+        # Check symlinks in tip
+        tip = repo['tip']
+        links = {
+            'linkaa': 'b',
+            'd2/linka': 'b',
+            }
+        for f in tip.manifest():
+            self.assertEqual(f in links, 'l' in tip[f].flags())
+            if f in links:
+                self.assertEqual(links[f], tip[f].data())
+
+    def test_symlinks_stupid(self):
+        self.test_symlinks(True)
+
+def suite():
+    all = [unittest.TestLoader().loadTestsFromTestCase(TestFetchSymlinks),
+          ]
+    return unittest.TestSuite(all)