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
--- 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')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-unamend.t Sun Sep 24 00:56:52 2017 +0530
@@ -0,0 +1,366 @@
+Test for command `hg unamend` which lives in uncommit extension
+===============================================================
+
+ $ cat >> $HGRCPATH << EOF
+ > [alias]
+ > glog = log -G -T '{rev}:{node|short} {desc}'
+ > [experimental]
+ > evolution = createmarkers, allowunstable
+ > [extensions]
+ > rebase =
+ > amend =
+ > uncommit =
+ > EOF
+
+Repo Setup
+
+ $ hg init repo
+ $ cd repo
+ $ for ch in {a..h}; do touch $ch; echo "foo" >> $ch; hg ci -Aqm "Added "$ch; done
+
+ $ hg glog
+ @ 7:ec2426147f0e Added h
+ |
+ o 6:87d6d6676308 Added g
+ |
+ o 5:825660c69f0c Added f
+ |
+ o 4:aa98ab95a928 Added e
+ |
+ o 3:62615734edd5 Added d
+ |
+ o 2:28ad74487de9 Added c
+ |
+ o 1:29becc82797a Added b
+ |
+ o 0:18d04c59bb5d Added a
+
+Trying to unamend when there was no amend done
+
+ $ hg unamend
+ abort: changeset must have one predecessor, found 0 predecessors
+ [255]
+
+Unamend on clean wdir and tip
+
+ $ echo "bar" >> h
+ $ hg amend
+
+ $ hg exp
+ # HG changeset patch
+ # User test
+ # Date 0 0
+ # Thu Jan 01 00:00:00 1970 +0000
+ # Node ID c9fa1a715c1b7661c0fafb362a9f30bd75878d7d
+ # Parent 87d6d66763085b629e6d7ed56778c79827273022
+ Added h
+
+ diff -r 87d6d6676308 -r c9fa1a715c1b h
+ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+ +++ b/h Thu Jan 01 00:00:00 1970 +0000
+ @@ -0,0 +1,2 @@
+ +foo
+ +bar
+
+ $ hg glog --hidden
+ @ 8:c9fa1a715c1b Added h
+ |
+ | x 7:ec2426147f0e Added h
+ |/
+ o 6:87d6d6676308 Added g
+ |
+ o 5:825660c69f0c Added f
+ |
+ o 4:aa98ab95a928 Added e
+ |
+ o 3:62615734edd5 Added d
+ |
+ o 2:28ad74487de9 Added c
+ |
+ o 1:29becc82797a Added b
+ |
+ o 0:18d04c59bb5d Added a
+
+ $ hg unamend
+ $ hg glog --hidden
+ @ 9:8da14a1fd653 Added h
+ |
+ | x 8:c9fa1a715c1b Added h
+ |/
+ | x 7:ec2426147f0e Added h
+ |/
+ o 6:87d6d6676308 Added g
+ |
+ o 5:825660c69f0c Added f
+ |
+ o 4:aa98ab95a928 Added e
+ |
+ o 3:62615734edd5 Added d
+ |
+ o 2:28ad74487de9 Added c
+ |
+ o 1:29becc82797a Added b
+ |
+ o 0:18d04c59bb5d Added a
+
+ $ hg diff
+ diff -r 8da14a1fd653 h
+ --- a/h Thu Jan 01 00:00:00 1970 +0000
+ +++ b/h Thu Jan 01 00:00:00 1970 +0000
+ @@ -1,1 +1,2 @@
+ foo
+ +bar
+
+ $ hg exp
+ # HG changeset patch
+ # User test
+ # Date 0 0
+ # Thu Jan 01 00:00:00 1970 +0000
+ # Node ID 8da14a1fd653c3f07fdad5760511c9e12652a306
+ # Parent 87d6d66763085b629e6d7ed56778c79827273022
+ Added h
+
+ diff -r 87d6d6676308 -r 8da14a1fd653 h
+ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+ +++ b/h Thu Jan 01 00:00:00 1970 +0000
+ @@ -0,0 +1,1 @@
+ +foo
+
+ $ hg status
+ M h
+
+ $ hg log -r . -T '{extras % "{extra}\n"}' --config alias.log=log
+ branch=default
+ unamend_source=\xc9\xfa\x1aq\\\x1bva\xc0\xfa\xfb6*\x9f0\xbdu\x87\x8d}
+
+Using unamend to undo an unamed (intentional)
+
+ $ hg unamend
+ $ hg exp
+ # HG changeset patch
+ # User test
+ # Date 0 0
+ # Thu Jan 01 00:00:00 1970 +0000
+ # Node ID 1c09887216571a9755fc9d2e7f0e41c2b49e341b
+ # Parent 87d6d66763085b629e6d7ed56778c79827273022
+ Added h
+
+ diff -r 87d6d6676308 -r 1c0988721657 h
+ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+ +++ b/h Thu Jan 01 00:00:00 1970 +0000
+ @@ -0,0 +1,2 @@
+ +foo
+ +bar
+ $ hg diff
+
+Unamend on a dirty working directory
+
+ $ echo "bar" >> a
+ $ hg amend
+ $ echo "foobar" >> a
+ $ echo "bar" >> b
+ $ hg status
+ M a
+ M b
+
+ $ hg unamend
+
+ $ hg status
+ M a
+ M b
+
+ $ hg diff
+ diff -r 956548224719 a
+ --- a/a Thu Jan 01 00:00:00 1970 +0000
+ +++ b/a Thu Jan 01 00:00:00 1970 +0000
+ @@ -1,1 +1,3 @@
+ foo
+ +bar
+ +foobar
+ diff -r 956548224719 b
+ --- a/b Thu Jan 01 00:00:00 1970 +0000
+ +++ b/b Thu Jan 01 00:00:00 1970 +0000
+ @@ -1,1 +1,2 @@
+ foo
+ +bar
+
+Unamending an added file
+
+ $ hg ci -m "Added things to a and b"
+ $ echo foo > bar
+ $ hg add bar
+ $ hg amend
+
+ $ hg unamend
+ $ hg status
+ A bar
+
+ $ hg revert --all
+ forgetting bar
+
+Unamending a removed file
+
+ $ hg remove a
+ $ hg amend
+
+ $ hg unamend
+ $ hg status
+ R a
+ ? bar
+
+ $ hg revert --all
+ undeleting a
+
+Unamending an added file with dirty wdir status
+
+ $ hg add bar
+ $ hg amend
+ $ echo bar >> bar
+ $ hg status
+ M bar
+
+ $ hg unamend
+ $ hg status
+ A bar
+ $ hg diff
+ diff -r ff917aa01c86 bar
+ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+ +++ b/bar Thu Jan 01 00:00:00 1970 +0000
+ @@ -0,0 +1,2 @@
+ +foo
+ +bar
+
+ $ hg revert --all
+ forgetting bar
+
+Unamending in middle of a stack
+
+ $ hg glog
+ @ 19:ff917aa01c86 Added things to a and b
+ |
+ o 12:956548224719 Added h
+ |
+ o 6:87d6d6676308 Added g
+ |
+ o 5:825660c69f0c Added f
+ |
+ o 4:aa98ab95a928 Added e
+ |
+ o 3:62615734edd5 Added d
+ |
+ o 2:28ad74487de9 Added c
+ |
+ o 1:29becc82797a Added b
+ |
+ o 0:18d04c59bb5d Added a
+
+ $ hg up 5
+ 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
+ $ echo bar >> f
+ $ hg amend
+ $ hg rebase -s 6 -d . -q
+
+ $ hg glog
+ o 23:2b41b42e192a Added things to a and b
+ |
+ o 22:1860df151f01 Added h
+ |
+ o 21:49635b68477e Added g
+ |
+ @ 20:93f0e8ffab32 Added f
+ |
+ o 4:aa98ab95a928 Added e
+ |
+ o 3:62615734edd5 Added d
+ |
+ o 2:28ad74487de9 Added c
+ |
+ o 1:29becc82797a Added b
+ |
+ o 0:18d04c59bb5d Added a
+
+
+ $ hg unamend
+ abort: cannot unamend a changeset with children
+ [255]
+
+Trying to unamend a public changeset
+
+ $ hg up
+ 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ hg phase -r . -p
+ $ hg unamend
+ abort: cannot unamend public changesets
+ [255]
+
+Testing whether unamend retains copies or not
+
+ $ hg status
+ ? bar
+
+ $ hg mv a foo
+
+ $ hg ci -m "Moved a to foo"
+ $ hg exp --git
+ # HG changeset patch
+ # User test
+ # Date 0 0
+ # Thu Jan 01 00:00:00 1970 +0000
+ # Node ID b087c66cada118d6c5487d3d7cb29cac239bd98a
+ # Parent 2b41b42e192adc425b106643c3fc89170d6b8add
+ Moved a to foo
+
+ diff --git a/a b/foo
+ rename from a
+ rename to foo
+
+ $ hg mv b foobar
+ $ hg diff --git
+ diff --git a/b b/foobar
+ rename from b
+ rename to foobar
+ $ hg amend
+
+ $ hg exp --git
+ # HG changeset patch
+ # User test
+ # Date 0 0
+ # Thu Jan 01 00:00:00 1970 +0000
+ # Node ID 9fa06fb09a83a86ec7368d15004dbc52ac1a5d2e
+ # Parent 2b41b42e192adc425b106643c3fc89170d6b8add
+ Moved a to foo
+
+ diff --git a/a b/foo
+ rename from a
+ rename to foo
+ diff --git a/b b/foobar
+ rename from b
+ rename to foobar
+
+ $ hg mv c wat
+ $ hg unamend
+
+Retained copies in new prdecessor commit
+
+ $ hg exp --git
+ # HG changeset patch
+ # User test
+ # Date 0 0
+ # Thu Jan 01 00:00:00 1970 +0000
+ # Node ID 4cf9987c9b941f615930e7c46307b4dbf35697cf
+ # Parent 2b41b42e192adc425b106643c3fc89170d6b8add
+ Moved a to foo
+
+ diff --git a/a b/foo
+ rename from a
+ rename to foo
+
+Retained copies in working directoy
+
+ $ hg diff --git
+ diff --git a/b b/foobar
+ rename from b
+ rename to foobar
+ diff --git a/c b/wat
+ rename from c
+ rename to wat