# HG changeset patch # User Patrick Mezard # Date 1227242475 21600 # Node ID 0d3a2a7cefa3e5779ef5121c4f12fa86cddc0129 # Parent 9b5e528f67f8cbdf1d373380306535e44c15d55d 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. diff --git a/fetch_command.py b/fetch_command.py --- 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) diff --git a/hg_delta_editor.py b/hg_delta_editor.py --- 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 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 @@ -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): diff --git a/tests/fixtures/symlinks.sh b/tests/fixtures/symlinks.sh 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 diff --git a/tests/fixtures/symlinks.svndump b/tests/fixtures/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 + diff --git a/tests/test_fetch_symlinks.py b/tests/test_fetch_symlinks.py 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)