changeset 938:f9014e28721b

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 '.
author Patrick Mezard <patrick@mezard.eu>
date Wed, 03 Oct 2012 21:27:02 +0200
parents fb6f6b7fa5a5
children 997de286ba0c
files hgsubversion/editor.py hgsubversion/replay.py tests/comprehensive/test_verify_and_startrev.py tests/fixtures/emptyrepo2.sh tests/fixtures/emptyrepo2.svndump tests/test_fetch_command.py
diffstat 6 files changed, 227 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- 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"
--- 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)
--- 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):
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 <<EOF
+#!/bin/sh
+exit 0
+EOF
+chmod +x testrepo2/hooks/pre-revprop-change
+
+svnurl2=file://`pwd`/testrepo2
+svnsync init --username svnsync $svnurl2 $svnurl/sub
+svnsync sync $svnurl2
+
+svnadmin dump testrepo2 > ../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
+
+
--- 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),