changeset 69:63ece4ea25c9

hg_delta_editor: register copies only if files are unchanged between source and dest Handle copies of items from revision X into revision Y where X is not the parent of Y. This cannot happen in Mercurial because copies always happen between parents and children. A file copy is recorded if: 1- Source and destination revs are in the same branch. 2- The file is unchanged (content, type, removal) through all revisions between destination and source, not including source and destination. Directory copies are registered only if the previous rules apply on all copied items. [1] is there because file copies across branches are meaningless in Mercurial world. We could have tried to remap the source rev to a similar one in the correct branch, but anyway the intent is wrong. [2] is more questionable but I think it's better this way for we live in a non-perfect svn world. In theory, 99% of copies out there should come from the direct parent. But the direct parent is a fuzzy notion when you can have a working directory composed of different directory at different revisions. So we assume that stuff copied from past revisions exactly matching the content of the direct parent revision is really copied from the parent revision. The alternative would be to discard the copy, which would always happen unless people kept updating the working directory after every commit (see tests).
author Patrick Mezard <pmezard@gmail.com>
date Wed, 05 Nov 2008 13:37:08 +0100
parents e0c86ebe05e3
children 49b7cbe4c8e3
files hg_delta_editor.py tests/fixtures/renames.sh tests/fixtures/renames.svndump tests/test_fetch_renames.py
diffstat 4 files changed, 290 insertions(+), 24 deletions(-) [+]
line wrap: on
line diff
--- a/hg_delta_editor.py
+++ b/hg_delta_editor.py
@@ -495,6 +495,30 @@ class HgChangeReceiver(delta.Editor):
                 self.base_revision = None
             self.should_edit_most_recent_plaintext = True
 
+    def _aresamefiles(self, parentctx, childctx, files):
+        """Assuming all files exist in childctx and parentctx, return True
+        if none of them was changed in-between.
+        """
+        if parentctx == childctx:
+            return True
+        if parentctx.rev() > childctx.rev():
+            parentctx, childctx = childctx, parentctx
+        
+        def selfandancestors(selfctx):
+            yield selfctx
+            for ctx in selfctx.ancestors():
+                yield ctx
+                
+        files = dict.fromkeys(files)
+        for pctx in selfandancestors(childctx):
+            if pctx.rev() <= parentctx.rev():
+                return True
+            for f in pctx.files():                
+                if f in files:
+                    return False
+        # parentctx is not an ancestor of childctx, files are unrelated
+        return False
+
     @stash_exception_on_self
     def add_file(self, path, parent_baton, copyfrom_path,
                  copyfrom_revision, file_pool=None):
@@ -528,7 +552,12 @@ class HgChangeReceiver(delta.Editor):
             self.current_files_symlink[cur_file] = 'l' in fctx.flags()
             self.current_files_exec[cur_file] = 'x' in fctx.flags()
         if from_branch == branch:
