rewriteutil: check for divergence
authorMartin von Zweigbergk <martinvonz@google.com>
Tue, 23 Feb 2021 10:28:42 -0800
changeset 47020 ba6881c6a178
parent 47019 c4dbbaecaad3
child 47021 45dcb63f8ead
rewriteutil: check for divergence This code is adapted from the code in the evolve extension. It seems to be equivalent as far as the evolve extension's test suite can tell (the only impact when making their `precheck()` delegate to our version is that error messages are less detailed). I had to change the error message to work with "change branch of" being inserted as the action. Differential Revision: https://phab.mercurial-scm.org/D10518
hgext/rebase.py
mercurial/obsolete.py
mercurial/rewriteutil.py
tests/test-amend.t
tests/test-branch-change.t
tests/test-obshistory.t
tests/test-obsmarker-template.t
tests/test-unamend.t
--- a/hgext/rebase.py	Wed Apr 28 08:48:10 2021 -0700
+++ b/hgext/rebase.py	Tue Feb 23 10:28:42 2021 -0800
@@ -446,8 +446,15 @@
             rebaseset = set(destmap.keys())
             rebaseset -= set(self.obsolete_with_successor_in_destination)
             rebaseset -= self.obsolete_with_successor_in_rebase_set
+            # We have our own divergence-checking in the rebase extension
+            overrides = {}
+            if obsolete.isenabled(self.repo, obsolete.createmarkersopt):
+                overrides = {
+                    (b'experimental', b'evolution.allowdivergence'): b'true'
+                }
             try:
-                rewriteutil.precheck(self.repo, rebaseset, action=b'rebase')
+                with self.ui.configoverride(overrides):
+                    rewriteutil.precheck(self.repo, rebaseset, action=b'rebase')
             except error.Abort as e:
                 if e.hint is None:
                     e.hint = _(b'use --keep to keep original changesets')
--- a/mercurial/obsolete.py	Wed Apr 28 08:48:10 2021 -0700
+++ b/mercurial/obsolete.py	Tue Feb 23 10:28:42 2021 -0800
@@ -106,6 +106,7 @@
 # Options for obsolescence
 createmarkersopt = b'createmarkers'
 allowunstableopt = b'allowunstable'
+allowdivergenceopt = b'allowdivergence'
 exchangeopt = b'exchange'
 
 
@@ -144,10 +145,13 @@
 
     createmarkersvalue = _getoptionvalue(repo, createmarkersopt)
     unstablevalue = _getoptionvalue(repo, allowunstableopt)
+    divergencevalue = _getoptionvalue(repo, allowdivergenceopt)
     exchangevalue = _getoptionvalue(repo, exchangeopt)
 
     # createmarkers must be enabled if other options are enabled
