diff --git a/.hgtags b/.hgtags
index 1c2e75c9b593d4303ccc78c9609f9e931d3783c6_LmhndGFncw==..94c6158b3bfaf8d3462f17b70fcd16a74fb521b5_LmhndGFncw== 100644
--- a/.hgtags
+++ b/.hgtags
@@ -7,6 +7,7 @@
 fe410e259ed0c62730e97b41daed46b37e203467 heptapod-0.8.1
 923cadf135af823d5ee64ffe8a713fb7a9c89b38 heptapod-0.8.2
 3aa13f3969d61d6722364a14f670a70dda91e117 heptapod-0.8.3
+98527b5cac76f2e9fc5bcb18127fd6cbb2905d2f heptapod-0.8.4
 5b382276649ef2f6daa00f4a570e5ff83fd4020f heptapod-0.12.0.dev0
 085bf3333fb6e18e29e2808fa319f24ffc35bafa heptapod-0.12.0rc1
 62c6cc9cae756436531b9cb7e8c8fdf934b616d9 heptapod-0.12.0rc2
@@ -15,3 +16,5 @@
 8685ff269a05a2bfbcf0548d2710aadbbf9d2fc6 heptapod-0.12.0rc5
 7f98fa9ae5a3282f2df38b1e81f549f1621ccab6 heptapod-0.12.0rc6
 6dbc6fcd63b9b2119c68cf55bec4db4416f3c331 heptapod-0.12.0
+8eb2109d97fbba92b618f10a66f4c4d07e558184 heptapod-0.12.1
+94dbb4a5d46c00d108078146351fb6a9fd69c150 heptapod-0.12.2
diff --git a/heptapod/required.hgrc b/heptapod/required.hgrc
index 1c2e75c9b593d4303ccc78c9609f9e931d3783c6_aGVwdGFwb2QvcmVxdWlyZWQuaGdyYw==..94c6158b3bfaf8d3462f17b70fcd16a74fb521b5_aGVwdGFwb2QvcmVxdWlyZWQuaGdyYw== 100644
--- a/heptapod/required.hgrc
+++ b/heptapod/required.hgrc
@@ -5,6 +5,7 @@
 topic=
 loggingmod =
 share =
+clonebundles =
 
 [logging]
 # see https://dev.heptapod.net/heptapod/hgext-loggingmod/blob/branch/default/README.md
diff --git a/hgext3rd/heptapod/__init__.py b/hgext3rd/heptapod/__init__.py
index 1c2e75c9b593d4303ccc78c9609f9e931d3783c6_aGdleHQzcmQvaGVwdGFwb2QvX19pbml0X18ucHk=..94c6158b3bfaf8d3462f17b70fcd16a74fb521b5_aGdleHQzcmQvaGVwdGFwb2QvX19pbml0X18ucHk= 100644
--- a/hgext3rd/heptapod/__init__.py
+++ b/hgext3rd/heptapod/__init__.py
@@ -24,6 +24,7 @@
 from . import (
     topic as topicmod,
     git,
+    obsutil,
 )
 
 # these have conditional imports and subsequent `None` testing in
@@ -123,6 +124,40 @@
     git.HeptapodGitHandler(repo, repo.ui).export_commits()
 
 
