diff -r d4805a5e7e70 -r 867990238dc6 hgext/uncommit.py --- 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')