--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/rewind.py Thu Feb 01 18:26:56 2018 +0100
@@ -0,0 +1,150 @@
+from __future__ import absolute_import
+
+import random
+
+from mercurial import (
+ error,
+ hg,
+ lock as lockmod,
+ obsolete,
+ rewriteutil,
+ scmutil,
+)
+
+from mercurial.i18n import _
+
+from . import (
+ exthelper,
+ rewriteutil,
+)
+
+eh = exthelper.exthelper()
+
+@eh.command(
+ '^rewind',
+ [('r', 'rev', [], _("revision to rewind")),
+ ],
+ _('[-r] [REV]'))
+def rewind(ui, repo, *rev, **kwargs):
+ """rewind let you restore a topological head closest predecessor. It main
+ use-case is cancel the latest history rewriting operation, for example
+ amend. It cannot handle rebased stacks yet.
+ """
+ unfilteredrepo = repo.unfiltered()
+ currentwdirparent = repo['.']
+ revs = scmutil.revrange(repo, kwargs.get('rev', []))
+
+ # Check that we can rewind these changesets
+ rewriteutil.precheck(repo, revs, action="rewind")
+
+ # Check that we are not rebasing non-heads for the moment
+ ndrevs = list(repo.revs('descendants(%ld)', revs))
+ if len(ndrevs) != len(revs):
+ raise error.Abort('rewind only support rewinding heads')
+
+ # Warning if we will leave bookmarks behind
+ notbookmarked = list(repo.revs('%ld - bookmark()', revs))
+ if len(notbookmarked) != len(revs):
+ for rev in revs:
+ ctx = repo[rev]
+ if ctx.bookmarks():
+ for bookmark in ctx.bookmarks():
+ ui.warn("Bookmark %r will be left behind\n" % bookmark)
+
+ revisionstorewind = _getrewindrevisions(revs, unfilteredrepo)
+
+ newrevs = {}
+
+ with repo.wlock(), repo.lock(), repo.transaction('rewind'):
+ for base, revision in revisionstorewind:
+ newrev = _rewindrevision(base, revision, unfilteredrepo)
+ newrevs[base] = newrev
+
+ # Finally update to the new changeset if we rewinded the current wdir
+ # parent
+ if currentwdirparent in newrevs:
+ hg.updaterepo(repo, newrevs[currentwdirparent], False)
+
+def _getrewindrevisions(revs, repo):
+ """returns the list of revisions ordered in a way that each revision
+ parents is either stable or is present before in the returned list
+ """
+
+ # We only support a single base revision for the moment
+ if len(revs) != 1:
+ raise error.Abort('rewind only support rewinding a single head')
+
+ base = list(revs)[0]
+ base = repo[base]
+
+ # Get the predecessor
+ predecessors = repo.obsstore.predecessors
+
+ preds = predecessors.get(base.node(), ())
+
+ if len(preds) == 0:
+ # Changeset has no predecessor, abort
+ raise error.Abort('revision %s has no predecessor' % base)
+ elif len(preds) > 1:
+ raise error.Abort('revision %s has too many predecessors' % base)
+
+ # Only support simple evolution flow for the moment
+ assert len(preds) == 1
+
+ # Take the predecessor as the start
+ nodes = [list(preds)[0][0]]
+ seen = set()
+ rewindrevisions = []
+
+ while nodes:
+ node = nodes[-1]
+ ctx = repo[node]
+
+ stable = True
+
+ # Check the parents
+ p1 = ctx.p1()
+ if p1 and p1.obsolete():
+ stable = False
+ nodes.append(p1)
+
+ p2 = ctx.p2()
+ if p2 and p2.obsolete():
+ stable = False
+ nodes.append(p2)
+
+ if stable is False:
+ continue
+
+ # If both parents are stable or in the list, add it to the plan
+ rewindrevisions.append(ctx)
+ nodes.pop()
+
+ return [(base, single_predecessor)]
+
+def _rewindrevision(base, ctx, repo):
+ """rewind a single revision named ctx. Base is the revision to mark as
+ obsolete
+ """
+ extra = ctx.extra().copy()
+ extra['__rewind-base__'] = base.node()
+
+ p1 = ctx.p1().node()
+ p1ctx = repo[p1]
+ p2 = ctx.p2().node()
+ p2ctx = repo[p2]
+
+ # Check that we are not creating orphans
+ if p1ctx.obsolete() or p1ctx.isunstable() or p2ctx.obsolete() or p2ctx.isunstable():
+ raise error.Abort("rewind don't works for rebased changesets yet")
+
+ extradict = {'extra': extra}
+
+ new, unusedvariable = rewriteutil.rewrite(repo, ctx, [], ctx,
+ [p1, p2],
+ commitopts=extradict)
+
+ # And create a obsmarker between the new node and the base
+ obsolete.createmarkers(repo, [(base, (repo[new],))])
+
+ return new