Patchwork D529: uncommit: move fb-extension to core which uncommits a changeset

login
register
mail settings
Submitter phabricator
Date Sept. 11, 2017, 8:51 p.m.
Message ID <c1075302edfeec029fa90c966331c515@localhost.localdomain>
Download mbox | patch
Permalink /patch/23793/
State Not Applicable
Headers show

Comments

phabricator - Sept. 11, 2017, 8:51 p.m.
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




To: pulkit, #hg-reviewers, quark, durham
Cc: durham, quark, martinvonz, yuja, mercurial-devel

Patch

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@gmail.com>
+#                Logilab SA        <contact@logilab.fr>
+#                Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#                Patrick Mezard <patrick@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)
+
+@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)