diff hgext/uncommit.py @ 35193:867990238dc6

unamend: move fb extension unamend to core unamend extension adds an unamend command which undoes the effect of the amend command. This patch moves the unamend command from that extension to uncommit extension and this one does not completely undoes the effect of amend command as it creates a new commit, rather than reviving the old one back. This also adds tests for the same. .. feature:: A new unamend command in uncommit extension which undoes the effect of the amend command by creating a new changeset which was there before amend and moving the changes that were amended to the working directory. Differential Revision: https://phab.mercurial-scm.org/D821
author Pulkit Goyal <7895pulkit@gmail.com>
date Sun, 24 Sep 2017 00:56:52 +0530
parents 3ebae3ec4664
children 9dadcb99cc17
line wrap: on
line diff
--- a/hgext/uncommit.py	Tue Nov 28 13:27:43 2017 -0500
+++ b/hgext/uncommit.py	Sun Sep 24 00:56:52 2017 +0530
@@ -29,6 +29,7 @@
     error,
     node,
     obsolete,
+    obsutil,
     pycompat,
     registrar,
     scmutil,
@@ -194,3 +195,124 @@
             with repo.dirstate.parentchange():
                 repo.dirstate.setparents(newid, node.nullid)
                 _uncommitdirstate(repo, old, match)
+
+def predecessormarkers(ctx):
+    """yields the obsolete markers marking the given changeset as a successor"""
+    for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
+        yield obsutil.marker(ctx.repo(), data)
+
+def _unamenddirstate(repo, predctx, curctx):
+    """"""
+
+    s = repo.status(predctx, curctx)
+    ds = repo.dirstate
+    copies = dict(ds.copies())
+    for f in s.modified:
+        if ds[f] == 'r':
+            # modified + removed -> removed
+            continue
+        ds.normallookup(f)
+
+    for f in s.added:
+        if ds[f] == 'r':
+            # added + removed -> unknown
+            ds.drop(f)
+        elif ds[f] != 'a':
+            ds.add(f)
+
+    for f in s.removed:
+        if ds[f] == 'a':
+            # removed + added -> normal
+            ds.normallookup(f)
+        elif ds[f] != 'r':
+            ds.remove(f)
+
+    # Merge old parent and old working dir copies
+    oldcopies = {}
+    for f in (s.modified + s.added):
+        src = curctx[f].renamed()
+        if src:
+            oldcopies[f] = src[0]
+    oldcopies.update(copies)
+    copies = dict((dst, oldcopies.get(src, src))
+                  for dst, src in oldcopies.iteritems())
+    # Adjust the dirstate copies
+    for dst, src in copies.iteritems():
+        if (src not in predctx or dst in predctx or ds[dst] != 'a'):
+            src = None
+        ds.copy(src, dst)
+
+@command('^unamend', [])
+def unamend(ui, repo, **opts):
+    """
+    undo the most recent amend operation on a current changeset
+
+    This command will roll back to the previous version of a changeset,
+    leaving working directory in state in which it was before running
+    `hg amend` (e.g. files modified as part of an amend will be
+    marked as modified `hg status`)
+    """
+
+    unfi = repo.unfiltered()
+
+    # identify the commit from which to unamend
+    curctx = repo['.']
+
+    with repo.wlock(), repo.lock(), repo.transaction('unamend'):
+        if not curctx.mutable():
+            raise error.Abort(_('cannot unamend public changesets'))
+
+        # identify the commit to which to unamend
+        markers = list(predecessormarkers(curctx))
+        if len(markers) != 1:
+            e = _("changeset must have one predecessor, found %i predecessors")
+            raise error.Abort(e % len(markers))
+
+        prednode = markers[0].prednode()
+        predctx = unfi[prednode]
+
+        if curctx.children():
+            raise error.Abort(_("cannot unamend a changeset with children"))
+
+        # add an extra so that we get a new hash
+        # note: allowing unamend to undo an unamend is an intentional feature
+        extras = predctx.extra()
+        extras['unamend_source'] = curctx.node()
+
+        def filectxfn(repo, ctx_, path):
+            try:
+                return predctx.filectx(path)
+            except KeyError:
+                return None
+
+        # Make a new commit same as predctx
+        newctx = context.memctx(repo,
+                                parents=(predctx.p1(), predctx.p2()),
+                                text=predctx.description(),
+                                files=predctx.files(),
+                                filectxfn=filectxfn,
+                                user=predctx.user(),
+                                date=predctx.date(),
+                                extra=extras)
+        # phase handling
+        commitphase = curctx.phase()
+        overrides = {('phases', 'new-commit'): commitphase}
+        with repo.ui.configoverride(overrides, 'uncommit'):
+            newprednode = repo.commitctx(newctx)
+
+        newpredctx = repo[newprednode]
+
+        changedfiles = []
+        wctx = repo[None]
+        wm = wctx.manifest()
+        cm = newpredctx.manifest()
+        dirstate = repo.dirstate
+        diff = cm.diff(wm)
+        changedfiles.extend(diff.iterkeys())
+
+        with dirstate.parentchange():
+            dirstate.setparents(newprednode, node.nullid)
+            _unamenddirstate(repo, newpredctx, curctx)
+
+        mapping = {curctx.node(): (newprednode,)}
+        scmutil.cleanupnodes(repo, mapping, 'unamend')