D529: uncommit: move fb-extension to core which uncommits a changeset
pulkit (Pulkit Goyal)
phabricator at mercurial-scm.org
Mon Sep 11 20:51:05 UTC 2017
pulkit updated this revision to Diff 1729.
pulkit edited the summary of this revision.
REPOSITORY
rHG Mercurial
CHANGES SINCE LAST UPDATE
https://phab.mercurial-scm.org/D529?vs=1712&id=1729
REVISION DETAIL
https://phab.mercurial-scm.org/D529
AFFECTED FILES
hgext/uncommit.py
tests/test-uncommit.t
CHANGE DETAILS
diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
new file mode 100644
--- /dev/null
+++ b/tests/test-uncommit.t
@@ -0,0 +1,366 @@
+Test uncommit - set up the config
+
+ $ cat >> $HGRCPATH <<EOF
+ > [experimental]
+ > evolution=createmarkers, allowunstable
+ > [extensions]
+ > uncommit =
+ > drawdag=$TESTDIR/drawdag.py
+ > EOF
+
+Build up a repo
+
+ $ hg init repo
+ $ cd repo
+ $ hg bookmark foo
+
+Help for uncommit
+
+ $ hg help uncommit
+ hg uncommit [OPTION]... [FILE]...
+
+ uncommit part or all of a local changeset
+
+ This command undoes the effect of a local commit, returning the affected
+ files to their uncommitted state. This means that files modified or
+ deleted in the changeset will be left unchanged, and so will remain
+ modified in the working directory.
+
+ (use 'hg help -e uncommit' to show help for the uncommit extension)
+
+ options ([+] can be repeated):
+
+ --empty allow an empty commit after uncommiting
+ -I --include PATTERN [+] include names matching the given patterns
+ -X --exclude PATTERN [+] exclude names matching the given patterns
+
+ (some details hidden, use --verbose to show complete help)
+
+Uncommit with no commits should fail
+
+ $ hg uncommit
+ abort: cannot uncommit null changeset
+ [255]
+
+Create some commits
+
+ $ touch files
+ $ hg add files
+ $ for i in a ab abc abcd abcde; do echo $i > files; echo $i > file-$i; hg add file-$i; hg commit -m "added file-$i"; done
+ $ ls
+ file-a
+ file-ab
+ file-abc
+ file-abcd
+ file-abcde
+ files
+
+ $ hg log -G -T '{rev}:{node} {desc}' --hidden
+ @ 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
+ |
+ o 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
+ |
+ o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
+ |
+ o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
+ |
+ o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
+
+Simple uncommit off the top, also moves bookmark
+
+ $ hg bookmark
+ * foo 4:6c4fd43ed714
+ $ hg uncommit
+ $ hg status
+ M files
+ A file-abcde
+ $ hg bookmark
+ * foo 3:6db330d65db4
+
+ $ hg log -G -T '{rev}:{node} {desc}' --hidden
+ x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
+ |
+ @ 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
+ |
+ o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
+ |
+ o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
+ |
+ o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
+
+
+Recommit
+
+ $ hg commit -m 'new change abcde'
+ $ hg status
+ $ hg heads -T '{rev}:{node} {desc}'
+ 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde (no-eol)
+
+Uncommit of non-existent and unchanged files has no effect
+ $ hg uncommit nothinghere
+ nothing to uncommit
+ [1]
+ $ hg status
+ $ hg uncommit file-abc
+ nothing to uncommit
+ [1]
+ $ hg status
+
+Try partial uncommit, also moves bookmark
+
+ $ hg bookmark
+ * foo 5:0c07a3ccda77
+ $ hg uncommit files
+ $ hg status
+ M files
+ $ hg bookmark
+ * foo 6:3727deee06f7
+ $ hg heads -T '{rev}:{node} {desc}'
+ 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde (no-eol)
+ $ hg log -r . -p -T '{rev}:{node} {desc}'
+ 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcdediff -r 6db330d65db4 -r 3727deee06f7 file-abcde
+ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+ +++ b/file-abcde Thu Jan 01 00:00:00 1970 +0000
+ @@ -0,0 +1,1 @@
+ +abcde
+
+ $ hg log -G -T '{rev}:{node} {desc}' --hidden
+ @ 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde
+ |
+ | x 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde
+ |/
+ | x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
+ |/
+ o 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
+ |
+ o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
+ |
+ o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
+ |
+ o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
+
+ $ hg commit -m 'update files for abcde'
+
+Uncommit with dirty state
+
+ $ echo "foo" >> files
+ $ cat files
+ abcde
+ foo
+ $ hg status
+ M files
+ $ hg uncommit files
+ $ cat files
+ abcde
+ foo
+ $ hg commit -m "files abcde + foo"
+
+Uncommit in the middle of a stack, does not move bookmark
+
+ $ hg checkout '.^^^'
+ 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
+ (leaving bookmark foo)
+ $ hg log -r . -p -T '{rev}:{node} {desc}'
+ 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abcdiff -r 69a232e754b0 -r abf2df566fc1 file-abc
+ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+ +++ b/file-abc Thu Jan 01 00:00:00 1970 +0000
+ @@ -0,0 +1,1 @@
+ +abc
+ diff -r 69a232e754b0 -r abf2df566fc1 files
+ --- a/files Thu Jan 01 00:00:00 1970 +0000
+ +++ b/files Thu Jan 01 00:00:00 1970 +0000
+ @@ -1,1 +1,1 @@
+ -ab
+ +abc
+
+ $ hg bookmark
+ foo 8:83815831694b
+ $ hg uncommit
+ $ hg status
+ M files
+ A file-abc
+ $ hg heads -T '{rev}:{node} {desc}'
+ 8:83815831694b1271e9f207cb1b79b2b19275edcb files abcde + foo (no-eol)
+ $ hg bookmark
+ foo 8:83815831694b
+ $ hg commit -m 'new abc'
+ created new head
+
+Partial uncommit in the middle, does not move bookmark
+
+ $ hg checkout '.^'
+ 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ $ hg log -r . -p -T '{rev}:{node} {desc}'
+ 1:69a232e754b08d568c4899475faf2eb44b857802 added file-abdiff -r 3004d2d9b508 -r 69a232e754b0 file-ab
+ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+ +++ b/file-ab Thu Jan 01 00:00:00 1970 +0000
+ @@ -0,0 +1,1 @@
+ +ab
+ diff -r 3004d2d9b508 -r 69a232e754b0 files
+ --- a/files Thu Jan 01 00:00:00 1970 +0000
+ +++ b/files Thu Jan 01 00:00:00 1970 +0000
+ @@ -1,1 +1,1 @@
+ -a
+ +ab
+
+ $ hg bookmark
+ foo 8:83815831694b
+ $ hg uncommit file-ab
+ $ hg status
+ A file-ab
+
+ $ hg heads -T '{rev}:{node} {desc}\n'
+ 10:8eb87968f2edb7f27f27fe676316e179de65fff6 added file-ab
+ 9:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
+ 8:83815831694b1271e9f207cb1b79b2b19275edcb files abcde + foo
+
+ $ hg bookmark
+ foo 8:83815831694b
+ $ hg commit -m 'update ab'
+ $ hg status
+ $ hg heads -T '{rev}:{node} {desc}\n'
+ 11:f21039c59242b085491bb58f591afc4ed1c04c09 update ab
+ 9:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
+ 8:83815831694b1271e9f207cb1b79b2b19275edcb files abcde + foo
+
+ $ hg log -G -T '{rev}:{node} {desc}' --hidden
+ @ 11:f21039c59242b085491bb58f591afc4ed1c04c09 update ab
+ |
+ o 10:8eb87968f2edb7f27f27fe676316e179de65fff6 added file-ab
+ |
+ | o 9:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
+ | |
+ | | o 8:83815831694b1271e9f207cb1b79b2b19275edcb files abcde + foo
+ | | |
+ | | | x 7:0977fa602c2fd7d8427ed4e7ee15ea13b84c9173 update files for abcde
+ | | |/
+ | | o 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde
+ | | |
+ | | | x 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde
+ | | |/
+ | | | x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
+ | | |/
+ | | o 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
+ | | |
+ | | x 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
+ | |/
+ | x 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
+ |/
+ o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
+
+Uncommit with draft parent
+
+ $ hg uncommit
+ $ hg phase -r .
+ 10: draft
+ $ hg commit -m 'update ab again'
+
+Uncommit with public parent
+
+ $ hg phase -p "::.^"
+ $ hg uncommit
+ $ hg phase -r .
+ 10: public
+
+Partial uncommit with public parent
+
+ $ echo xyz > xyz
+ $ hg add xyz
+ $ hg commit -m "update ab and add xyz"
+ $ hg uncommit xyz
+ $ hg status
+ A xyz
+ $ hg phase -r .
+ 14: draft
+ $ hg phase -r ".^"
+ 10: public
+
+Uncommit leaving an empty changeset
+
+ $ cd $TESTTMP
+ $ hg init repo1
+ $ cd repo1
+ $ hg debugdrawdag <<'EOS'
+ > Q
+ > |
+ > P
+ > EOS
+ $ hg up Q -q
+ $ hg uncommit --empty
+ $ hg log -G -T '{desc} FILES: {files}'
+ @ Q FILES:
+ |
+ | x Q FILES: Q
+ |/
+ o P FILES: P
+
+ $ hg status
+ A Q
+
+ $ cd ..
+ $ rm repo1 -rf
+
+Testing uncommit while merge
+
+ $ hg init repo2
+ $ cd repo2
+
+Create some history
+
+ $ touch a
+ $ hg add a
+ $ for i in 1 2 3; do echo $i > a; hg commit -m "a $i"; done
+ $ hg checkout 0
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ touch b
+ $ hg add b
+ $ for i in 1 2 3; do echo $i > b; hg commit -m "b $i"; done
+ created new head
+ $ hg log -G -T '{rev}:{node} {desc}' --hidden
+ @ 5:2cd56cdde163ded2fbb16ba2f918c96046ab0bf2 b 3
+ |
+ o 4:c3a0d5bb3b15834ffd2ef9ef603e93ec65cf2037 b 2
+ |
+ o 3:49bb009ca26078726b8870f1edb29fae8f7618f5 b 1
+ |
+ | o 2:990982b7384266e691f1bc08ca36177adcd1c8a9 a 3
+ | |
+ | o 1:24d38e3cf160c7b6f5ffe82179332229886a6d34 a 2
+ |/
+ o 0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
+
+
+Add and expect uncommit to fail on both merge working dir and merge changeset
+
+ $ hg merge 2
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (branch merge, don't forget to commit)
+
+ $ hg uncommit
+ abort: cannot uncommit while merging
+ [255]
+
+ $ hg status
+ M a
+ $ hg commit -m 'merge a and b'
+
+ $ hg uncommit
+ abort: cannot uncommit merge changeset
+ [255]
+
+ $ hg status
+ $ hg log -G -T '{rev}:{node} {desc}' --hidden
+ @ 6:c03b9c37bc67bf504d4912061cfb527b47a63c6e merge a and b
+ |\
+ | o 5:2cd56cdde163ded2fbb16ba2f918c96046ab0bf2 b 3
+ | |
+ | o 4:c3a0d5bb3b15834ffd2ef9ef603e93ec65cf2037 b 2
+ | |
+ | o 3:49bb009ca26078726b8870f1edb29fae8f7618f5 b 1
+ | |
+ o | 2:990982b7384266e691f1bc08ca36177adcd1c8a9 a 3
+ | |
+ o | 1:24d38e3cf160c7b6f5ffe82179332229886a6d34 a 2
+ |/
+ o 0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
+
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
new file mode 100644
--- /dev/null
+++ b/hgext/uncommit.py
@@ -0,0 +1,181 @@
+# uncommit - undo the actions of a commit
+#
+# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht at gmail.com>
+# Logilab SA <contact at logilab.fr>
+# Pierre-Yves David <pierre-yves.david at ens-lyon.org>
+# Patrick Mezard <patrick at mezard.eu>
+# Copyright 2016 Facebook, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""uncommit part or all of a local changeset (EXPERIMENTAL)
+
+This command undoes the effect of a local commit, returning the affected
+files to their uncommitted state. This means that files modified, added or
+removed in the changeset will be left unchanged, and so will remain modified,
+added and removed in the working directory.
+"""
+
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+
+from mercurial import (
+ commands,
+ context,
+ copies,
+ error,
+ node,
+ obsolete,
+ phases,
+ registrar,
+ scmutil,
+)
+
+cmdtable = {}
+command = registrar.command(cmdtable)
+
+# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
+# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
+# be specifying the version(s) of Mercurial they are tested with, or
+# leave the attribute unspecified.
+testedwith = 'ships-with-hg-core'
+
+def _commitfiltered(repo, ctx, match, allowempty):
+ """Recommit ctx with changed files not in match. Return the new
+ node identifier, or None if nothing changed.
+ """
+ base = ctx.p1()
+ # ctx
+ initialfiles = set(ctx.files())
+ exclude = set(f for f in initialfiles if match(f))
+
+ # No files matched commit, so nothing excluded
+ if not exclude:
+ return None
+
+ files = (initialfiles - exclude)
+ # return the p1 so that we don't create an obsmarker later
+ if not files and not allowempty:
+ return ctx.parents()[0].node()
+
+ # Filter copies
+ copied = copies.pathcopies(base, ctx)
+ copied = dict((dst, src) for dst, src in copied.iteritems()
+ if dst in files)
+ def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
+ if path not in contentctx:
+ return None
+ fctx = contentctx[path]
+ mctx = context.memfilectx(repo, fctx.path(), fctx.data(),
+ fctx.islink(),
+ fctx.isexec(),
+ copied=copied.get(path))
+ return mctx
+
+ new = context.memctx(repo,
+ parents=[base.node(), node.nullid],
+ text=ctx.description(),
+ files=files,
+ filectxfn=filectxfn,
+ user=ctx.user(),
+ date=ctx.date(),
+ extra=ctx.extra())
+ newid = repo.commitctx(new)
+ return newid
+
+def _uncommitdirstate(repo, oldctx, match):
+ """Fix the dirstate after switching the working directory from
+ oldctx to a copy of oldctx not containing changed files matched by
+ match.
+ """
+ ctx = repo['.']
+ ds = repo.dirstate
+ copies = dict(ds.copies())
+ s = repo.status(oldctx.p1(), oldctx, match=match)
+ 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 = oldctx[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 ctx or dst in ctx or ds[dst] != 'a'):
+ src = None
+ ds.copy(src, dst)
+
+ at command('uncommit',
+ [('', 'empty', False, _('allow an empty commit after uncommiting')),
+ ] + commands.walkopts,
+ _('[OPTION]... [FILE]...'))
+def uncommit(ui, repo, *pats, **opts):
+ """uncommit part or all of a local changeset
+
+ This command undoes the effect of a local commit, returning the affected
+ files to their uncommitted state. This means that files modified or
+ deleted in the changeset will be left unchanged, and so will remain
+ modified in the working directory.
+ """
+
+ with repo.wlock(), repo.lock():
+ wctx = repo[None]
+
+ if wctx.parents()[0].node() == node.nullid:
+ raise error.Abort(_("cannot uncommit null changeset"))
+ if len(wctx.parents()) > 1:
+ raise error.Abort(_("cannot uncommit while merging"))
+ old = repo['.']
+ if not old.mutable():
+ raise error.Abort(_('cannot uncommit public changesets'))
+ if len(old.parents()) > 1:
+ raise error.Abort(_("cannot uncommit merge changeset"))
+ allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
+ if not allowunstable and old.children():
+ raise error.Abort(_('cannot uncommit changeset with children'))
+
+ with repo.transaction('uncommit') as tr:
+ match = scmutil.match(old, pats, opts)
+ newid = _commitfiltered(repo, old, match, opts.get('empty'))
+ if newid is None:
+ ui.status(_("nothing to uncommit\n"))
+ return 1
+
+ mapping = {}
+ if newid != old.p1().node():
+ # Move local changes on filtered changeset
+ mapping[old.node()] = (newid,)
+ phases.retractboundary(repo, tr, old.phase(), [newid])
+ else:
+ # Fully removed the old commit
+ mapping[old.node()] = ()
+
+ scmutil.cleanupnodes(repo, mapping, 'uncommit')
+
+ with repo.dirstate.parentchange():
+ repo.dirstate.setparents(newid, node.nullid)
+ _uncommitdirstate(repo, old, match)
To: pulkit, #hg-reviewers, quark, durham
Cc: durham, quark, martinvonz, yuja, mercurial-devel
More information about the Mercurial-devel
mailing list