+@command(b'hpd-unique-successor',
+         [(b'r', b'rev', b'', _(b'specified revision'), _(b'REV')),
+          ])
+def unique_successor(ui, repo, rev=None, **opts):
+    """Display the node ID of the obsolescence successor of REV if unique.
+
+    This can be useful after a simple rebase or fold, as a direct way to
+    find the resulting changeset.
+
+    If REV isn't obsolete, the output is REV.
+    If there is any divergence, the command will fail.
+
+    The same functionality can be accomplished with
+    ``hg log -T {successorsets}`` but the latter
+
+    1. won't fail on divergence
+    2. will present as with ``{rev}:{node|short}``, making a second ``hg log``
+       call necessary to get the full hash.
+
+    In the context of the Rails app 1) could be almost acceptable by
+    detecting multiple results and refusing them (that's already some parsing),
+    but together with 2) that's too much, we'll have a
+    better robustness with this simple, fully tested command.
+
+    See also: https://foss.heptapod.net/mercurial/evolve/issues/13
+    """
+    if not rev:
+        raise error.Abort(_(b"Please specify a revision"))
+    # rev would typically be an obsolete revision, we need to see them
+    ctx = scmutil.revsingle(repo.unfiltered(), rev)
+    succ_ctx = obsutil.latest_unique_successor(ctx)
+    ui.write(succ_ctx.hex())
+
+
 def runsystem(orig, ui, cmd, environ, cwd, out):
     heptapod_env = {k: v for k, v in ui.environ.items()
                     if k.startswith('HEPTAPOD_')}
diff --git a/hgext3rd/heptapod/git.py b/hgext3rd/heptapod/git.py
index 1c2e75c9b593d4303ccc78c9609f9e931d3783c6_aGdleHQzcmQvaGVwdGFwb2QvZ2l0LnB5..94c6158b3bfaf8d3462f17b70fcd16a74fb521b5_aGdleHQzcmQvaGVwdGFwb2QvZ2l0LnB5 100644
--- a/hgext3rd/heptapod/git.py
+++ b/hgext3rd/heptapod/git.py
@@ -18,10 +18,9 @@
 from mercurial import (
     error,
     hg,
-    obsutil,
     phases,
     pycompat,
 )
 import re
 import traceback
 
@@ -22,9 +21,11 @@
     phases,
     pycompat,
 )
 import re
 import traceback
 
+from . import obsutil
+
 
 class MultipleDescendants(LookupError):
     pass
@@ -295,7 +296,7 @@
             return self.published_topic_latest_hg_sha(topic, before_ctx)
 
         try:
-            succctx = self.latest_unique_successor(before_ctx)
+            succctx = obsutil.latest_unique_successor(before_ctx)
         except error.Abort as exc:
             if initial_import:
                 # we don't want to break an initial import because
@@ -596,25 +597,3 @@
                 "Can't find public descendants of %r in topic %r" % (
                     ctx.hex(), topic))
         return self.repo[rev]
