changeset 952:9c3b4f59e7e6

stupid: do not close branch upon branch-wide revert Reverting a branch with a remove followed by a copy results in a branch replacement. By default, branch replacements are handled by closing the replaced branch and committing the new branch on top of it. But we do not really want that when reverting a branch, we only want a linear history with a changeset capturing the revert.
author Patrick Mezard <patrick@mezard.eu>
date Tue, 16 Oct 2012 21:17:55 +0200
parents bd9c292665fd
children 3b43b1c45e2e
files hgsubversion/editor.py hgsubversion/stupid.py hgsubversion/svnmeta.py hgsubversion/util.py tests/fixtures/revert.sh tests/fixtures/revert.svndump tests/test_fetch_command.py tests/test_util.py
diffstat 8 files changed, 327 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/editor.py
+++ b/hgsubversion/editor.py
@@ -421,10 +421,6 @@ class HgEditor(svnwrap.Editor):
         else:
             source_rev = copyfrom_revision
             frompath, source_branch = self.meta.split_branch_path(copyfrom_path)[:2]
-            if frompath == '' and br_path == '':
-                assert br_path is not None
-                tmp = source_branch, source_rev, self.current.rev.revnum
-                self.meta.branches[branch] = tmp
         new_hash = self.meta.get_parent_revision(source_rev + 1, source_branch, True)
         if new_hash == node.nullid:
             self.current.addmissing('%s/' % path)
@@ -444,6 +440,14 @@ class HgEditor(svnwrap.Editor):
                 # are the same, not need to do anything but restore
                 # files marked as deleted.
                 copyfromparent = True