-            self.copies[path] = from_file
+            parentid = self.get_parent_revision(self.current_rev.revnum,
+                                                branch)
+            if parentid != revlog.nullid:
+                parentctx = self.repo.changectx(parentid)
+                if self._aresamefiles(parentctx, ctx, [from_file]):
+                    self.copies[path] = from_file
 
     @stash_exception_on_self
     def add_directory(self, path, parent_baton, copyfrom_path,
@@ -565,18 +594,28 @@ class HgChangeReceiver(delta.Editor):
             cp_f = '%s/' % cp_f
         else:
             cp_f = ''
+        copies = {}
         for f in cp_f_ctx:
-            if f.startswith(cp_f):
-                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()
-                if fp_c in self.deleted_files:
-                    del self.deleted_files[fp_c]
-                if branch == source_branch:
-                    self.copies[fp_c] = f
+            if not f.startswith(cp_f):
+                continue
+            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()
+            if fp_c in self.deleted_files:
+                del self.deleted_files[fp_c]
+            if branch == source_branch:
+                copies[fp_c] = 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.get_parent_revision(self.current_rev.revnum, branch)
+            if parentid != revlog.nullid:
+                parentctx = self.repo.changectx(parentid)
+                if self._aresamefiles(parentctx, cp_f_ctx, copies.values()):
+                    self.copies.update(copies)
 
     @stash_exception_on_self
     def change_file_prop(self, file_baton, name, value, pool=None):
--- a/tests/fixtures/renames.sh
+++ b/tests/fixtures/renames.sh
@@ -18,18 +18,32 @@ svn import project-orig $svnurl -m "init
 
 svn co $svnurl project
 cd project/trunk
+# Entries for regular tests
 echo a > a
 echo b > b
 mkdir -p da/db
 echo c > da/daf
 echo d > da/db/dbf
+# Entries to test delete + copy
 echo deleted > deletedfile
 mkdir deleteddir
 echo deleteddir > deleteddir/f
-svn add a b da deletedfile deleteddir
+# Entries to test copy before change
+echo changed > changed
+mkdir changeddir
+echo changed2 > changeddir/f
+# Entries unchanged in the rest of history
+echo unchanged > unchanged
+mkdir unchangeddir
+echo unchanged2 > unchangeddir/f
+svn add a b da deletedfile deleteddir changed changeddir unchanged unchangeddir
 svn ci -m "add a and b"
+# Remove files to be copied later
 svn rm deletedfile
 svn rm deleteddir
+# Update files to be copied before this change
+echo changed >> changed
+echo changed2 >> changeddir/f
 svn ci -m "delete files and dirs"
 cd ../branches
 svn cp ../trunk branch1
@@ -63,6 +77,16 @@ svn ci -m "copy b from branch1"
 svn cp $svnurl/trunk/deletedfile@2 deletedfile
 svn cp $svnurl/trunk/deleteddir@2 deleteddir
 svn ci -m "copy stuff from the past"
+# Copy data from the past before it was changed
+svn cp $svnurl/trunk/changed@2 changed2
+svn cp $svnurl/trunk/changeddir@2 changeddir2
+svn ci -m "copy stuff from the past before change"
+# Copy unchanged stuff from the past. Since no changed occured in these files
+# between the source and parent revision, we record them as copy from parent
+# instead of source rev.
+svn cp $svnurl/trunk/unchanged@2 unchanged2
+svn cp $svnurl/trunk/unchangeddir@2 unchangeddir2
+svn ci -m "copy unchanged stuff from the past"
 cd ../..
 
 svnadmin dump testrepo > ../renames.svndump
--- a/tests/fixtures/renames.svndump
+++ b/tests/fixtures/renames.svndump
@@ -1,6 +1,6 @@
 SVN-fs-dump-format-version: 2
 
-UUID: 6ebabe06-44c1-4542-ad93-bd59fbe3743b
+UUID: fa1ccad6-11a6-48b0-ba92-9a083fa61127
 
 Revision-number: 0
 Prop-content-length: 56
@@ -9,7 +9,7 @@ Content-length: 56
 K 8
 svn:date
 V 27
-2008-11-02T10:33:21.311566Z
+2008-11-02T15:08:30.507812Z
 PROPS-END
 
 Revision-number: 1
@@ -27,7 +27,7 @@ pmezard
 K 8
 svn:date
 V 27
-2008-11-02T10:33:21.376729Z
+2008-11-02T15:08:30.609102Z
 PROPS-END
 
 Node-path: branches
@@ -63,7 +63,7 @@ pmezard
 K 8
 svn:date
 V 27
-2008-11-02T10:33:22.217406Z
+2008-11-02T15:08:31.258716Z
 PROPS-END
 
 Node-path: trunk/a
@@ -90,6 +90,39 @@ PROPS-END
 b
 
 
+Node-path: trunk/changed
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 8
+Text-content-md5: ec1bebaea2c042beb68f7679ddd106a4
+Content-length: 18
+
+PROPS-END
+changed
+
+
+Node-path: trunk/changeddir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/changeddir/f
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 9
+Text-content-md5: 2dfdfd8492a2c558ec838d69f73f5f6b
+Content-length: 19
+
+PROPS-END
+changed2
+
+
 Node-path: trunk/da
 Node-kind: dir
 Node-action: add
@@ -165,6 +198,39 @@ PROPS-END
 deleted
 
 
+Node-path: trunk/unchanged
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 10
+Text-content-md5: 85ae5b04dd0a666efad8633d142a4635
+Content-length: 20
+
+PROPS-END
+unchanged
+
+
+Node-path: trunk/unchangeddir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/unchangeddir/f
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 11
+Text-content-md5: a11092875079a002afb9ecef07f510e7
+Content-length: 21
+
+PROPS-END
+unchanged2
+
+
 Revision-number: 3
 Prop-content-length: 123
 Content-length: 123
@@ -180,9 +246,31 @@ pmezard
 K 8
 svn:date
 V 27
-2008-11-02T10:33:23.197795Z
+2008-11-02T15:08:32.203445Z
 PROPS-END
 
+Node-path: trunk/changed
+Node-kind: file
+Node-action: change
+Text-content-length: 16
+Text-content-md5: 1725f40a29aad369a39b0f96c82d50f9
+Content-length: 16
+
+changed
+changed
+
+
+Node-path: trunk/changeddir/f
+Node-kind: file
+Node-action: change
+Text-content-length: 18
+Text-content-md5: 984b8c4ab9193b7659b9f914897a949c
+Content-length: 18
+
+changed2
+changed2
+
+
 Node-path: trunk/deleteddir
 Node-action: delete
 
@@ -206,7 +294,7 @@ pmezard
 K 8
 svn:date
 V 27
-2008-11-02T10:33:25.160163Z
+2008-11-02T15:08:34.175358Z
 PROPS-END
 
 Node-path: branches/branch1
@@ -240,6 +328,35 @@ Node-copyfrom-path: trunk/b
 Text-copy-source-md5: 3b5d5c3712955042212316173ccf37be
 
 
+Node-path: branches/branch1/changed
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 3
+Node-copyfrom-path: trunk/changed
+Text-copy-source-md5: 1725f40a29aad369a39b0f96c82d50f9
+
+
+Node-path: branches/branch1/changeddir
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/changeddir
+
+
+Node-path: branches/branch1/changeddir/f
+Node-kind: file
+Node-action: delete
+
+Node-path: branches/branch1/changeddir/f
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 3
+Node-copyfrom-path: trunk/changeddir/f
+Text-copy-source-md5: 984b8c4ab9193b7659b9f914897a949c
+
+
+
+
 Node-path: branches/branch1/da
 Node-kind: dir
 Node-action: add
@@ -247,6 +364,21 @@ Node-copyfrom-rev: 2
 Node-copyfrom-path: trunk/da
 
 
+Node-path: branches/branch1/unchanged
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/unchanged
+Text-copy-source-md5: 85ae5b04dd0a666efad8633d142a4635
+
+
+Node-path: branches/branch1/unchangeddir
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/unchangeddir
+
+
 Revision-number: 5
 Prop-content-length: 106
 Content-length: 106
@@ -262,7 +394,7 @@ pmezard
 K 8
 svn:date
 V 27
-2008-11-02T10:33:26.173058Z
+2008-11-02T15:08:35.168793Z
 PROPS-END
 
 Node-path: branches/branch1/c
@@ -292,7 +424,7 @@ pmezard
 K 8
 svn:date
 V 27
-2008-11-02T10:33:33.195961Z
+2008-11-02T15:08:42.197170Z
 PROPS-END
 
 Node-path: branches/branch1/c1
@@ -431,7 +563,7 @@ pmezard
 K 8
 svn:date
 V 27
-2008-11-02T10:33:35.147012Z
+2008-11-02T15:08:44.147557Z
 PROPS-END
 
 Node-path: trunk/c
@@ -465,7 +597,7 @@ pmezard
 K 8
 svn:date
 V 27
-2008-11-02T10:33:38.154817Z
+2008-11-02T15:08:47.152642Z
 PROPS-END
 
 Node-path: trunk/deleteddir
@@ -483,3 +615,69 @@ Node-copyfrom-path: trunk/deletedfile
 Text-copy-source-md5: 4d742b2f247bec99b41a60acbebc149a
 
 
+Revision-number: 9
+Prop-content-length: 140
+Content-length: 140
+
+K 7
+svn:log
+V 38
+copy stuff from the past before change
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2008-11-02T15:08:50.152360Z
+PROPS-END
+
+Node-path: trunk/changed2
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/changed
+Text-copy-source-md5: ec1bebaea2c042beb68f7679ddd106a4
+
+
+Node-path: trunk/changeddir2
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/changeddir
+
+
+Revision-number: 10
+Prop-content-length: 136
+Content-length: 136
+
+K 7
+svn:log
+V 34
+copy unchanged stuff from the past
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2008-11-02T15:08:53.156849Z
+PROPS-END
+
+Node-path: trunk/unchanged2
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/unchanged
+Text-copy-source-md5: 85ae5b04dd0a666efad8633d142a4635
+
+
+Node-path: trunk/unchangeddir2
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/unchangeddir
+
+
--- a/tests/test_fetch_renames.py
+++ b/tests/test_fetch_renames.py
@@ -53,6 +53,10 @@ class TestFetchRenames(unittest.TestCase
                 },
             5: {
                 'c1': ('c', 'c\nc\n'),
+                },
+            9: {
+                'unchanged2': ('unchanged', 'unchanged\n'),
+                'unchangeddir2/f': ('unchangeddir/f', 'unchanged2\n'),
                 }
             }
         for rev in repo:
@@ -60,7 +64,8 @@ class TestFetchRenames(unittest.TestCase
             copymap = copies.get(rev, {})
             for f in ctx.manifest():
                 cp = ctx[f].renamed()
-                self.assertEqual(bool(cp), bool(copymap.get(f)))
+                self.assertEqual(bool(cp), bool(copymap.get(f)),
+                                 'copy records differ for %s in %d' % (f, rev))
                 if not cp:
                     continue
                 self.assertEqual(cp[0], copymap[f][0])