-
-    def latest_unique_successor(self, ctx):
-        """Return the latest of unique set of successors of a changeset.
-
-        The changeset is given by its context, and a context is also returned,
-        or `None` if there is no successor.
-
-        `Abort` gets raised if there are several sets of successors.
-        """
-        successorssets = obsutil.successorssets(self.repo,
-                                                ctx.node())
-        if not successorssets:
-            return None
-        if len(successorssets) > 1:
-            # There is more than one set of successors, so we have
-            # a divergence, and we cannot handle it automatically
-            raise error.Abort(("Changeset %r evolved into a "
-                               "divergent state") % ctx.hex())
-
-        # Single successor set: it can be one or many (split)
-        # In all cases, we can take the last one.
-        return self.repo[successorssets[0][-1]]
diff --git a/hgext3rd/heptapod/obsutil.py b/hgext3rd/heptapod/obsutil.py
new file mode 100644
index 0000000000000000000000000000000000000000..94c6158b3bfaf8d3462f17b70fcd16a74fb521b5_aGdleHQzcmQvaGVwdGFwb2Qvb2JzdXRpbC5weQ==
--- /dev/null
+++ b/hgext3rd/heptapod/obsutil.py
@@ -0,0 +1,27 @@
+from mercurial import (
+    error,
+    obsutil as hgobsutil,
+)
+
+
+def latest_unique_successor(ctx):
+    """Return the latest of unique set of successors of a changeset.
+
+    The changeset is given by its context, and a context is also returned,
+    or `None` if there is no successor.
+
+    `Abort` gets raised if there are several sets of successors.
+    """
+    repo = ctx.repo()
+    successorssets = hgobsutil.successorssets(repo, ctx.node())
+    if not successorssets:
+        return None
+    if len(successorssets) > 1:
+        # There is more than one set of successors, so we have
+        # a divergence, and we cannot handle it automatically
+        raise error.Abort(("Changeset %r evolved into a "
+                           "divergent state") % ctx.hex())
+
+    # Single successor set: it can be one or many (split)
+    # In all cases, we can take the last one.
+    return repo[successorssets[0][-1]]
diff --git a/hgext3rd/heptapod/tests/test_commands_misc.py b/hgext3rd/heptapod/tests/test_commands_misc.py
new file mode 100644
index 0000000000000000000000000000000000000000..94c6158b3bfaf8d3462f17b70fcd16a74fb521b5_aGdleHQzcmQvaGVwdGFwb2QvdGVzdHMvdGVzdF9jb21tYW5kc19taXNjLnB5
--- /dev/null
+++ b/hgext3rd/heptapod/tests/test_commands_misc.py
@@ -0,0 +1,70 @@
+# Copyright 2019-2020 Georges Racinet <georges.racinet@octobus.net>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+from __future__ import absolute_import
+
+from mercurial import (
+    error,
+    scmutil,
+)
+import pytest
+
+from heptapod.testhelpers import (
+    LocalRepoWrapper,
+)
+from .utils import common_config
+
+
+def test_hpd_unique_successor(tmpdir, monkeypatch):
+    repo_path = tmpdir.join('repo')
+    wrapper = LocalRepoWrapper.init(repo_path, config=common_config())
+    ctx = wrapper.write_commit('foo', message="default0",
+                               return_ctx=True)
+    repo_path.join('foo').write('amend 1')
+    wrapper.command('amend', message='amend1')
+    repo_path.join('foo').write('amend 2')
+    wrapper.command('amend', message='amend2')
+
+    records = []
+
+    def write(*args, **opts):
+        records.append((args, opts))
+
+    wrapper.repo.ui.write = write
+    wrapper.command('hpd-unique-successor', rev=ctx.hex())
+    out = records[0][0][0]
+
+    succ_ctx = scmutil.revsingle(wrapper.repo, out)
+    assert succ_ctx.description() == 'amend2'
+
+
+def test_hpd_unique_successor_divergence(tmpdir, monkeypatch):
+    repo_path = tmpdir.join('repo')
+    config = common_config()
+    config.setdefault('experimental', {})['evolution.allowdivergence'] = 'yes'
+    wrapper = LocalRepoWrapper.init(repo_path, config=config)
+    ctx = wrapper.write_commit('foo', message="default0",
+                               return_ctx=True)
+    repo_path.join('foo').write('amend 1')
+    wrapper.command('amend', message='amend1')
+
+    # let's create the divergence
+    wrapper.update(ctx.hex(), hidden=True)
+    repo_path.join('foo').write('amend 2')
+    wrapper.command('amend', message='amend2')
+
+    with pytest.raises(error.Abort) as exc_info:
+        wrapper.command('hpd-unique-successor', rev=ctx.hex())
+    assert 'divergent' in exc_info.value.args[0]
+
+
+def test_hpd_unique_successor_missing_rev(tmpdir, monkeypatch):
+    repo_path = tmpdir.join('repo')
+    wrapper = LocalRepoWrapper.init(repo_path, config=common_config())
+
+    with pytest.raises(error.Abort) as exc_info:
+        wrapper.command('hpd-unique-successor')
+    assert 'specify a revision' in exc_info.value.args[0]
diff --git a/install-requirements.txt b/install-requirements.txt
index 1c2e75c9b593d4303ccc78c9609f9e931d3783c6_aW5zdGFsbC1yZXF1aXJlbWVudHMudHh0..94c6158b3bfaf8d3462f17b70fcd16a74fb521b5_aW5zdGFsbC1yZXF1aXJlbWVudHMudHh0 100644
--- a/install-requirements.txt
+++ b/install-requirements.txt
@@ -1,4 +1,4 @@
-Mercurial>=5.2
+Mercurial>=5.2,<5.4
 hg-evolve>=9.2.1
 hg-git>=0.8.13
 requests