+            # Get the parent which would have been used for this branch
+            # without the replace action.
+            oldpnode = self.meta.get_parent_revision(
+                    self.current.rev.revnum, branch, exact=True)
+            if (oldpnode != revlog.nullid
+                    and util.isancestor(self._getctx(oldpnode), fromctx)):
+                # Branch-wide replacement, unmark the branch as deleted
+                self.meta.closebranches.discard(branch)
 
         svncopies = {}
         copies = {}
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -539,7 +539,12 @@ def fetch_branchrev(svn, meta, branch, b
     else:
         branchprefix = (branchpath and branchpath + '/') or ''
         for path, e in r.paths.iteritems():
-            if not path.startswith(branchprefix):
+            if path == branchpath:
+                if e.action != 'R' or branch not in meta.branches:
+                    # Full-branch replacements are handled as reverts,
+                    # skip everything else.
+                    continue
+            elif not path.startswith(branchprefix):
                 continue
             if not meta.is_path_valid(path):
                 continue
@@ -703,6 +708,20 @@ def convert_rev(ui, meta, svn, r, tbdelt
         branch = meta.localname(p)
         if not (r.paths[p].action == 'R' and branch in meta.branches):
             continue
+        # Check the branch is not being replaced by one of its
+        # ancestors, it happens a lot with project-wide reverts.
+        frompath = r.paths[p].copyfrom_path
+        frompath, frombranch = meta.split_branch_path(
+            frompath, existing=False)[:2]
+        if frompath == '':
+            fromnode = meta.get_parent_revision(
+                    r.paths[p].copyfrom_rev + 1, frombranch, exact=True)
+            if fromnode != node.nullid:
+                fromctx = meta.repo[fromnode]
+                pctx = meta.repo[meta.get_parent_revision(
+                    r.revnum, branch, exact=True)]
+                if util.isancestor(pctx, fromctx):
+                    continue
         closed = checkbranch(meta, r, branch)
         if closed is not None:
             deleted_branches[branch] = closed
--- a/hgsubversion/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -544,7 +544,9 @@ class SVNMeta(object):
             # 1. Is the file located inside any currently known
             #    branch?  If yes, then we're done with it, this isn't
             #    interesting.
-            # 2. Does the file have copyfrom information? If yes, then
+            # 2. Does the file have copyfrom information? If yes, and
+            #    the branch is being replaced by what would be an
+            #    ancestor, treat it as a regular revert. Otherwise,
             #    we're done: this is a new branch, and we record the
             #    copyfrom in added_branches if it comes from the root
             #    of another branch, or create it from scratch.
@@ -565,6 +567,18 @@ class SVNMeta(object):
                     if paths[p].action == 'D':
                         self.closebranches.add(br) # case 4
                     elif paths[p].action == 'R':
+                        # Check the replacing source is not an ancestor
+                        # branch of the branch being replaced, this
+                        # would just be a revert.
+                        cfi, cbr = self.split_branch_path(
+                            paths[p].copyfrom_path, paths[p].copyfrom_rev)[:2]
+                        if cfi == '':
+                            cctx = self.repo[self.get_parent_revision(
+                                paths[p].copyfrom_rev + 1, cbr)]
+                            ctx = self.repo[self.get_parent_revision(
+                                revision.revnum, br)]
+                            if cctx and util.isancestor(ctx, cctx):
+                                continue
                         parent = self._determine_parent_branch(
                             p, paths[p].copyfrom_path, paths[p].copyfrom_rev,
                             revision.revnum)
--- a/hgsubversion/util.py
+++ b/hgsubversion/util.py
@@ -212,6 +212,14 @@ def swap_out_encoding(new_encoding="UTF-
     encoding.encoding = new_encoding
     return old
 
+def isancestor(ctx, ancestorctx):
+    """Return True if ancestorctx is equal or an ancestor of ctx."""
+    if ctx == ancestorctx:
+        return True
+    for actx in ctx.ancestors():
+        if actx == ancestorctx:
+            return True
+    return False
 
 def issamefile(parentctx, childctx, f):
     """Return True if f exists and is the same in childctx and parentctx"""
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/revert.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+#
+# Generate revert.svndump
+#
+
+rm -rf temp
+mkdir temp
+cd temp
+mkdir -p import/trunk/dir
+cd import/trunk
+echo a > a
+echo b > dir/b
+cd ../..
+
+svnadmin create testrepo
+svnurl=file://`pwd`/testrepo
+svn import import $svnurl -m init
+
+svn co $svnurl project
+cd project
+echo a >> trunk/a
+echo b >> trunk/dir/b
+svn ci -m changefiles
+svn up
+# Test directory revert
+svn rm trunk
+svn cp $svnurl/trunk@1 trunk
+svn st
+svn ci -m revert
+svn up
+# Test file revert
+svn rm trunk/a
+svn rm trunk/dir/b
+svn cp $svnurl/trunk/a@2 trunk/a
+svn cp $svnurl/trunk/dir/b@2 trunk/dir/b
+svn ci -m revert2
+cd ..
+
+svnadmin dump testrepo > ../revert.svndump
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/revert.svndump
@@ -0,0 +1,197 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 307f02f4-2d74-44cb-98a4-4e162241d396
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2012-10-06T08:50:46.559327Z
+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-06T08:50:46.581582Z
+K 7
+svn:log
+V 4
+init
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+Content-length: 12
+
+PROPS-END
+a
+
+
+Node-path: trunk/dir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/dir/b
+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
+
+
+Revision-number: 2
+Prop-content-length: 113
+Content-length: 113
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-06T08:50:47.048033Z
+K 7
+svn:log
+V 11
+changefiles
+PROPS-END
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: change
+Text-content-length: 4
+Text-content-md5: 0d227f1abf8c2932d342e9b99cc957eb
+Text-content-sha1: d7c8127a20a396cff08af086a1c695b0636f0c29
+Content-length: 4
+
+a
+a
+
+
+Node-path: trunk/dir/b
+Node-kind: file
+Node-action: change
+Text-content-length: 4
+Text-content-md5: 06ac26ed8b614fc0b141e4542aa067c2
+Text-content-sha1: f6980469e74f7125178e88ec571e06fe6ce86e95
+Content-length: 4
+
+b
+b
+
+
+Revision-number: 3
+Prop-content-length: 107
+Content-length: 107
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-06T08:50:50.058224Z
+K 7
+svn:log
+V 6
+revert
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: delete
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+
+
+Revision-number: 4
+Prop-content-length: 108
+Content-length: 108
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-06T08:50:54.047396Z
+K 7
+svn:log
+V 7
+revert2
+PROPS-END
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: delete
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/a
+Text-copy-source-md5: 0d227f1abf8c2932d342e9b99cc957eb
+Text-copy-source-sha1: d7c8127a20a396cff08af086a1c695b0636f0c29
+
+
+
+
+Node-path: trunk/dir/b
+Node-kind: file
+Node-action: delete
+
+Node-path: trunk/dir/b
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/dir/b
+Text-copy-source-md5: 06ac26ed8b614fc0b141e4542aa067c2
+Text-copy-source-sha1: f6980469e74f7125178e88ec571e06fe6ce86e95
+
+
+
+
--- a/tests/test_fetch_command.py
+++ b/tests/test_fetch_command.py
@@ -237,6 +237,39 @@ class TestStupidPull(test_util.TestBase)
     def test_empty_repo_stupid(self):
         self.test_empty_repo(stupid=True)
 
+    def test_fetch_revert(self, stupid=False):
+        repo = self._load_fixture_and_fetch('revert.svndump', stupid=stupid)
+        graph = self.getgraph(repo)
+        refgraph = """\
+o  changeset: 3:937dcd1206d4
+|  branch:
+|  tags:      tip
+|  summary:   revert2
+|  files:     a dir/b
+|
+o  changeset: 2:9317a748b7c3
+|  branch:
+|  tags:
+|  summary:   revert
+|  files:     a dir/b
+|
+o  changeset: 1:243259a4138a
+|  branch:
+|  tags:
+|  summary:   changefiles
+|  files:     a dir/b
+|
+o  changeset: 0:ab86791fc857
+   branch:
+   tags:
+   summary:   init
+   files:     a dir/b
+"""
+        self.assertEqual(refgraph.strip(), graph.strip())
+
+    def test_fetch_revert_stupid(self):
+        self.test_fetch_revert(stupid=True)
+
 def suite():
     all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestBasicRepoLayout),
            unittest.TestLoader().loadTestsFromTestCase(TestStupidPull),
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -520,7 +520,7 @@ class TestBase(unittest.TestCase):
             msg = '%s\n%s' % (msg or '', diff)
             raise self.failureException, msg
 
-    def draw(self, repo):
+    def getgraph(self, repo):
         """Helper function displaying a repository graph, especially
         useful when debugging comprehensive tests.
         """
@@ -537,4 +537,10 @@ summary:   {desc|firstline}
 files:     {files}
 
 """
+        _ui.pushbuffer()
         graphlog.graphlog(_ui, repo, rev=None, template=templ)
+        return _ui.popbuffer()
+
+    def draw(self, repo):
+        sys.stdout.write(self.getgraph(repo))
+