D529: uncommit: move fb-extension to core which uncommits a changeset
pulkit (Pulkit Goyal)
phabricator at mercurial-scm.org
Sun Aug 27 20:45:08 UTC 2017
pulkit created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.
REVISION SUMMARY
uncommit extension in fb-hgext adds a uncommit command which by default
uncommits a changeset and move all the changes to the working directory. If
file names are passed, uncommit moves the changes from those files to the
working directory and left the changeset with remaining committed files.
The uncommit extension in fb-hgext does not creates an empty commit like the one
in evolve extension unless user has specified ui.alllowemptycommit to True.
The test file added is a combination of tests from test-uncommit.t,
test-uncommit-merge.t and test-uncommit-bookmark.t from fb-hgext.
REPOSITORY
rHG Mercurial
REVISION DETAIL
https://phab.mercurial-scm.org/D529
AFFECTED FILES
hgext/uncommit.py
tests/test-help.t
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,365 @@
+Test uncommit - set up the config
+
+ $ cat >> $HGRCPATH <<EOF
+ > [experimental]
+ > evolution=createmarkers
+ > [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 some 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):
+
+ -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
+ abort: nothing to uncommit
+ [255]
+ $ hg status
+ $ hg uncommit file-abc
+ abort: nothing to uncommit
+ [255]
+ $ 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 --config ui.allowemptycommit=1
+ $ 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/tests/test-help.t b/tests/test-help.t
--- a/tests/test-help.t
+++ b/tests/test-help.t
@@ -276,6 +276,7 @@
shelve save and restore changes to the working directory
strip strip changesets and their descendants from history
transplant command to transplant changesets from another branch
+ uncommit uncommit some or all of a local changeset
win32mbcs allow the use of MBCS paths with problematic encodings
zeroconf discover and advertise repositories on the local network
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
new file mode 100644
--- /dev/null
+++ b/hgext/uncommit.py
@@ -0,0 +1,182 @@
+# 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 some 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.
+"""
+
+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)
+
+testedwith = 'ships-with-hg-core'
+
+def _updatebookmarks(repo, oldid, newid, tr):
+ oldbookmarks = repo.nodebookmarks(oldid)
+ if oldbookmarks:
+ changes = []
+ for b in oldbookmarks:
+ changes.append((b, newid))
+ repo._bookmarks.applychanges(repo, tr, changes)
+
+def _commitfiltered(repo, ctx, match):
+ """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)
+ if not files and not repo.ui.configbool('ui', 'allowemptycommit'):
+ 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())
+ m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+ for f in m:
+ if ds[f] == 'r':
+ # modified + removed -> removed
+ continue
+ ds.normallookup(f)
+
+ for f in a:
+ if ds[f] == 'r':
+ # added + removed -> unknown
+ ds.drop(f)
+ elif ds[f] != 'a':
+ ds.add(f)
+
+ for f in r:
+ 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 (m + a):
+ 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',
+ commands.walkopts,
+ _('[OPTION]... [FILE]...'))
+def uncommit(ui, repo, *pats, **opts):
+ """uncommit some 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.
+ """
+
+ if not obsolete.isenabled(repo, obsolete.createmarkersopt):
+ raise error.Abort(_("must have obsolescence enabled to uncommit"))
+
+ with repo.wlock(), repo.lock():
+ wctx = repo[None]
+
+ if len(wctx.parents()) <= 0 or not wctx.parents()[0]:
+ raise error.Abort(_("cannot uncommit null changeset"))
+ if len(wctx.parents()) > 1:
+ raise error.Abort(_("cannot uncommit while merging"))
+ old = repo['.']
+ oldphase = old.phase()
+ if oldphase == phases.public:
+ raise error.Abort(_("cannot rewrite immutable changeset"))
+ if len(old.parents()) > 1:
+ raise error.Abort(_("cannot uncommit merge changeset"))
+
+ with repo.transaction('uncommit') as tr:
+ match = scmutil.match(old, pats, opts)
+ newid = _commitfiltered(repo, old, match)
+ if newid is None:
+ raise error.Abort(_('nothing to uncommit'))
+
+ if newid != old.p1().node():
+ # Move local changes on filtered changeset
+ obsolete.createmarkers(repo, [(old, (repo[newid],))])
+ phases.retractboundary(repo, tr, oldphase, [newid])
+ else:
+ # Fully removed the old commit
+ obsolete.createmarkers(repo, [(old, ())])
+
+ with repo.dirstate.parentchange():
+ repo.dirstate.setparents(newid, node.nullid)
+ _uncommitdirstate(repo, old, match)
+
+ _updatebookmarks(repo, old.node(), newid, tr)
To: pulkit, #hg-reviewers
Cc: mercurial-devel
More information about the Mercurial-devel
mailing list