-    if (unstablevalue or exchangevalue) and not createmarkersvalue:
+    if (
+        unstablevalue or divergencevalue or exchangevalue
+    ) and not createmarkersvalue:
         raise error.Abort(
             _(
                 b"'createmarkers' obsolete option must be enabled "
@@ -158,6 +162,7 @@
     return {
         createmarkersopt: createmarkersvalue,
         allowunstableopt: unstablevalue,
+        allowdivergenceopt: divergencevalue,
         exchangeopt: exchangevalue,
     }
 
--- a/mercurial/rewriteutil.py	Wed Apr 28 08:48:10 2021 -0700
+++ b/mercurial/rewriteutil.py	Tue Feb 23 10:28:42 2021 -0800
@@ -44,7 +44,9 @@
         revs = (r.rev() for r in revs)
 
     if len(repo[None].parents()) > 1:
-        raise error.StateError(_(b"cannot %s changesets while merging") % action)
+        raise error.StateError(
+            _(b"cannot %s changesets while merging") % action
+        )
 
     publicrevs = repo.revs(b'%ld and public()', revs)
     if publicrevs:
@@ -59,6 +61,38 @@
             _(b"cannot %s changeset with children") % action, hint=hint
         )
 
+    if not obsolete.isenabled(repo, obsolete.allowdivergenceopt):
+        new_divergence = _find_new_divergence(repo, revs)
+        if new_divergence:
+            local_ctx, other_ctx, base_ctx = new_divergence
+            msg = _(
+                b'cannot %s %s, as that creates content-divergence with %s'
+            ) % (
+                action,
+                local_ctx,
+                other_ctx,
+            )
+            if local_ctx.rev() != base_ctx.rev():
+                msg += _(b', from %s') % base_ctx
+            if repo.ui.verbose:
+                if local_ctx.rev() != base_ctx.rev():
+                    msg += _(
+                        b'\n    changeset %s is a successor of ' b'changeset %s'
+                    ) % (local_ctx, base_ctx)
+                msg += _(
+                    b'\n    changeset %s already has a successor in '
+                    b'changeset %s\n'
+                    b'    rewriting changeset %s would create '
+                    b'"content-divergence"\n'
+                    b'    set experimental.evolution.allowdivergence=True to '
+                    b'skip this check'
+                ) % (base_ctx, other_ctx, local_ctx)
+                raise error.InputError(msg)
+            else:
+                raise error.InputError(
+                    msg, hint=_(b"add --verbose for details")
+                )
+
 
 def disallowednewunstable(repo, revs):
     """Checks whether editing the revs will create new unstable changesets and
@@ -73,6 +107,40 @@
     return repo.revs(b"(%ld::) - %ld", revs, revs)
 
 
+def _find_new_divergence(repo, revs):
+    obsrevs = repo.revs(b'%ld and obsolete()', revs)
+    for r in obsrevs:
+        div = find_new_divergence_from(repo, repo[r])
+        if div:
+            return (repo[r], repo[div[0]], repo[div[1]])
+    return None
+
+
+def find_new_divergence_from(repo, ctx):
+    """return divergent revision if rewriting an obsolete cset (ctx) will
+    create divergence
+
+    Returns (<other node>, <common ancestor node>) or None
+    """
+    if not ctx.obsolete():
+        return None
+    # We need to check two cases that can cause divergence:
+    # case 1: the rev being rewritten has a non-obsolete successor (easily
+    #     detected by successorssets)
+    sset = obsutil.successorssets(repo, ctx.node())
+    if sset:
+        return (sset[0][0], ctx.node())
+    else:
+        # case 2: one of the precursors of the rev being revived has a
+        #     non-obsolete successor (we need divergentsets for this)
+        divsets = obsutil.divergentsets(repo, ctx)
+        if divsets:
+            nsuccset = divsets[0][b'divergentnodes']
+            prec = divsets[0][b'commonpredecessor']
+            return (nsuccset[0], prec)
+        return None
+
+
 def skip_empty_successor(ui, command):
     empty_successor = ui.config(b'rewrite', b'empty-successor')
     if empty_successor == b'skip':
--- a/tests/test-amend.t	Wed Apr 28 08:48:10 2021 -0700
+++ b/tests/test-amend.t	Tue Feb 23 10:28:42 2021 -0800
@@ -232,6 +232,17 @@
   $ hg debugobsolete -r .
   112478962961147124edd43549aedd1a335e44bf be169c7e8dbe21cd10b3d79691cbe7f241e3c21c 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'operation': 'amend', 'user': 'test'}
   be169c7e8dbe21cd10b3d79691cbe7f241e3c21c 16084da537dd8f84cfdb3055c633772269d62e1b 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'note': 'adding bar', 'operation': 'amend', 'user': 'test'}
+
+Cannot cause divergence by default
+
+  $ hg co --hidden 1
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg amend -m divergent
+  abort: cannot amend 112478962961, as that creates content-divergence with 16084da537dd
+  (add --verbose for details)
+  [10]
+  $ hg amend -m divergent --config experimental.evolution.allowdivergence=true
+  2 new content-divergent changesets
 #endif
 
 Cannot amend public changeset
--- a/tests/test-branch-change.t	Wed Apr 28 08:48:10 2021 -0700
+++ b/tests/test-branch-change.t	Tue Feb 23 10:28:42 2021 -0800
@@ -150,7 +150,8 @@
   [255]
 
   $ hg branch -r 4 --hidden foobar
-  abort: cannot change branch of a obsolete changeset
+  abort: cannot change branch of 3938acfb5c0f, as that creates content-divergence with 7c1991464886
+  (add --verbose for details)
   [10]
 
 Make sure bookmark movement is correct
--- a/tests/test-obshistory.t	Wed Apr 28 08:48:10 2021 -0700
+++ b/tests/test-obshistory.t	Tue Feb 23 10:28:42 2021 -0800
@@ -13,6 +13,7 @@
   > [experimental]
   > evolution.createmarkers = yes
   > evolution.effect-flags = yes
+  > evolution.allowdivergence=true
   > EOF
 
 Test output on amended commit
--- a/tests/test-obsmarker-template.t	Wed Apr 28 08:48:10 2021 -0700
+++ b/tests/test-obsmarker-template.t	Tue Feb 23 10:28:42 2021 -0800
@@ -11,6 +11,7 @@
   > publish=False
   > [experimental]
   > evolution=true
+  > evolution.allowdivergence=true
   > [templates]
   > obsfatesuccessors = "{if(successors, " as ")}{join(successors, ", ")}"
   > obsfateverb = "{obsfateverb(successors, markers)}"
--- a/tests/test-unamend.t	Wed Apr 28 08:48:10 2021 -0700
+++ b/tests/test-unamend.t	Tue Feb 23 10:28:42 2021 -0800
@@ -6,6 +6,7 @@
   > glog = log -G -T '{rev}:{node|short}  {desc}'
   > [experimental]
   > evolution = createmarkers, allowunstable
+  > evolution.allowdivergence = true
   > [extensions]
   > rebase =
   > amend =