# HG changeset patch # User Patrick Mezard # Date 1349292422 -7200 # Node ID f9014e28721b24ae1ad4ee799b58f15127c40779 # Parent fb6f6b7fa5a5a84b46c37c41098aae59c95d1e7d editor: start separating svn copies from open files The separation is not complete as we still have to update the RevisionData deleted set when registering svn copies. This will be cleaned up once open files are themselves separated from RevisionData. Copied symlinks are also being prefixed with 'link '. diff --git a/hgsubversion/editor.py b/hgsubversion/editor.py --- a/hgsubversion/editor.py +++ b/hgsubversion/editor.py @@ -117,6 +117,8 @@ class HgEditor(svnwrap.Editor): self._filecounter = 0 self._filebatons = {} self._files = {} + # A mapping of svn paths to (data, isexec, islink, copypath) tuples + self._svncopies = {} def _addfilebaton(self, path): # XXX: enforce unicity here. This cannot be done right now @@ -137,6 +139,16 @@ class HgEditor(svnwrap.Editor): # Tag deletion is not handled as branched deletion return self.meta.closebranches.add(branch) + + # Delete copied entries, no need to check they exist in hg + # parent revision. + if path in self._svncopies: + del self._svncopies[path] + prefix = path + '/' + for f in list(self._svncopies): + if f.startswith(prefix): + del self._svncopies[f] + if br_path is not None: ha = self.meta.get_parent_revision(self.current.rev.revnum, branch) if ha == revlog.nullid: @@ -148,21 +160,13 @@ class HgEditor(svnwrap.Editor): br_path2 = br_path + '/' # assuming it is a directory self.current.externals[path] = None - prefix = path + '/' - map(self.current.delete, - [pat for pat in self.current.files.iterkeys() - if pat.startswith(prefix)]) for f in ctx.walk(util.PrefixMatch(br_path2)): f_p = '%s/%s' % (path, f[len(br_path2):]) if f_p not in self.current.files: self.current.delete(f_p) - # Remove copied but deleted files - for f in list(self._files): - if f.startswith(prefix): - del self._filebatons[self._files.pop(f)] + if f_p in self._svncopies: + del self._svncopies[f_p] self.current.delete(path) - if path in self._files: - del self._filebatons[self._files.pop(path)] @svnwrap.ieditor def open_file(self, path, parent_baton, base_revision, p=None): @@ -171,13 +175,14 @@ class HgEditor(svnwrap.Editor): self.ui.debug('WARNING: Opening non-existant file %s\n' % path) return None - if path in self._files: - # Remove this when copied files are no longer registered as - # open files. - return self._files[path] - self.ui.note('M %s\n' % path) + if path in self._svncopies: + base, isexec, islink, copypath = self._svncopies.pop(path) + self.current.set(path, base, isexec, islink) + self.current.copies[path] = copypath + return self._addfilebaton(path) + if not self.meta.is_path_valid(path): return None @@ -203,6 +208,8 @@ class HgEditor(svnwrap.Editor): @svnwrap.ieditor def add_file(self, path, parent_baton=None, copyfrom_path=None, copyfrom_revision=None, file_pool=None): + if path in self._svncopies: + raise EditingError('trying to replace copied file %s' % path) if path in self.current.deleted: del self.current.deleted[path] fpath, branch = self.meta.split_branch_path(path, existing=False)[:2] @@ -283,29 +290,38 @@ class HgEditor(svnwrap.Editor): frompath = '%s/' % frompath else: frompath = '' + svncopies = {} copies = {} for f in fromctx: if not f.startswith(frompath): continue fctx = fromctx.filectx(f) dest = path + '/' + f[len(frompath):] - self.current.set(dest, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags()) - # Put copied files with open files for now, they should - # really be separated to reduce resource usage. - self._addfilebaton(dest) + flags = fctx.flags() + islink = 'l' in flags + data = fctx.data() + if islink: + data = 'link ' + data + svncopies[dest] = (data, 'x' in flags, islink, None) if dest in self.current.deleted: + # Remove this once svn copies and edited files are + # clearly separated. del self.current.deleted[dest] if branch == source_branch: copies[dest] = f if copies: # Preserve the directory copy records if no file was changed between # the source and destination revisions, or discard it completely. - parentid = self.meta.get_parent_revision(self.current.rev.revnum, branch) + parentid = self.meta.get_parent_revision( + self.current.rev.revnum, branch) if parentid != revlog.nullid: parentctx = self.repo.changectx(parentid) for k, v in copies.iteritems(): if util.issamefile(parentctx, fromctx, v): - self.current.copies[k] = v + data, isexec, islink = svncopies[k][:-1] + svncopies[k] = (data, isexec, islink, v) + self._svncopies.update(svncopies) + # Copy the externals definitions of copied directories fromext = svnexternals.parse(self.ui, fromctx) for p, v in fromext.iteritems(): @@ -414,6 +430,13 @@ class HgEditor(svnwrap.Editor): raise return txdelt_window + def close(self): + for c in self._svncopies.iteritems(): + dest, (data, isexec, islink, copied) = c + self.current.set(dest, data, isexec, islink) + self.current.copies[dest] = copied + self._svncopies.clear() + _TXDELT_WINDOW_HANDLER_FAILURE_MSG = ( "Your SVN repository may not be supplying correct replay deltas." " It is strongly" diff --git a/hgsubversion/replay.py b/hgsubversion/replay.py --- a/hgsubversion/replay.py +++ b/hgsubversion/replay.py @@ -76,6 +76,7 @@ def convert_rev(ui, meta, svn, r, tbdelt svn.get_revision(r.revnum, editor) else: svn.get_replay(r.revnum, editor, meta.revmap.oldest) + editor.close() current = editor.current current.findmissing(svn) diff --git a/tests/comprehensive/test_verify_and_startrev.py b/tests/comprehensive/test_verify_and_startrev.py --- a/tests/comprehensive/test_verify_and_startrev.py +++ b/tests/comprehensive/test_verify_and_startrev.py @@ -33,6 +33,7 @@ from hgsubversion import verify 'subdir_is_file_prefix.svndump', 'correct.svndump', 'corrupt.svndump', + 'emptyrepo2.svndump', ]) def _do_case(self, name, stupid, layout): diff --git a/tests/fixtures/emptyrepo2.sh b/tests/fixtures/emptyrepo2.sh new file mode 100755 --- /dev/null +++ b/tests/fixtures/emptyrepo2.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# +# Create emptyrepo2.svndump +# +# The generated repository contains a sequence of empty revisions +# created with a combination of svnsync and filtering + +mkdir temp +cd temp + +mkdir project-orig +cd project-orig +mkdir -p sub/trunk other +echo a > other/a +cd .. + +svnadmin create testrepo +svnurl=file://`pwd`/testrepo +svn import project-orig $svnurl -m init + +svn co $svnurl project +cd project +echo a >> other/a +svn ci -m othera +echo a >> other/a +svn ci -m othera2 +echo b > sub/trunk/a +svn add sub/trunk/a +svn ci -m adda +cd .. + +svnadmin create testrepo2 +cat > testrepo2/hooks/pre-revprop-change < ../emptyrepo2.svndump + diff --git a/tests/fixtures/emptyrepo2.svndump b/tests/fixtures/emptyrepo2.svndump new file mode 100644 --- /dev/null +++ b/tests/fixtures/emptyrepo2.svndump @@ -0,0 +1,129 @@ +SVN-fs-dump-format-version: 2 + +UUID: 293d1f29-635d-48b8-9cdf-468fd987067a + +Revision-number: 0 +Prop-content-length: 261 +Content-length: 261 + +K 8 +svn:date +V 27 +2012-10-03T18:58:42.535317Z +K 17 +svn:sync-from-url +V 74 +file:///Users/pmezard/dev/hg/hgsubversion/tests/fixtures/temp/testrepo/sub +K 18 +svn:sync-from-uuid +V 36 +241badf9-093f-4e71-8a58-1028abf52758 +K 24 +svn:sync-last-merged-rev +V 1 +4 +PROPS-END + +Revision-number: 1 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-03T18:58:42.556405Z +K 7 +svn:log +V 4 +init +PROPS-END + +Node-path: sub +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: sub/trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Revision-number: 2 +Prop-content-length: 107 +Content-length: 107 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-03T18:58:43.040912Z +K 7 +svn:log +V 6 +othera +PROPS-END + +Revision-number: 3 +Prop-content-length: 108 +Content-length: 108 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-03T18:58:44.042124Z +K 7 +svn:log +V 7 +othera2 +PROPS-END + +Revision-number: 4 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-03T18:58:45.053459Z +K 7 +svn:log +V 4 +adda +PROPS-END + +Node-path: sub/trunk/a +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 3b5d5c3712955042212316173ccf37be +Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b +Content-length: 12 + +PROPS-END +b + + diff --git a/tests/test_fetch_command.py b/tests/test_fetch_command.py --- a/tests/test_fetch_command.py +++ b/tests/test_fetch_command.py @@ -226,6 +226,14 @@ class TestStupidPull(test_util.TestBase) self.assertEqual(node.hex(repo['tip'].node()), '1a6c3f30911d57abb67c257ec0df3e7bc44786f7') + def test_empty_repo(self, stupid=False): + # This used to crash HgEditor because it could be closed without + # having been initialized again. + self._load_fixture_and_fetch('emptyrepo2.svndump', stupid=stupid) + + def test_empty_repo_stupid(self): + self.test_empty_repo(stupid=True) + def suite(): all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestBasicRepoLayout), unittest.TestLoader().loadTestsFromTestCase(TestStupidPull),