D5792: uncommit: added interactive mode(issue6062)

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
35 messages Options
12
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
taapas1128 created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

AFFECTED FILES
  hgext/uncommit.py
  tests/test-uncommit.t

CHANGE DETAILS

diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
--- a/tests/test-uncommit.t
+++ b/tests/test-uncommit.t
@@ -1,6 +1,8 @@
 Test uncommit - set up the config
 
   $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
   > [experimental]
   > evolution.createmarkers=True
   > evolution.allowunstable=True
@@ -34,6 +36,7 @@
   
   options ([+] can be repeated):
   
+   -i --interactive         use interactive mode
       --keep                allow an empty commit after uncommiting
    -I --include PATTERN [+] include names matching the given patterns
    -X --exclude PATTERN [+] exclude names matching the given patterns
@@ -398,3 +401,17 @@
   |/
   o  0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
   
+Test for interactive mode
+  $ hg init repo3
+  $ touch x
+  $ hg add x
+  $ hg commit -m "added x"
+  $ hg uncommit -i<<EOF
+  > y
+  > EOF
+  diff --git a/x b/x
+  new file mode 100644
+  examine changes to 'x'? [Ynesfdaq?] y
+  
+  abort: empty commit message
+  [255]
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
--- a/hgext/uncommit.py
+++ b/hgext/uncommit.py
@@ -136,7 +136,8 @@
         ds.copy(src, dst)
 
 @command('uncommit',
-    [('', 'keep', False, _('allow an empty commit after uncommiting')),
+    [('i', 'interactive', None, _('use interactive mode')),
+    ('', 'keep', False, _('allow an empty commit after uncommiting')),
     ] + commands.walkopts,
     _('[OPTION]... [FILE]...'),
     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
@@ -151,6 +152,7 @@
     If no files are specified, the commit will be pruned, unless --keep is
     given.
     """
+
     opts = pycompat.byteskwargs(opts)
 
     with repo.wlock(), repo.lock():
@@ -186,6 +188,11 @@
                 s = old.p1().status(old, match=match)
                 _fixdirstate(repo, old, repo[newid], s)
 
+    if opts.get(r'interactive'):
+        opts.pop(r'interactive')
+        ret = cmdutil.dorecord(ui, repo, commands.commit, None, False,
+                               cmdutil.recordfilter, *pats,
+                               **opts)
 def predecessormarkers(ctx):
     """yields the obsolete markers marking the given changeset as a successor"""
     for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):



To: taapas1128, #hg-reviewers
Cc: mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
taapas1128 updated this revision to Diff 13669.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D5792?vs=13668&id=13669

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

AFFECTED FILES
  hgext/uncommit.py
  tests/test-uncommit.t

CHANGE DETAILS

diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
--- a/tests/test-uncommit.t
+++ b/tests/test-uncommit.t
@@ -1,6 +1,8 @@
 Test uncommit - set up the config
 
   $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
   > [experimental]
   > evolution.createmarkers=True
   > evolution.allowunstable=True
@@ -34,6 +36,7 @@
   
   options ([+] can be repeated):
   
+   -i --interactive         use interactive mode
       --keep                allow an empty commit after uncommiting
    -I --include PATTERN [+] include names matching the given patterns
    -X --exclude PATTERN [+] exclude names matching the given patterns
@@ -398,3 +401,17 @@
   |/
   o  0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
   
+Test for interactive mode
+  $ hg init repo3
+  $ touch x
+  $ hg add x
+  $ hg commit -m "added x"
+  $ hg uncommit -i<<EOF
+  > y
+  > EOF
+  diff --git a/x b/x
+  new file mode 100644
+  examine changes to 'x'? [Ynesfdaq?] y
+  
+  abort: empty commit message
+  [255]
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
--- a/hgext/uncommit.py
+++ b/hgext/uncommit.py
@@ -136,7 +136,8 @@
         ds.copy(src, dst)
 
 @command('uncommit',
-    [('', 'keep', False, _('allow an empty commit after uncommiting')),
+    [('i', 'interactive', None, _('use interactive mode')),
+    ('', 'keep', False, _('allow an empty commit after uncommiting')),
     ] + commands.walkopts,
     _('[OPTION]... [FILE]...'),
     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
@@ -186,6 +187,11 @@
                 s = old.p1().status(old, match=match)
                 _fixdirstate(repo, old, repo[newid], s)
 
+    if opts.get(r'interactive'):
+        opts.pop(r'interactive')
+        ret = cmdutil.dorecord(ui, repo, commands.commit, None, False,
+                               cmdutil.recordfilter, *pats,
+                               **opts)
 def predecessormarkers(ctx):
     """yields the obsolete markers marking the given changeset as a successor"""
     for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):



To: taapas1128, #hg-reviewers
Cc: mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
pulkit added subscribers: lothiraldan, pulkit.
pulkit added a comment.


  Hi, thanks for the patch. Your patch is a good start at implementing `uncommit -i`. It misses multiple things like dirstate handling.
 
  The good news is that you don't need to implement all that.  There is an evolve extension which has already implemented that: https://www.mercurial-scm.org/repo/evolve/file/8494015ec24b/hgext3rd/evolve/cmdrewrite.py#l460. You can import that from there.
 
  If you plan to import that from there, I suggest notifying @lothiraldan about that too so that they can manage things in evolve.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 added a comment.


  Thanks for the review @pulkit . By importing do you suggest importing the code for interactive or someway is there is someway to link `evolve` to `hg-stable` ?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
pulkit added a comment.


  In https://phab.mercurial-scm.org/D5792#84959, @taapas1128 wrote:
 
  > Thanks for the review @pulkit . By importing do you suggest importing the code for interactive or someway is there is someway to link `evolve` to `hg-stable` ?
 
 
  I mean importing the code.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 updated this revision to Diff 13701.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D5792?vs=13669&id=13701

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

AFFECTED FILES
  hgext/uncommit.py
  tests/test-uncommit.t

CHANGE DETAILS

diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
--- a/tests/test-uncommit.t
+++ b/tests/test-uncommit.t
@@ -1,6 +1,8 @@
 Test uncommit - set up the config
 
   $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
   > [experimental]
   > evolution.createmarkers=True
   > evolution.allowunstable=True
@@ -34,6 +36,7 @@
   
   options ([+] can be repeated):
   
+   -i --interactive         interactive mode to uncommit
       --keep                allow an empty commit after uncommiting
    -I --include PATTERN [+] include names matching the given patterns
    -X --exclude PATTERN [+] exclude names matching the given patterns
@@ -398,3 +401,15 @@
   |/
   o  0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
   
+Test for interactive mode
+  $ hg init repo3
+  $ touch x
+  $ hg add x
+  $ hg commit -m "added x"
+  $ hg uncommit -i<<EOF
+  > y
+  > EOF
+  diff --git a/x b/x
+  new file mode 100644
+  examine changes to 'x'? [Ynesfdaq?] y
+  
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
--- a/hgext/uncommit.py
+++ b/hgext/uncommit.py
@@ -28,11 +28,14 @@
     copies,
     error,
     node,
+    obsolete,
     obsutil,
+    patch,
     pycompat,
     registrar,
     rewriteutil,
     scmutil,
+    util,
 )
 
 cmdtable = {}
@@ -45,6 +48,8 @@
     default=False,
 )
 
+stringio = util.stringio
+
 # 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
@@ -135,8 +140,107 @@
             src = None
         ds.copy(src, dst)
 
+
+def _uncommitdirstate(repo, oldctx, match, interactive):
+    """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())
+    if interactive:
+        # In interactive cases, we will find the status between oldctx and ctx
+        # and considering only the files which are changed between oldctx and
+        # ctx, and the status of what changed between oldctx and ctx will help
+        # us in defining the exact behavior
+        m, a, r = repo.status(oldctx, ctx, match=match)[:3]
+        for f in m:
+            # These are files which are modified between oldctx and ctx which
+            # contains two cases: 1) Were modified in oldctx and some
+            # modifications are uncommitted
+            # 2) Were added in oldctx but some part is uncommitted (this cannot
+            # contain the case when added files are uncommitted completely as
+            # that will result in status as removed not modified.)
+            # Also any modifications to a removed file will result the status as
+            # added, so we have only two cases. So in either of the cases, the
+            # resulting status can be modified or clean.
+            if ds[f] == 'r':
+                # But the file is removed in the working directory, leaving that
+                # as removed
+                continue
+            ds.normallookup(f)
+
+        for f in a:
+            # These are the files which are added between oldctx and ctx(new
+            # one), which means the files which were removed in oldctx
+            # but uncommitted completely while making the ctx
+            # This file should be marked as removed if the working directory
+            # does not adds it back. If it's adds it back, we do a normallookup.
+            # The file can't be removed in working directory, because it was
+            # removed in oldctx
+            if ds[f] == 'a':
+                ds.normallookup(f)
+                continue
+            ds.remove(f)
+
+        for f in r:
+            # These are files which are removed between oldctx and ctx, which
+            # means the files which were added in oldctx and were completely
+            # uncommitted in ctx. If a added file is partially uncommitted, that
+            # would have resulted in modified status, not removed.
+            # So a file added in a commit, and uncommitting that addition must
+            # result in file being stated as unknown.
+            if ds[f] == 'r':
+                # The working directory say it's removed, so lets make the file
+                # unknown
+                ds.drop(f)
+                continue
+            ds.add(f)
+    else:
+        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 = {}
+    if interactive:
+        # Interactive had different meaning of the variables so restoring the
+        # original meaning to use them
+        m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+    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)
+
 @command('uncommit',
-    [('', 'keep', False, _('allow an empty commit after uncommiting')),
+    [('i', 'interactive', False, _('interactive mode to uncommit')),
+    ('', 'keep', False, _('allow an empty commit after uncommiting')),
     ] + commands.walkopts,
     _('[OPTION]... [FILE]...'),
     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
@@ -152,6 +256,7 @@
     given.
     """
     opts = pycompat.byteskwargs(opts)
+    interactive = opts.get('interactive')
 
     with repo.wlock(), repo.lock():
 
@@ -167,6 +272,10 @@
             match = scmutil.match(old, pats, opts)
             keepcommit = opts.get('keep') or pats
             newid = _commitfiltered(repo, old, match, keepcommit)
+            if interactive:
+                match = scmutil.match(old, pats, opts)
+                newid = _interactiveuncommit(ui, repo, old, match)
+
             if newid is None:
                 ui.status(_("nothing to uncommit\n"))
                 return 1
@@ -183,8 +292,101 @@
 
             with repo.dirstate.parentchange():
                 repo.dirstate.setparents(newid, node.nullid)
-                s = old.p1().status(old, match=match)
-                _fixdirstate(repo, old, repo[newid], s)
+                _uncommitdirstate(repo, old, match, interactive)
+
+def _interactiveuncommit(ui, repo, old, match):
+    """ The function which contains all the logic for interactively uncommiting
+    a commit. This function makes a temporary commit with the chunks which user
+    selected to uncommit. After that the diff of the parent and that commit is
+    applied to the working directory and committed again which results in the
+    new commit which should be one after uncommitted.
+    """
+
+    # create a temporary commit with hunks user selected
+    tempnode = _createtempcommit(ui, repo, old, match)
+
+    diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+    diffopts.nodates = True
+    diffopts.git = True
+    fp = stringio()
+    for chunk, label in patch.diffui(repo, tempnode, old.node(), None,
+                                     opts=diffopts):
+            fp.write(chunk)
+
+    fp.seek(0)
+    newnode = _patchtocommit(ui, repo, old, fp)
+    # creating obs marker temp -> ()
+    obsolete.createmarkers(repo, [(repo[tempnode], ())], operation="uncommit")
+    return newnode
+def _createtempcommit(ui, repo, old, match):
+    """ Creates a temporary commit for `uncommit --interative` which contains
+    the hunks which were selected by the user to uncommit.
+    """
+
+    pold = old.p1()
+    # The logic to interactively selecting something copied from
+    # cmdutil.revert()
+    diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+    diffopts.nodates = True
+    diffopts.git = True
+    diff = patch.diff(repo, pold.node(), old.node(), match, opts=diffopts)
+    originalchunks = patch.parsepatch(diff)
+    # XXX: The interactive selection is buggy and does not let you
+    # uncommit a removed file partially.
+    # TODO: wrap the operations in mercurial/patch.py and mercurial/crecord.py
+    # to add uncommit as an operation taking care of BC.
+    chunks, opts = cmdutil.recordfilter(repo.ui, originalchunks,
+                                        operation='discard')
+    if not chunks:
+        raise error.Abort(_("nothing selected to uncommit"))
+    fp = stringio()
+    for c in chunks:
+            c.write(fp)
+
+    fp.seek(0)
+    oldnode = node.hex(old.node())[:12]
+    message = 'temporary commit for uncommiting %s' % oldnode
+    tempnode = _patchtocommit(ui, repo, old, fp, message, oldnode)
+    return tempnode
+
+def _patchtocommit(ui, repo, old, fp, message=None, extras=None):
+    """ A function which will apply the patch to the working directory and
+    make a commit whose parents are same as that of old argument. The message
+    argument tells us whether to use the message of the old commit or a
+    different message which is passed. Returns the node of new commit made.
+    """
+    pold = old.p1()
+    parents = (old.p1().node(), old.p2().node())
+    date = old.date()
+    branch = old.branch()
+    user = old.user()
+    extra = old.extra()
+    if extras:
+        extra['uncommit_source'] = extras
+    if not message:
+        message = old.description()
+    store = patch.filestore()
+    try:
+        files = set()
+        try:
+            patch.patchrepo(ui, repo, pold, store, fp, 1, '',
+                            files=files, eolmode=None)
+        except patch.PatchError as err:
+            raise error.Abort(str(err))
+
+        finally:
+            del fp
+
+        memctx = context.memctx(repo, parents, message, files=files,
+                                filectxfn=store,
+                                user=user,
+                                date=date,
+                                branch=branch,
+                                extra=extra)
+        newcm = memctx.commit()
+    finally:
+        store.close()
+    return newcm
 
 def predecessormarkers(ctx):
     """yields the obsolete markers marking the given changeset as a successor"""



To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 added a comment.


  @pulkit I have made an attempt to import the code please review. @lothiraldan  can you please review and help me if I have missed out any portion of interactive from `evolve`

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
pulkit added a comment.


  In https://phab.mercurial-scm.org/D5792#85088, @taapas1128 wrote:
 
  > @pulkit I have made an attempt to import the code please review. @lothiraldan  can you please review and help me if I have missed out any portion of interactive from `evolve`
 
 
  @taapas1128 haven't looked at the code in detail yet, but there are tests in evolve which should also be moved.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 updated this revision to Diff 13737.
taapas1128 edited the summary of this revision.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D5792?vs=13701&id=13737

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

AFFECTED FILES
  hgext/uncommit.py
  tests/test-uncommit-interactive.t
  tests/test-uncommit.t

CHANGE DETAILS

diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
--- a/tests/test-uncommit.t
+++ b/tests/test-uncommit.t
@@ -1,6 +1,8 @@
 Test uncommit - set up the config
 
   $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
   > [experimental]
   > evolution.createmarkers=True
   > evolution.allowunstable=True
@@ -34,6 +36,7 @@
   
   options ([+] can be repeated):
   
+   -i --interactive         interactive mode to uncommit
       --keep                allow an empty commit after uncommiting
    -I --include PATTERN [+] include names matching the given patterns
    -X --exclude PATTERN [+] exclude names matching the given patterns
@@ -398,3 +401,15 @@
   |/
   o  0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
   
+Test for interactive mode
+  $ hg init repo3
+  $ touch x
+  $ hg add x
+  $ hg commit -m "added x"
+  $ hg uncommit -i<<EOF
+  > y
+  > EOF
+  diff --git a/x b/x
+  new file mode 100644
+  examine changes to 'x'? [Ynesfdaq?] y
+  
diff --git a/tests/test-uncommit-interactive.t b/tests/test-uncommit-interactive.t
new file mode 100644
--- /dev/null
+++ b/tests/test-uncommit-interactive.t
@@ -0,0 +1,873 @@
+================================================
+||  The test for `hg uncommit --interactive`  ||
+================================================
+
+Repo Setup
+============
+
+  $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
+  > [experimental]
+  > evolution.createmarkers=True
+  > evolution.allowunstable=True
+  > uncommitondirtywdir = true
+  > [extensions]
+  > uncommit =
+  > amend =
+  > drawdag=$TESTDIR/drawdag.py
+  > EOF
+  $ glog() {
+  >   hg log -G --template '{rev}:{node|short}@{branch}({separate("/", obsolete, phase)}) {desc|firstline}\n' "$@"
+  > }
+
+  $ hg init repo
+  $ cd repo
+
+  $ touch a
+  $ cat >> a << EOF
+  > 1
+  > 2
+  > 3
+  > 4
+  > 5
+  > EOF
+
+  $ hg add a
+  $ hg ci -m "The base commit"
+
+Make sure aborting the interactive selection does no magic
+----------------------------------------------------------
+
+  $ hg status
+  $ hg uncommit -i<<EOF
+  > q
+  > EOF
+  diff --git a/a b/a
+  new file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] q
+  
+  abort: user quit
+  [255]
+  $ hg status
+
+Make a commit with multiple hunks
+---------------------------------
+
+  $ cat > a << EOF
+  > -2
+  > -1
+  > 0
+  > 1
+  > 2
+  > 3
+  > foo
+  > bar
+  > 4
+  > 5
+  > babar
+  > EOF
+
+  $ hg diff
+  diff -r 7733902a8d94 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+  $ hg ci -m "another one"
+
+Not selecting anything to uncommit
+==================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > n
+  > n
+  > n
+  > EOF
+  diff --git a/a b/a
+  3 hunks, 6 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -4,2 +9,3 @@
+   4
+   5
+  +babar
+  discard change 3/3 to 'a'? [Ynesfdaq?] n
+  
+  abort: nothing selected to uncommit
+  [255]
+  $ hg status
+
+Uncommit a chunk
+================
+
+  $ hg amend --extract -n "note on amend --extract" -i<<EOF
+  > y
+  > y
+  > n
+  > n
+  > EOF
+  hg amend: option --extract not recognized
+  hg amend [OPTION]... [FILE]...
+  
+  amend the working copy parent with all or specified outstanding changes
+  
+  (use 'hg help -e amend' to show help for the amend extension)
+  
+  options ([+] can be repeated):
+  
+   -A --addremove           mark new/missing files as added/removed before
+                            committing
+   -e --edit                invoke editor on commit messages
+   -i --interactive         use interactive mode
+   -n --note VALUE          store a note on the amend
+   -I --include PATTERN [+] include names matching the given patterns
+   -X --exclude PATTERN [+] exclude names matching the given patterns
+   -m --message TEXT        use text as commit message
+   -l --logfile FILE        read commit message from file
+   -d --date DATE           record the specified date as commit date
+   -u --user USER           record the specified user as committer
+  
+  (use 'hg amend -h' to show more help)
+  [255]
+
+  $ hg obslog
+  hg: unknown command 'obslog'
+  (did you mean log?)
+  [255]
+The unselected part should be in the diff
+-----------------------------------------
+
+  $ hg diff
+
+The commit should contain the rest of part
+------------------------------------------
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID f70fb463d5bf9f0ffd38f105521d96e9f2591bc1
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r f70fb463d5bf a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+Uncommiting on dirty working directory
+======================================
+
+  $ hg status
+  $ hg diff
+
+  $ hg uncommit -n "testing uncommit on dirty wdir" -i<<EOF
+  > y
+  > n
+  > y
+  > EOF
+  hg uncommit: option -n not recognized
+  hg uncommit [OPTION]... [FILE]...
+  
+  uncommit part or all of a local changeset
+  
+  (use 'hg help -e uncommit' to show help for the uncommit extension)
+  
+  options ([+] can be repeated):
+  
+   -i --interactive         interactive mode to uncommit
+      --keep                allow an empty commit after uncommiting
+   -I --include PATTERN [+] include names matching the given patterns
+   -X --exclude PATTERN [+] exclude names matching the given patterns
+  
+  (use 'hg uncommit -h' to show more help)
+  [255]
+
+  $ hg diff
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID f70fb463d5bf9f0ffd38f105521d96e9f2591bc1
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r f70fb463d5bf a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+Checking the obsolescence history
+
+  $ hg obslog
+  hg: unknown command 'obslog'
+  (did you mean log?)
+  [255]
+
+Push the changes back to the commit and more commits for more testing
+
+  $ hg amend
+  nothing changed
+  [1]
+  $ glog
+  @  1:f70fb463d5bf@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+  $ touch foo
+  $ echo "hey" >> foo
+  $ hg ci -Am "Added foo"
+  adding foo
+
+Testing uncommiting a whole changeset and also for a file addition
+==================================================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > EOF
+  diff --git a/foo b/foo
+  new file mode 100644
+  examine changes to 'foo'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +hey
+  discard this change to 'foo'? [Ynesfdaq?] y
+  
+
+  $ hg status
+  A foo
+  $ hg diff
+  diff -r 273577554504 foo
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/foo Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +hey
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 2735775545047d4f1f10a6932b15a9ab7734d5cd
+  # Parent  f70fb463d5bf9f0ffd38f105521d96e9f2591bc1
+  Added foo
+  
+  $ hg amend
+
+Testing to uncommit removed files completely
+============================================
+
+  $ hg rm a
+  $ hg ci -m "Removed a"
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID a8f42932eeb3c17c221414c2f3523edf00f86a09
+  # Parent  b31f68d8184de381eaaba311623e26db8779a9c4
+  Removed a
+  
+  diff -r b31f68d8184d -r a8f42932eeb3 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,11 +0,0 @@
+  --2
+  --1
+  -0
+  -1
+  -2
+  -3
+  -foo
+  -bar
+  -4
+  -5
+  -babar
+
+Not examining the file
+----------------------
+
+  $ hg uncommit -i<<EOF
+  > n
+  > EOF
+  diff --git a/a b/a
+  deleted file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] n
+  
+  abort: nothing selected to uncommit
+  [255]
+
+Examining the file
+------------------
+XXX: there is a bug in interactive selection as it is not letting to examine the
+file. Tried with curses too. In the curses UI, if you just unselect the hunks
+and the not file mod thing at the top, it will show the same "nothing unselected
+to uncommit" message which is a bug in interactive selection.
+
+  $ hg uncommit -i<<EOF
+  > y
+  > EOF
+  diff --git a/a b/a
+  deleted file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+
+  $ hg diff
+  diff -r 8543804c21a2 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,11 +0,0 @@
+  --2
+  --1
+  -0
+  -1
+  -2
+  -3
+  -foo
+  -bar
+  -4
+  -5
+  -babar
+  $ hg status
+  R a
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 8543804c21a216ff38d4ca446857592fd15b4d90
+  # Parent  b31f68d8184de381eaaba311623e26db8779a9c4
+  Removed a
+  
+
+  $ hg prune .
+  hg: unknown command 'prune'
+  (use 'hg help' for a list of commands)
+  [255]
+  $ hg revert --all
+  undeleting a
+
+  $ glog
+  @  8:8543804c21a2@default(draft) Removed a
+  |
+  o  5:b31f68d8184d@default(draft) Added foo
+  |
+  o  1:f70fb463d5bf@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+
+Testing when a new file is added in the last commit
+===================================================
+
+  $ echo "foo" >> foo
+  $ touch x
+  $ echo "abcd" >> x
+  $ hg add x
+  $ hg ci -m "Added x"
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > y
+  > n
+  > EOF
+  diff --git a/foo b/foo
+  1 hunks, 1 lines changed
+  examine changes to 'foo'? [Ynesfdaq?] y
+  
+  @@ -1,1 +1,2 @@
+   hey
+  +foo
+  discard change 1/2 to 'foo'? [Ynesfdaq?] y
+  
+  diff --git a/x b/x
+  new file mode 100644
+  examine changes to 'x'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +abcd
+  discard change 2/2 to 'x'? [Ynesfdaq?] n
+  
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 9476ced971e5aaf898c1a847a0d813ac3b259b64
+  # Parent  8543804c21a216ff38d4ca446857592fd15b4d90
+  Added x
+  
+  diff -r 8543804c21a2 -r 9476ced971e5 x
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/x Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +abcd
+
+  $ hg diff
+  diff -r 9476ced971e5 foo
+  --- a/foo Thu Jan 01 00:00:00 1970 +0000
+  +++ b/foo Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,1 +1,2 @@
+   hey
+  +foo
+
+  $ hg status
+  M foo
+
+  $ hg revert --all
+  reverting foo
+
+Testing between the stack and with dirty working copy
+=====================================================
+
+  $ glog
+  @  11:9476ced971e5@default(draft) Added x
+  |
+  o  8:8543804c21a2@default(draft) Removed a
+  |
+  o  5:b31f68d8184d@default(draft) Added foo
+  |
+  o  1:f70fb463d5bf@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+  $ hg up f70fb463d5bf
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+
+  $ touch bar
+  $ echo "foo" >> bar
+  $ hg add bar
+  $ hg status
+  A bar
+  ? foo.orig
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID f70fb463d5bf9f0ffd38f105521d96e9f2591bc1
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r f70fb463d5bf a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+  $ hg uncommit -i<<EOF
+  > y
+  > n
+  > n
+  > y
+  > EOF
+  diff --git a/a b/a
+  3 hunks, 6 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -4,2 +9,3 @@
+   4
+   5
+  +babar
+  discard change 3/3 to 'a'? [Ynesfdaq?] y
+  
+  patching file a
+  Hunk #1 succeeded at 1 with fuzz 1 (offset -1 lines).
+  3 new orphan changesets
+
+  $ hg diff
+  diff -r c867648f402f a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -8,3 +8,4 @@
+   bar
+   4
+   5
+  +babar
+  diff -r c867648f402f bar
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/bar Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +foo
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID c867648f402fa4130636203db77810b9c76a4f47
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r c867648f402f a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,10 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  $ hg status
+  M a
+  A bar
+  ? foo.orig
+
+More uncommit on the same dirty working copy
+=============================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > n
+  > EOF
+  diff --git a/a b/a
+  2 hunks, 5 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/2 to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/2 to 'a'? [Ynesfdaq?] n
+  
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 46e35360be473bf761bedf3d05de4a68ffd9d9f8
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r 46e35360be47 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+
+  $ hg diff
+  diff -r 46e35360be47 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  @@ -5,3 +8,4 @@
+   bar
+   4
+   5
+  +babar
+  diff -r 46e35360be47 bar
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/bar Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +foo
+
+  $ hg status
+  M a
+  A bar
+  ? foo.orig
+
+Interactive uncommit with a pattern
+-----------------------------------
+
+(more setup)
+
+  $ hg ci -m 'roaming changes'
+  $ cat > b << EOF
+  > a
+  > b
+  > c
+  > d
+  > e
+  > f
+  > h
+  > EOF
+  $ hg add b
+  $ hg ci -m 'add b'
+  $ echo 'celeste' >> a
+  $ echo 'i' >> b
+  $ hg ci -m 'some more changes'
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 61cbe268a7c00ce4a801f62e743f30da82818003
+  # Parent  443d17b6b44b50d8fcd9c0cf6d635d95c00f3720
+  some more changes
+  
+  diff -r 443d17b6b44b -r 61cbe268a7c0 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  diff -r 443d17b6b44b -r 61cbe268a7c0 b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+  $ hg uncommit -i a << DONE
+  > y
+  > y
+  > DONE
+  diff --git a/a b/a
+  1 hunks, 1 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  discard this change to 'a'? [Ynesfdaq?] y
+  
+  $ hg status
+  M a
+  ? foo.orig
+  $ hg diff
+  diff -r 228b5665c0a7 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 228b5665c0a70e98d04b5a7bcaec33278e6ea448
+  # Parent  443d17b6b44b50d8fcd9c0cf6d635d95c00f3720
+  some more changes
+  
+  diff -r 443d17b6b44b -r 228b5665c0a7 b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+(reset)
+
+  $ cat << EOF  > a
+  > -3
+  > -2
+  > -1
+  > 0
+  > 1
+  > 2
+  > 3
+  > foo
+  > bar
+  > 4
+  > 5
+  > babar
+  > celeste
+  > EOF
+  $ hg amend
+
+Same but do not select some change in 'a'
+
+  $ hg uncommit -i a << DONE
+  > y
+  > y
+  > n
+  > DONE
+  diff --git a/a b/a
+  2 hunks, 2 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,4 @@
+  +-3
+   -2
+   -1
+   0
+  discard change 1/2 to 'a'? [Ynesfdaq?] y
+  
+  @@ -9,3 +10,4 @@
+   4
+   5
+   babar
+  +celeste
+  discard change 2/2 to 'a'? [Ynesfdaq?] n
+  
+  $ hg status
+  M a
+  ? foo.orig
+
+  $ hg diff
+  diff -r a8761d3de8f0 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,4 @@
+  +-3
+   -2
+   -1
+   0
+
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID a8761d3de8f0c2d89e2452dd7798644d63f9a58a
+  # Parent  443d17b6b44b50d8fcd9c0cf6d635d95c00f3720
+  some more changes
+  
+  diff -r 443d17b6b44b -r a8761d3de8f0 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  diff -r 443d17b6b44b -r a8761d3de8f0 b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+  $ cat b
+  a
+  b
+  c
+  d
+  e
+  f
+  h
+  i
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
--- a/hgext/uncommit.py
+++ b/hgext/uncommit.py
@@ -28,11 +28,14 @@
     copies,
     error,
     node,
+    obsolete,
     obsutil,
+    patch,
     pycompat,
     registrar,
     rewriteutil,
     scmutil,
+    util,
 )
 
 cmdtable = {}
@@ -45,6 +48,8 @@
     default=False,
 )
 
+stringio = util.stringio
+
 # 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
@@ -135,8 +140,107 @@
             src = None
         ds.copy(src, dst)
 
+
+def _uncommitdirstate(repo, oldctx, match, interactive):
+    """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())
+    if interactive:
+        # In interactive cases, we will find the status between oldctx and ctx
+        # and considering only the files which are changed between oldctx and
+        # ctx, and the status of what changed between oldctx and ctx will help
+        # us in defining the exact behavior
+        m, a, r = repo.status(oldctx, ctx, match=match)[:3]
+        for f in m:
+            # These are files which are modified between oldctx and ctx which
+            # contains two cases: 1) Were modified in oldctx and some
+            # modifications are uncommitted
+            # 2) Were added in oldctx but some part is uncommitted (this cannot
+            # contain the case when added files are uncommitted completely as
+            # that will result in status as removed not modified.)
+            # Also any modifications to a removed file will result the status as
+            # added, so we have only two cases. So in either of the cases, the
+            # resulting status can be modified or clean.
+            if ds[f] == 'r':
+                # But the file is removed in the working directory, leaving that
+                # as removed
+                continue
+            ds.normallookup(f)
+
+        for f in a:
+            # These are the files which are added between oldctx and ctx(new
+            # one), which means the files which were removed in oldctx
+            # but uncommitted completely while making the ctx
+            # This file should be marked as removed if the working directory
+            # does not adds it back. If it's adds it back, we do a normallookup.
+            # The file can't be removed in working directory, because it was
+            # removed in oldctx
+            if ds[f] == 'a':
+                ds.normallookup(f)
+                continue
+            ds.remove(f)
+
+        for f in r:
+            # These are files which are removed between oldctx and ctx, which
+            # means the files which were added in oldctx and were completely
+            # uncommitted in ctx. If a added file is partially uncommitted, that
+            # would have resulted in modified status, not removed.
+            # So a file added in a commit, and uncommitting that addition must
+            # result in file being stated as unknown.
+            if ds[f] == 'r':
+                # The working directory say it's removed, so lets make the file
+                # unknown
+                ds.drop(f)
+                continue
+            ds.add(f)
+    else:
+        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 = {}
+    if interactive:
+        # Interactive had different meaning of the variables so restoring the
+        # original meaning to use them
+        m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+    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)
+
 @command('uncommit',
-    [('', 'keep', False, _('allow an empty commit after uncommiting')),
+    [('i', 'interactive', False, _('interactive mode to uncommit')),
+    ('', 'keep', False, _('allow an empty commit after uncommiting')),
     ] + commands.walkopts,
     _('[OPTION]... [FILE]...'),
     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
@@ -152,6 +256,7 @@
     given.
     """
     opts = pycompat.byteskwargs(opts)
+    interactive = opts.get('interactive')
 
     with repo.wlock(), repo.lock():
 
@@ -167,6 +272,10 @@
             match = scmutil.match(old, pats, opts)
             keepcommit = opts.get('keep') or pats
             newid = _commitfiltered(repo, old, match, keepcommit)
+            if interactive:
+                match = scmutil.match(old, pats, opts)
+                newid = _interactiveuncommit(ui, repo, old, match)
+
             if newid is None:
                 ui.status(_("nothing to uncommit\n"))
                 return 1
@@ -183,8 +292,101 @@
 
             with repo.dirstate.parentchange():
                 repo.dirstate.setparents(newid, node.nullid)
-                s = old.p1().status(old, match=match)
-                _fixdirstate(repo, old, repo[newid], s)
+                _uncommitdirstate(repo, old, match, interactive)
+
+def _interactiveuncommit(ui, repo, old, match):
+    """ The function which contains all the logic for interactively uncommiting
+    a commit. This function makes a temporary commit with the chunks which user
+    selected to uncommit. After that the diff of the parent and that commit is
+    applied to the working directory and committed again which results in the
+    new commit which should be one after uncommitted.
+    """
+
+    # create a temporary commit with hunks user selected
+    tempnode = _createtempcommit(ui, repo, old, match)
+
+    diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+    diffopts.nodates = True
+    diffopts.git = True
+    fp = stringio()
+    for chunk, label in patch.diffui(repo, tempnode, old.node(), None,
+                                     opts=diffopts):
+            fp.write(chunk)
+
+    fp.seek(0)
+    newnode = _patchtocommit(ui, repo, old, fp)
+    # creating obs marker temp -> ()
+    obsolete.createmarkers(repo, [(repo[tempnode], ())], operation="uncommit")
+    return newnode
+def _createtempcommit(ui, repo, old, match):
+    """ Creates a temporary commit for `uncommit --interative` which contains
+    the hunks which were selected by the user to uncommit.
+    """
+
+    pold = old.p1()
+    # The logic to interactively selecting something copied from
+    # cmdutil.revert()
+    diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+    diffopts.nodates = True
+    diffopts.git = True
+    diff = patch.diff(repo, pold.node(), old.node(), match, opts=diffopts)
+    originalchunks = patch.parsepatch(diff)
+    # XXX: The interactive selection is buggy and does not let you
+    # uncommit a removed file partially.
+    # TODO: wrap the operations in mercurial/patch.py and mercurial/crecord.py
+    # to add uncommit as an operation taking care of BC.
+    chunks, opts = cmdutil.recordfilter(repo.ui, originalchunks,
+                                        operation='discard')
+    if not chunks:
+        raise error.Abort(_("nothing selected to uncommit"))
+    fp = stringio()
+    for c in chunks:
+            c.write(fp)
+
+    fp.seek(0)
+    oldnode = node.hex(old.node())[:12]
+    message = 'temporary commit for uncommiting %s' % oldnode
+    tempnode = _patchtocommit(ui, repo, old, fp, message, oldnode)
+    return tempnode
+
+def _patchtocommit(ui, repo, old, fp, message=None, extras=None):
+    """ A function which will apply the patch to the working directory and
+    make a commit whose parents are same as that of old argument. The message
+    argument tells us whether to use the message of the old commit or a
+    different message which is passed. Returns the node of new commit made.
+    """
+    pold = old.p1()
+    parents = (old.p1().node(), old.p2().node())
+    date = old.date()
+    branch = old.branch()
+    user = old.user()
+    extra = old.extra()
+    if extras:
+        extra['uncommit_source'] = extras
+    if not message:
+        message = old.description()
+    store = patch.filestore()
+    try:
+        files = set()
+        try:
+            patch.patchrepo(ui, repo, pold, store, fp, 1, '',
+                            files=files, eolmode=None)
+        except patch.PatchError as err:
+            raise error.Abort(str(err))
+
+        finally:
+            del fp
+
+        memctx = context.memctx(repo, parents, message, files=files,
+                                filectxfn=store,
+                                user=user,
+                                date=date,
+                                branch=branch,
+                                extra=extra)
+        newcm = memctx.commit()
+    finally:
+        store.close()
+    return newcm
 
 def predecessormarkers(ctx):
     """yields the obsolete markers marking the given changeset as a successor"""



To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 updated this revision to Diff 13738.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D5792?vs=13737&id=13738

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

AFFECTED FILES
  hgext/uncommit.py
  tests/test-uncommit-interactive.t
  tests/test-uncommit.t

CHANGE DETAILS

diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
--- a/tests/test-uncommit.t
+++ b/tests/test-uncommit.t
@@ -34,6 +34,7 @@
   
   options ([+] can be repeated):
   
+   -i --interactive         interactive mode to uncommit
       --keep                allow an empty commit after uncommiting
    -I --include PATTERN [+] include names matching the given patterns
    -X --exclude PATTERN [+] exclude names matching the given patterns
diff --git a/tests/test-uncommit-interactive.t b/tests/test-uncommit-interactive.t
new file mode 100644
--- /dev/null
+++ b/tests/test-uncommit-interactive.t
@@ -0,0 +1,873 @@
+================================================
+||  The test for `hg uncommit --interactive`  ||
+================================================
+
+Repo Setup
+============
+
+  $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
+  > [experimental]
+  > evolution.createmarkers=True
+  > evolution.allowunstable=True
+  > uncommitondirtywdir = true
+  > [extensions]
+  > uncommit =
+  > amend =
+  > drawdag=$TESTDIR/drawdag.py
+  > EOF
+  $ glog() {
+  >   hg log -G --template '{rev}:{node|short}@{branch}({separate("/", obsolete, phase)}) {desc|firstline}\n' "$@"
+  > }
+
+  $ hg init repo
+  $ cd repo
+
+  $ touch a
+  $ cat >> a << EOF
+  > 1
+  > 2
+  > 3
+  > 4
+  > 5
+  > EOF
+
+  $ hg add a
+  $ hg ci -m "The base commit"
+
+Make sure aborting the interactive selection does no magic
+----------------------------------------------------------
+
+  $ hg status
+  $ hg uncommit -i<<EOF
+  > q
+  > EOF
+  diff --git a/a b/a
+  new file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] q
+  
+  abort: user quit
+  [255]
+  $ hg status
+
+Make a commit with multiple hunks
+---------------------------------
+
+  $ cat > a << EOF
+  > -2
+  > -1
+  > 0
+  > 1
+  > 2
+  > 3
+  > foo
+  > bar
+  > 4
+  > 5
+  > babar
+  > EOF
+
+  $ hg diff
+  diff -r 7733902a8d94 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+  $ hg ci -m "another one"
+
+Not selecting anything to uncommit
+==================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > n
+  > n
+  > n
+  > EOF
+  diff --git a/a b/a
+  3 hunks, 6 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -4,2 +9,3 @@
+   4
+   5
+  +babar
+  discard change 3/3 to 'a'? [Ynesfdaq?] n
+  
+  abort: nothing selected to uncommit
+  [255]
+  $ hg status
+
+Uncommit a chunk
+================
+
+  $ hg amend --extract -n "note on amend --extract" -i<<EOF
+  > y
+  > y
+  > n
+  > n
+  > EOF
+  hg amend: option --extract not recognized
+  hg amend [OPTION]... [FILE]...
+  
+  amend the working copy parent with all or specified outstanding changes
+  
+  (use 'hg help -e amend' to show help for the amend extension)
+  
+  options ([+] can be repeated):
+  
+   -A --addremove           mark new/missing files as added/removed before
+                            committing
+   -e --edit                invoke editor on commit messages
+   -i --interactive         use interactive mode
+   -n --note VALUE          store a note on the amend
+   -I --include PATTERN [+] include names matching the given patterns
+   -X --exclude PATTERN [+] exclude names matching the given patterns
+   -m --message TEXT        use text as commit message
+   -l --logfile FILE        read commit message from file
+   -d --date DATE           record the specified date as commit date
+   -u --user USER           record the specified user as committer
+  
+  (use 'hg amend -h' to show more help)
+  [255]
+
+  $ hg obslog
+  hg: unknown command 'obslog'
+  (did you mean log?)
+  [255]
+The unselected part should be in the diff
+-----------------------------------------
+
+  $ hg diff
+
+The commit should contain the rest of part
+------------------------------------------
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID f70fb463d5bf9f0ffd38f105521d96e9f2591bc1
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r f70fb463d5bf a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+Uncommiting on dirty working directory
+======================================
+
+  $ hg status
+  $ hg diff
+
+  $ hg uncommit -n "testing uncommit on dirty wdir" -i<<EOF
+  > y
+  > n
+  > y
+  > EOF
+  hg uncommit: option -n not recognized
+  hg uncommit [OPTION]... [FILE]...
+  
+  uncommit part or all of a local changeset
+  
+  (use 'hg help -e uncommit' to show help for the uncommit extension)
+  
+  options ([+] can be repeated):
+  
+   -i --interactive         interactive mode to uncommit
+      --keep                allow an empty commit after uncommiting
+   -I --include PATTERN [+] include names matching the given patterns
+   -X --exclude PATTERN [+] exclude names matching the given patterns
+  
+  (use 'hg uncommit -h' to show more help)
+  [255]
+
+  $ hg diff
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID f70fb463d5bf9f0ffd38f105521d96e9f2591bc1
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r f70fb463d5bf a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+Checking the obsolescence history
+
+  $ hg obslog
+  hg: unknown command 'obslog'
+  (did you mean log?)
+  [255]
+
+Push the changes back to the commit and more commits for more testing
+
+  $ hg amend
+  nothing changed
+  [1]
+  $ glog
+  @  1:f70fb463d5bf@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+  $ touch foo
+  $ echo "hey" >> foo
+  $ hg ci -Am "Added foo"
+  adding foo
+
+Testing uncommiting a whole changeset and also for a file addition
+==================================================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > EOF
+  diff --git a/foo b/foo
+  new file mode 100644
+  examine changes to 'foo'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +hey
+  discard this change to 'foo'? [Ynesfdaq?] y
+  
+
+  $ hg status
+  A foo
+  $ hg diff
+  diff -r 273577554504 foo
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/foo Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +hey
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 2735775545047d4f1f10a6932b15a9ab7734d5cd
+  # Parent  f70fb463d5bf9f0ffd38f105521d96e9f2591bc1
+  Added foo
+  
+  $ hg amend
+
+Testing to uncommit removed files completely
+============================================
+
+  $ hg rm a
+  $ hg ci -m "Removed a"
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID a8f42932eeb3c17c221414c2f3523edf00f86a09
+  # Parent  b31f68d8184de381eaaba311623e26db8779a9c4
+  Removed a
+  
+  diff -r b31f68d8184d -r a8f42932eeb3 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,11 +0,0 @@
+  --2
+  --1
+  -0
+  -1
+  -2
+  -3
+  -foo
+  -bar
+  -4
+  -5
+  -babar
+
+Not examining the file
+----------------------
+
+  $ hg uncommit -i<<EOF
+  > n
+  > EOF
+  diff --git a/a b/a
+  deleted file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] n
+  
+  abort: nothing selected to uncommit
+  [255]
+
+Examining the file
+------------------
+XXX: there is a bug in interactive selection as it is not letting to examine the
+file. Tried with curses too. In the curses UI, if you just unselect the hunks
+and the not file mod thing at the top, it will show the same "nothing unselected
+to uncommit" message which is a bug in interactive selection.
+
+  $ hg uncommit -i<<EOF
+  > y
+  > EOF
+  diff --git a/a b/a
+  deleted file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+
+  $ hg diff
+  diff -r 8543804c21a2 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,11 +0,0 @@
+  --2
+  --1
+  -0
+  -1
+  -2
+  -3
+  -foo
+  -bar
+  -4
+  -5
+  -babar
+  $ hg status
+  R a
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 8543804c21a216ff38d4ca446857592fd15b4d90
+  # Parent  b31f68d8184de381eaaba311623e26db8779a9c4
+  Removed a
+  
+
+  $ hg prune .
+  hg: unknown command 'prune'
+  (use 'hg help' for a list of commands)
+  [255]
+  $ hg revert --all
+  undeleting a
+
+  $ glog
+  @  8:8543804c21a2@default(draft) Removed a
+  |
+  o  5:b31f68d8184d@default(draft) Added foo
+  |
+  o  1:f70fb463d5bf@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+
+Testing when a new file is added in the last commit
+===================================================
+
+  $ echo "foo" >> foo
+  $ touch x
+  $ echo "abcd" >> x
+  $ hg add x
+  $ hg ci -m "Added x"
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > y
+  > n
+  > EOF
+  diff --git a/foo b/foo
+  1 hunks, 1 lines changed
+  examine changes to 'foo'? [Ynesfdaq?] y
+  
+  @@ -1,1 +1,2 @@
+   hey
+  +foo
+  discard change 1/2 to 'foo'? [Ynesfdaq?] y
+  
+  diff --git a/x b/x
+  new file mode 100644
+  examine changes to 'x'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +abcd
+  discard change 2/2 to 'x'? [Ynesfdaq?] n
+  
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 9476ced971e5aaf898c1a847a0d813ac3b259b64
+  # Parent  8543804c21a216ff38d4ca446857592fd15b4d90
+  Added x
+  
+  diff -r 8543804c21a2 -r 9476ced971e5 x
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/x Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +abcd
+
+  $ hg diff
+  diff -r 9476ced971e5 foo
+  --- a/foo Thu Jan 01 00:00:00 1970 +0000
+  +++ b/foo Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,1 +1,2 @@
+   hey
+  +foo
+
+  $ hg status
+  M foo
+
+  $ hg revert --all
+  reverting foo
+
+Testing between the stack and with dirty working copy
+=====================================================
+
+  $ glog
+  @  11:9476ced971e5@default(draft) Added x
+  |
+  o  8:8543804c21a2@default(draft) Removed a
+  |
+  o  5:b31f68d8184d@default(draft) Added foo
+  |
+  o  1:f70fb463d5bf@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+  $ hg up f70fb463d5bf
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+
+  $ touch bar
+  $ echo "foo" >> bar
+  $ hg add bar
+  $ hg status
+  A bar
+  ? foo.orig
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID f70fb463d5bf9f0ffd38f105521d96e9f2591bc1
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r f70fb463d5bf a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+  $ hg uncommit -i<<EOF
+  > y
+  > n
+  > n
+  > y
+  > EOF
+  diff --git a/a b/a
+  3 hunks, 6 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -4,2 +9,3 @@
+   4
+   5
+  +babar
+  discard change 3/3 to 'a'? [Ynesfdaq?] y
+  
+  patching file a
+  Hunk #1 succeeded at 1 with fuzz 1 (offset -1 lines).
+  3 new orphan changesets
+
+  $ hg diff
+  diff -r c867648f402f a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -8,3 +8,4 @@
+   bar
+   4
+   5
+  +babar
+  diff -r c867648f402f bar
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/bar Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +foo
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID c867648f402fa4130636203db77810b9c76a4f47
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r c867648f402f a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,10 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  $ hg status
+  M a
+  A bar
+  ? foo.orig
+
+More uncommit on the same dirty working copy
+=============================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > n
+  > EOF
+  diff --git a/a b/a
+  2 hunks, 5 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/2 to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/2 to 'a'? [Ynesfdaq?] n
+  
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 46e35360be473bf761bedf3d05de4a68ffd9d9f8
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r 46e35360be47 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+
+  $ hg diff
+  diff -r 46e35360be47 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  @@ -5,3 +8,4 @@
+   bar
+   4
+   5
+  +babar
+  diff -r 46e35360be47 bar
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/bar Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +foo
+
+  $ hg status
+  M a
+  A bar
+  ? foo.orig
+
+Interactive uncommit with a pattern
+-----------------------------------
+
+(more setup)
+
+  $ hg ci -m 'roaming changes'
+  $ cat > b << EOF
+  > a
+  > b
+  > c
+  > d
+  > e
+  > f
+  > h
+  > EOF
+  $ hg add b
+  $ hg ci -m 'add b'
+  $ echo 'celeste' >> a
+  $ echo 'i' >> b
+  $ hg ci -m 'some more changes'
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 61cbe268a7c00ce4a801f62e743f30da82818003
+  # Parent  443d17b6b44b50d8fcd9c0cf6d635d95c00f3720
+  some more changes
+  
+  diff -r 443d17b6b44b -r 61cbe268a7c0 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  diff -r 443d17b6b44b -r 61cbe268a7c0 b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+  $ hg uncommit -i a << DONE
+  > y
+  > y
+  > DONE
+  diff --git a/a b/a
+  1 hunks, 1 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  discard this change to 'a'? [Ynesfdaq?] y
+  
+  $ hg status
+  M a
+  ? foo.orig
+  $ hg diff
+  diff -r 228b5665c0a7 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 228b5665c0a70e98d04b5a7bcaec33278e6ea448
+  # Parent  443d17b6b44b50d8fcd9c0cf6d635d95c00f3720
+  some more changes
+  
+  diff -r 443d17b6b44b -r 228b5665c0a7 b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+(reset)
+
+  $ cat << EOF  > a
+  > -3
+  > -2
+  > -1
+  > 0
+  > 1
+  > 2
+  > 3
+  > foo
+  > bar
+  > 4
+  > 5
+  > babar
+  > celeste
+  > EOF
+  $ hg amend
+
+Same but do not select some change in 'a'
+
+  $ hg uncommit -i a << DONE
+  > y
+  > y
+  > n
+  > DONE
+  diff --git a/a b/a
+  2 hunks, 2 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,4 @@
+  +-3
+   -2
+   -1
+   0
+  discard change 1/2 to 'a'? [Ynesfdaq?] y
+  
+  @@ -9,3 +10,4 @@
+   4
+   5
+   babar
+  +celeste
+  discard change 2/2 to 'a'? [Ynesfdaq?] n
+  
+  $ hg status
+  M a
+  ? foo.orig
+
+  $ hg diff
+  diff -r a8761d3de8f0 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,4 @@
+  +-3
+   -2
+   -1
+   0
+
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID a8761d3de8f0c2d89e2452dd7798644d63f9a58a
+  # Parent  443d17b6b44b50d8fcd9c0cf6d635d95c00f3720
+  some more changes
+  
+  diff -r 443d17b6b44b -r a8761d3de8f0 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  diff -r 443d17b6b44b -r a8761d3de8f0 b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+  $ cat b
+  a
+  b
+  c
+  d
+  e
+  f
+  h
+  i
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
--- a/hgext/uncommit.py
+++ b/hgext/uncommit.py
@@ -28,11 +28,14 @@
     copies,
     error,
     node,
+    obsolete,
     obsutil,
+    patch,
     pycompat,
     registrar,
     rewriteutil,
     scmutil,
+    util,
 )
 
 cmdtable = {}
@@ -45,6 +48,8 @@
     default=False,
 )
 
+stringio = util.stringio
+
 # 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
@@ -135,8 +140,107 @@
             src = None
         ds.copy(src, dst)
 
+
+def _uncommitdirstate(repo, oldctx, match, interactive):
+    """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())
+    if interactive:
+        # In interactive cases, we will find the status between oldctx and ctx
+        # and considering only the files which are changed between oldctx and
+        # ctx, and the status of what changed between oldctx and ctx will help
+        # us in defining the exact behavior
+        m, a, r = repo.status(oldctx, ctx, match=match)[:3]
+        for f in m:
+            # These are files which are modified between oldctx and ctx which
+            # contains two cases: 1) Were modified in oldctx and some
+            # modifications are uncommitted
+            # 2) Were added in oldctx but some part is uncommitted (this cannot
+            # contain the case when added files are uncommitted completely as
+            # that will result in status as removed not modified.)
+            # Also any modifications to a removed file will result the status as
+            # added, so we have only two cases. So in either of the cases, the
+            # resulting status can be modified or clean.
+            if ds[f] == 'r':
+                # But the file is removed in the working directory, leaving that
+                # as removed
+                continue
+            ds.normallookup(f)
+
+        for f in a:
+            # These are the files which are added between oldctx and ctx(new
+            # one), which means the files which were removed in oldctx
+            # but uncommitted completely while making the ctx
+            # This file should be marked as removed if the working directory
+            # does not adds it back. If it's adds it back, we do a normallookup.
+            # The file can't be removed in working directory, because it was
+            # removed in oldctx
+            if ds[f] == 'a':
+                ds.normallookup(f)
+                continue
+            ds.remove(f)
+
+        for f in r:
+            # These are files which are removed between oldctx and ctx, which
+            # means the files which were added in oldctx and were completely
+            # uncommitted in ctx. If a added file is partially uncommitted, that
+            # would have resulted in modified status, not removed.
+            # So a file added in a commit, and uncommitting that addition must
+            # result in file being stated as unknown.
+            if ds[f] == 'r':
+                # The working directory say it's removed, so lets make the file
+                # unknown
+                ds.drop(f)
+                continue
+            ds.add(f)
+    else:
+        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 = {}
+    if interactive:
+        # Interactive had different meaning of the variables so restoring the
+        # original meaning to use them
+        m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+    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)
+
 @command('uncommit',
-    [('', 'keep', False, _('allow an empty commit after uncommiting')),
+    [('i', 'interactive', False, _('interactive mode to uncommit')),
+    ('', 'keep', False, _('allow an empty commit after uncommiting')),
     ] + commands.walkopts,
     _('[OPTION]... [FILE]...'),
     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
@@ -152,6 +256,7 @@
     given.
     """
     opts = pycompat.byteskwargs(opts)
+    interactive = opts.get('interactive')
 
     with repo.wlock(), repo.lock():
 
@@ -167,6 +272,10 @@
             match = scmutil.match(old, pats, opts)
             keepcommit = opts.get('keep') or pats
             newid = _commitfiltered(repo, old, match, keepcommit)
+            if interactive:
+                match = scmutil.match(old, pats, opts)
+                newid = _interactiveuncommit(ui, repo, old, match)
+
             if newid is None:
                 ui.status(_("nothing to uncommit\n"))
                 return 1
@@ -183,8 +292,101 @@
 
             with repo.dirstate.parentchange():
                 repo.dirstate.setparents(newid, node.nullid)
-                s = old.p1().status(old, match=match)
-                _fixdirstate(repo, old, repo[newid], s)
+                _uncommitdirstate(repo, old, match, interactive)
+
+def _interactiveuncommit(ui, repo, old, match):
+    """ The function which contains all the logic for interactively uncommiting
+    a commit. This function makes a temporary commit with the chunks which user
+    selected to uncommit. After that the diff of the parent and that commit is
+    applied to the working directory and committed again which results in the
+    new commit which should be one after uncommitted.
+    """
+
+    # create a temporary commit with hunks user selected
+    tempnode = _createtempcommit(ui, repo, old, match)
+
+    diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+    diffopts.nodates = True
+    diffopts.git = True
+    fp = stringio()
+    for chunk, label in patch.diffui(repo, tempnode, old.node(), None,
+                                     opts=diffopts):
+            fp.write(chunk)
+
+    fp.seek(0)
+    newnode = _patchtocommit(ui, repo, old, fp)
+    # creating obs marker temp -> ()
+    obsolete.createmarkers(repo, [(repo[tempnode], ())], operation="uncommit")
+    return newnode
+def _createtempcommit(ui, repo, old, match):
+    """ Creates a temporary commit for `uncommit --interative` which contains
+    the hunks which were selected by the user to uncommit.
+    """
+
+    pold = old.p1()
+    # The logic to interactively selecting something copied from
+    # cmdutil.revert()
+    diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+    diffopts.nodates = True
+    diffopts.git = True
+    diff = patch.diff(repo, pold.node(), old.node(), match, opts=diffopts)
+    originalchunks = patch.parsepatch(diff)
+    # XXX: The interactive selection is buggy and does not let you
+    # uncommit a removed file partially.
+    # TODO: wrap the operations in mercurial/patch.py and mercurial/crecord.py
+    # to add uncommit as an operation taking care of BC.
+    chunks, opts = cmdutil.recordfilter(repo.ui, originalchunks,
+                                        operation='discard')
+    if not chunks:
+        raise error.Abort(_("nothing selected to uncommit"))
+    fp = stringio()
+    for c in chunks:
+            c.write(fp)
+
+    fp.seek(0)
+    oldnode = node.hex(old.node())[:12]
+    message = 'temporary commit for uncommiting %s' % oldnode
+    tempnode = _patchtocommit(ui, repo, old, fp, message, oldnode)
+    return tempnode
+
+def _patchtocommit(ui, repo, old, fp, message=None, extras=None):
+    """ A function which will apply the patch to the working directory and
+    make a commit whose parents are same as that of old argument. The message
+    argument tells us whether to use the message of the old commit or a
+    different message which is passed. Returns the node of new commit made.
+    """
+    pold = old.p1()
+    parents = (old.p1().node(), old.p2().node())
+    date = old.date()
+    branch = old.branch()
+    user = old.user()
+    extra = old.extra()
+    if extras:
+        extra['uncommit_source'] = extras
+    if not message:
+        message = old.description()
+    store = patch.filestore()
+    try:
+        files = set()
+        try:
+            patch.patchrepo(ui, repo, pold, store, fp, 1, '',
+                            files=files, eolmode=None)
+        except patch.PatchError as err:
+            raise error.Abort(str(err))
+
+        finally:
+            del fp
+
+        memctx = context.memctx(repo, parents, message, files=files,
+                                filectxfn=store,
+                                user=user,
+                                date=date,
+                                branch=branch,
+                                extra=extra)
+        newcm = memctx.commit()
+    finally:
+        store.close()
+    return newcm
 
 def predecessormarkers(ctx):
     """yields the obsolete markers marking the given changeset as a successor"""



To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 added a comment.


  @pulkit I was able to import the tests and make them compatible with `hg-stable` . Some tests which include commands like `hg obslog` , `hg amend --extract` , and `hg uncommit -n` which are not part of `hg-stable` but `evolve` are not running else all tests are now running smoothly .

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
pulkit added a comment.


  In https://phab.mercurial-scm.org/D5792#85236, @taapas1128 wrote:
 
  > @pulkit I was able to import the tests and make them compatible with `hg-stable` . Some tests which include commands like `hg obslog` , `hg amend --extract` , and `hg uncommit -n` which are not part of `hg-stable` but `evolve` are not running else all tests are now running smoothly .
 
 
  `hg amend --extract` is just another name for `hg uncommit`.
  You can move `--note` flag to core uncommit as a part of different patch.
  Related to obslog, you can do `hg log -r . -r precursor(.)` or `hg diff -r . -r precursor(.)` but I don't think that will be sufficient.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 added a comment.


  @pulkit I have added some inline comments . So what do you suggest one way can be merging this and I will send a follow up for integrating `-n` to `hg uncommit`and then amending the tests. The other way is I should send a request for `-n` flag and then make all these changes again as a follow up pr for that request.

INLINE COMMENTS

> test-uncommit-interactive.t:135
> +
> +  $ hg amend --extract -n "note on amend --extract" -i<<EOF
> +  > y

`hg amend --extract` uses `-n` flag here so it cant be completed with `hg uncommit` alone `-n` integration will be necessary. Also `hg obslog` is the same test so without this it can also not be replaced.

> test-uncommit-interactive.t:208
> +
> +  $ hg uncommit -n "testing uncommit on dirty wdir" -i<<EOF
> +  > y

Same problem here.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
pulkit added a comment.


  In https://phab.mercurial-scm.org/D5792#86123, @taapas1128 wrote:
 
  > @pulkit I have added some inline comments . So what do you suggest one way can be merging this and I will send a follow up for integrating `-n` to `hg uncommit`and then amending the tests. The other way is I should send a request for `-n` flag and then make all these changes again as a follow up pr for that request.
 
 
  It will be nice if you send a patch for `-n` flag first, then rebase these patch on top of that.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 updated this revision to Diff 13915.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D5792?vs=13738&id=13915

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

AFFECTED FILES
  hgext/uncommit.py
  tests/test-uncommit-interactive.t
  tests/test-uncommit.t

CHANGE DETAILS

diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
--- a/tests/test-uncommit.t
+++ b/tests/test-uncommit.t
@@ -34,6 +34,7 @@
   
   options ([+] can be repeated):
   
+   -i --interactive         interactive mode to uncommit
       --keep                allow an empty commit after uncommiting
    -I --include PATTERN [+] include names matching the given patterns
    -X --exclude PATTERN [+] exclude names matching the given patterns
diff --git a/tests/test-uncommit-interactive.t b/tests/test-uncommit-interactive.t
new file mode 100644
--- /dev/null
+++ b/tests/test-uncommit-interactive.t
@@ -0,0 +1,969 @@
+================================================
+||  The test for `hg uncommit --interactive`  ||
+================================================
+
+Repo Setup
+============
+
+  $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
+  > [experimental]
+  > evolution.createmarkers=True
+  > evolution.allowunstable=True
+  > uncommitondirtywdir = true
+  > [extensions]
+  > uncommit =
+  > amend =
+  > drawdag=$TESTDIR/drawdag.py
+  > EOF
+  $ glog() {
+  >   hg log -G --template '{rev}:{node|short}@{branch}({separate("/", obsolete, phase)}) {desc|firstline}\n' "$@"
+  > }
+
+  $ hg init repo
+  $ cd repo
+
+  $ touch a
+  $ cat >> a << EOF
+  > 1
+  > 2
+  > 3
+  > 4
+  > 5
+  > EOF
+
+  $ hg add a
+  $ hg ci -m "The base commit"
+
+Make sure aborting the interactive selection does no magic
+----------------------------------------------------------
+
+  $ hg status
+  $ hg uncommit -i<<EOF
+  > q
+  > EOF
+  diff --git a/a b/a
+  new file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] q
+  
+  abort: user quit
+  [255]
+  $ hg status
+
+Make a commit with multiple hunks
+---------------------------------
+
+  $ cat > a << EOF
+  > -2
+  > -1
+  > 0
+  > 1
+  > 2
+  > 3
+  > foo
+  > bar
+  > 4
+  > 5
+  > babar
+  > EOF
+
+  $ hg diff
+  diff -r 7733902a8d94 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+  $ hg ci -m "another one"
+
+Not selecting anything to uncommit
+==================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > n
+  > n
+  > n
+  > EOF
+  diff --git a/a b/a
+  3 hunks, 6 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -4,2 +9,3 @@
+   4
+   5
+  +babar
+  discard change 3/3 to 'a'? [Ynesfdaq?] n
+  
+  abort: nothing selected to uncommit
+  [255]
+  $ hg status
+
+Uncommit a chunk
+================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > n
+  > n
+  > EOF
+  diff --git a/a b/a
+  3 hunks, 6 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/3 to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -4,2 +9,3 @@
+   4
+   5
+  +babar
+  discard change 3/3 to 'a'? [Ynesfdaq?] n
+  
+
+  $ hg log -G --hidden
+  @  changeset:   3:678a59e5ff90
+  |  tag:         tip
+  |  parent:      0:7733902a8d94
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     another one
+  |
+  | x  changeset:   2:e9635f4beaf1
+  |/   parent:      0:7733902a8d94
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    obsolete:    pruned using uncommit
+  |    summary:     temporary commit for uncommiting f70fb463d5bf
+  |
+  | x  changeset:   1:f70fb463d5bf
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    obsolete:    rewritten using uncommit as 3:678a59e5ff90
+  |    summary:     another one
+  |
+  o  changeset:   0:7733902a8d94
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     The base commit
+  
+The unselected part should be in the diff
+-----------------------------------------
+
+  $ hg diff
+  diff -r 678a59e5ff90 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+
+The commit should contain the rest of part
+------------------------------------------
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 678a59e5ff90754d5e94719bd82ad169be773c21
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r 678a59e5ff90 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,8 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+Uncommiting on dirty working directory
+======================================
+
+  $ hg status
+  M a
+  $ hg diff
+  diff -r 678a59e5ff90 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+
+  $ hg uncommit -i<<EOF
+  > y
+  > n
+  > y
+  > EOF
+  diff --git a/a b/a
+  2 hunks, 3 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,5 +1,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 1/2 to 'a'? [Ynesfdaq?] n
+  
+  @@ -4,2 +6,3 @@
+   4
+   5
+  +babar
+  discard change 2/2 to 'a'? [Ynesfdaq?] y
+  
+  patching file a
+  Hunk #1 succeeded at 2 with fuzz 1 (offset 0 lines).
+
+  $ hg diff
+  diff -r 46e35360be47 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  @@ -5,3 +8,4 @@
+   bar
+   4
+   5
+  +babar
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 46e35360be473bf761bedf3d05de4a68ffd9d9f8
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r 46e35360be47 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+
+Checking the obsolescence history
+
+  $ hg log -G --hidden
+  @  changeset:   5:46e35360be47
+  |  tag:         tip
+  |  parent:      0:7733902a8d94
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     another one
+  |
+  | x  changeset:   4:7ca9935a62f1
+  |/   parent:      0:7733902a8d94
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    obsolete:    pruned using uncommit
+  |    summary:     temporary commit for uncommiting 678a59e5ff90
+  |
+  | x  changeset:   3:678a59e5ff90
+  |/   parent:      0:7733902a8d94
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    obsolete:    rewritten using uncommit as 5:46e35360be47
+  |    summary:     another one
+  |
+  | x  changeset:   2:e9635f4beaf1
+  |/   parent:      0:7733902a8d94
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    obsolete:    pruned using uncommit
+  |    summary:     temporary commit for uncommiting f70fb463d5bf
+  |
+  | x  changeset:   1:f70fb463d5bf
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    obsolete:    rewritten using uncommit as 3:678a59e5ff90
+  |    summary:     another one
+  |
+  o  changeset:   0:7733902a8d94
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     The base commit
+  
+Push the changes back to the commit and more commits for more testing
+
+  $ hg amend
+  $ glog
+  @  6:905eb2a23ea2@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+  $ touch foo
+  $ echo "hey" >> foo
+  $ hg ci -Am "Added foo"
+  adding foo
+
+Testing uncommiting a whole changeset and also for a file addition
+==================================================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > EOF
+  diff --git a/foo b/foo
+  new file mode 100644
+  examine changes to 'foo'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +hey
+  discard this change to 'foo'? [Ynesfdaq?] y
+  
+
+  $ hg status
+  A foo
+  $ hg diff
+  diff -r 857367499298 foo
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/foo Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +hey
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 857367499298e999b5841bb01df65f73088b5d3b
+  # Parent  905eb2a23ea2d92073419d0e19165b90d36ea223
+  Added foo
+  
+  $ hg amend
+
+Testing to uncommit removed files completely
+============================================
+
+  $ hg rm a
+  $ hg ci -m "Removed a"
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 219cfe20964e93f8bb9bd82ceaa54d3b776046db
+  # Parent  42cc15efbec26c14d96d805dee2766ba91d1fd31
+  Removed a
+  
+  diff -r 42cc15efbec2 -r 219cfe20964e a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,11 +0,0 @@
+  --2
+  --1
+  -0
+  -1
+  -2
+  -3
+  -foo
+  -bar
+  -4
+  -5
+  -babar
+
+Not examining the file
+----------------------
+
+  $ hg uncommit -i<<EOF
+  > n
+  > EOF
+  diff --git a/a b/a
+  deleted file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] n
+  
+  abort: nothing selected to uncommit
+  [255]
+
+Examining the file
+------------------
+XXX: there is a bug in interactive selection as it is not letting to examine the
+file. Tried with curses too. In the curses UI, if you just unselect the hunks
+and the not file mod thing at the top, it will show the same "nothing unselected
+to uncommit" message which is a bug in interactive selection.
+
+  $ hg uncommit -i<<EOF
+  > y
+  > EOF
+  diff --git a/a b/a
+  deleted file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+
+  $ hg diff
+  diff -r 737487f1e5f8 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,11 +0,0 @@
+  --2
+  --1
+  -0
+  -1
+  -2
+  -3
+  -foo
+  -bar
+  -4
+  -5
+  -babar
+  $ hg status
+  R a
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 737487f1e5f853e55decb73ea31522c63e7f5980
+  # Parent  42cc15efbec26c14d96d805dee2766ba91d1fd31
+  Removed a
+  
+
+  $ hg prune .
+  hg: unknown command 'prune'
+  (use 'hg help' for a list of commands)
+  [255]
+  $ hg revert --all
+  undeleting a
+
+  $ glog
+  @  13:737487f1e5f8@default(draft) Removed a
+  |
+  o  10:42cc15efbec2@default(draft) Added foo
+  |
+  o  6:905eb2a23ea2@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+
+Testing when a new file is added in the last commit
+===================================================
+
+  $ echo "foo" >> foo
+  $ touch x
+  $ echo "abcd" >> x
+  $ hg add x
+  $ hg ci -m "Added x"
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > y
+  > n
+  > EOF
+  diff --git a/foo b/foo
+  1 hunks, 1 lines changed
+  examine changes to 'foo'? [Ynesfdaq?] y
+  
+  @@ -1,1 +1,2 @@
+   hey
+  +foo
+  discard change 1/2 to 'foo'? [Ynesfdaq?] y
+  
+  diff --git a/x b/x
+  new file mode 100644
+  examine changes to 'x'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +abcd
+  discard change 2/2 to 'x'? [Ynesfdaq?] n
+  
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID f7b39cf595081f8e63fe1119953cc7f669663720
+  # Parent  737487f1e5f853e55decb73ea31522c63e7f5980
+  Added x
+  
+  diff -r 737487f1e5f8 -r f7b39cf59508 x
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/x Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +abcd
+
+  $ hg diff
+  diff -r f7b39cf59508 foo
+  --- a/foo Thu Jan 01 00:00:00 1970 +0000
+  +++ b/foo Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,1 +1,2 @@
+   hey
+  +foo
+
+  $ hg status
+  M foo
+
+  $ hg revert --all
+  reverting foo
+
+Testing between the stack and with dirty working copy
+=====================================================
+
+  $ glog
+  @  16:f7b39cf59508@default(draft) Added x
+  |
+  o  13:737487f1e5f8@default(draft) Removed a
+  |
+  o  10:42cc15efbec2@default(draft) Added foo
+  |
+  o  6:905eb2a23ea2@default(draft) another one
+  |
+  o  0:7733902a8d94@default(draft) The base commit
+  
+  $ hg up 905eb2a23ea2
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+
+  $ touch bar
+  $ echo "foo" >> bar
+  $ hg add bar
+  $ hg status
+  A bar
+  ? foo.orig
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 905eb2a23ea2d92073419d0e19165b90d36ea223
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r 905eb2a23ea2 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,11 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  +babar
+
+  $ hg uncommit -i<<EOF
+  > y
+  > n
+  > n
+  > y
+  > EOF
+  diff --git a/a b/a
+  3 hunks, 6 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/3 to 'a'? [Ynesfdaq?] n
+  
+  @@ -4,2 +9,3 @@
+   4
+   5
+  +babar
+  discard change 3/3 to 'a'? [Ynesfdaq?] y
+  
+  patching file a
+  Hunk #1 succeeded at 1 with fuzz 1 (offset -1 lines).
+  3 new orphan changesets
+
+  $ hg diff
+  diff -r 676366511f95 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -8,3 +8,4 @@
+   bar
+   4
+   5
+  +babar
+  diff -r 676366511f95 bar
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/bar Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +foo
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 676366511f95ca4122413dcf79b45eaab61fb387
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r 676366511f95 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,10 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  $ hg status
+  M a
+  A bar
+  ? foo.orig
+
+More uncommit on the same dirty working copy
+=============================================
+
+  $ hg uncommit -i<<EOF
+  > y
+  > y
+  > n
+  > EOF
+  diff --git a/a b/a
+  2 hunks, 5 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  discard change 1/2 to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,5 +4,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+  discard change 2/2 to 'a'? [Ynesfdaq?] n
+  
+
+  $ hg exp
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 62d907d0c4fa13b4b8bfeed05f13751035daf963
+  # Parent  7733902a8d94c789ca81d866bea1893d79442db6
+  another one
+  
+  diff -r 7733902a8d94 -r 62d907d0c4fa a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,5 +1,7 @@
+   1
+   2
+   3
+  +foo
+  +bar
+   4
+   5
+
+  $ hg diff
+  diff -r 62d907d0c4fa a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,6 @@
+  +-2
+  +-1
+  +0
+   1
+   2
+   3
+  @@ -5,3 +8,4 @@
+   bar
+   4
+   5
+  +babar
+  diff -r 62d907d0c4fa bar
+  --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+  +++ b/bar Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +foo
+
+  $ hg status
+  M a
+  A bar
+  ? foo.orig
+
+Interactive uncommit with a pattern
+-----------------------------------
+
+(more setup)
+
+  $ hg ci -m 'roaming changes'
+  $ cat > b << EOF
+  > a
+  > b
+  > c
+  > d
+  > e
+  > f
+  > h
+  > EOF
+  $ hg add b
+  $ hg ci -m 'add b'
+  $ echo 'celeste' >> a
+  $ echo 'i' >> b
+  $ hg ci -m 'some more changes'
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID be5c67225e80b050867862bbd9f4755c4e9207c5
+  # Parent  c280a907fddcef2ffe9fadcc2d87f29998e22b2f
+  some more changes
+  
+  diff -r c280a907fddc -r be5c67225e80 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  diff -r c280a907fddc -r be5c67225e80 b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+  $ hg uncommit -i a << DONE
+  > y
+  > y
+  > DONE
+  diff --git a/a b/a
+  1 hunks, 1 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  discard this change to 'a'? [Ynesfdaq?] y
+  
+  $ hg status
+  M a
+  ? foo.orig
+  $ hg diff
+  diff -r c701d7c8d18b a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID c701d7c8d18be55a92688f4458c26bd74fb1f525
+  # Parent  c280a907fddcef2ffe9fadcc2d87f29998e22b2f
+  some more changes
+  
+  diff -r c280a907fddc -r c701d7c8d18b b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+(reset)
+
+  $ cat << EOF  > a
+  > -3
+  > -2
+  > -1
+  > 0
+  > 1
+  > 2
+  > 3
+  > foo
+  > bar
+  > 4
+  > 5
+  > babar
+  > celeste
+  > EOF
+  $ hg amend
+
+Same but do not select some change in 'a'
+
+  $ hg uncommit -i a << DONE
+  > y
+  > y
+  > n
+  > DONE
+  diff --git a/a b/a
+  2 hunks, 2 lines changed
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -1,3 +1,4 @@
+  +-3
+   -2
+   -1
+   0
+  discard change 1/2 to 'a'? [Ynesfdaq?] y
+  
+  @@ -9,3 +10,4 @@
+   4
+   5
+   babar
+  +celeste
+  discard change 2/2 to 'a'? [Ynesfdaq?] n
+  
+  $ hg status
+  M a
+  ? foo.orig
+
+  $ hg diff
+  diff -r 28d5de12b225 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,3 +1,4 @@
+  +-3
+   -2
+   -1
+   0
+
+  $ hg export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 28d5de12b225d1e0951110cced8d8994227be026
+  # Parent  c280a907fddcef2ffe9fadcc2d87f29998e22b2f
+  some more changes
+  
+  diff -r c280a907fddc -r 28d5de12b225 a
+  --- a/a Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a Thu Jan 01 00:00:00 1970 +0000
+  @@ -9,3 +9,4 @@
+   4
+   5
+   babar
+  +celeste
+  diff -r c280a907fddc -r 28d5de12b225 b
+  --- a/b Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b Thu Jan 01 00:00:00 1970 +0000
+  @@ -5,3 +5,4 @@
+   e
+   f
+   h
+  +i
+
+  $ cat b
+  a
+  b
+  c
+  d
+  e
+  f
+  h
+  i
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
--- a/hgext/uncommit.py
+++ b/hgext/uncommit.py
@@ -28,11 +28,14 @@
     copies,
     error,
     node,
+    obsolete,
     obsutil,
+    patch,
     pycompat,
     registrar,
     rewriteutil,
     scmutil,
+    util,
 )
 
 cmdtable = {}
@@ -45,6 +48,8 @@
     default=False,
 )
 
+stringio = util.stringio
+
 # 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
@@ -135,8 +140,107 @@
             src = None
         ds.copy(src, dst)
 
+
+def _uncommitdirstate(repo, oldctx, match, interactive):
+    """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())
+    if interactive:
+        # In interactive cases, we will find the status between oldctx and ctx
+        # and considering only the files which are changed between oldctx and
+        # ctx, and the status of what changed between oldctx and ctx will help
+        # us in defining the exact behavior
+        m, a, r = repo.status(oldctx, ctx, match=match)[:3]
+        for f in m:
+            # These are files which are modified between oldctx and ctx which
+            # contains two cases: 1) Were modified in oldctx and some
+            # modifications are uncommitted
+            # 2) Were added in oldctx but some part is uncommitted (this cannot
+            # contain the case when added files are uncommitted completely as
+            # that will result in status as removed not modified.)
+            # Also any modifications to a removed file will result the status as
+            # added, so we have only two cases. So in either of the cases, the
+            # resulting status can be modified or clean.
+            if ds[f] == 'r':
+                # But the file is removed in the working directory, leaving that
+                # as removed
+                continue
+            ds.normallookup(f)
+
+        for f in a:
+            # These are the files which are added between oldctx and ctx(new
+            # one), which means the files which were removed in oldctx
+            # but uncommitted completely while making the ctx
+            # This file should be marked as removed if the working directory
+            # does not adds it back. If it's adds it back, we do a normallookup.
+            # The file can't be removed in working directory, because it was
+            # removed in oldctx
+            if ds[f] == 'a':
+                ds.normallookup(f)
+                continue
+            ds.remove(f)
+
+        for f in r:
+            # These are files which are removed between oldctx and ctx, which
+            # means the files which were added in oldctx and were completely
+            # uncommitted in ctx. If a added file is partially uncommitted, that
+            # would have resulted in modified status, not removed.
+            # So a file added in a commit, and uncommitting that addition must
+            # result in file being stated as unknown.
+            if ds[f] == 'r':
+                # The working directory say it's removed, so lets make the file
+                # unknown
+                ds.drop(f)
+                continue
+            ds.add(f)
+    else:
+        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 = {}
+    if interactive:
+        # Interactive had different meaning of the variables so restoring the
+        # original meaning to use them
+        m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+    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)
+
 @command('uncommit',
-    [('', 'keep', False, _('allow an empty commit after uncommiting')),
+    [('i', 'interactive', False, _('interactive mode to uncommit')),
+    ('', 'keep', False, _('allow an empty commit after uncommiting')),
     ] + commands.walkopts,
     _('[OPTION]... [FILE]...'),
     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
@@ -152,6 +256,7 @@
     given.
     """
     opts = pycompat.byteskwargs(opts)
+    interactive = opts.get('interactive')
 
     with repo.wlock(), repo.lock():
 
@@ -167,6 +272,10 @@
             match = scmutil.match(old, pats, opts)
             keepcommit = opts.get('keep') or pats
             newid = _commitfiltered(repo, old, match, keepcommit)
+            if interactive:
+                match = scmutil.match(old, pats, opts)
+                newid = _interactiveuncommit(ui, repo, old, match)
+
             if newid is None:
                 ui.status(_("nothing to uncommit\n"))
                 return 1
@@ -183,8 +292,101 @@
 
             with repo.dirstate.parentchange():
                 repo.dirstate.setparents(newid, node.nullid)
-                s = old.p1().status(old, match=match)
-                _fixdirstate(repo, old, repo[newid], s)
+                _uncommitdirstate(repo, old, match, interactive)
+
+def _interactiveuncommit(ui, repo, old, match):
+    """ The function which contains all the logic for interactively uncommiting
+    a commit. This function makes a temporary commit with the chunks which user
+    selected to uncommit. After that the diff of the parent and that commit is
+    applied to the working directory and committed again which results in the
+    new commit which should be one after uncommitted.
+    """
+
+    # create a temporary commit with hunks user selected
+    tempnode = _createtempcommit(ui, repo, old, match)
+
+    diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+    diffopts.nodates = True
+    diffopts.git = True
+    fp = stringio()
+    for chunk, label in patch.diffui(repo, tempnode, old.node(), None,
+                                     opts=diffopts):
+            fp.write(chunk)
+
+    fp.seek(0)
+    newnode = _patchtocommit(ui, repo, old, fp)
+    # creating obs marker temp -> ()
+    obsolete.createmarkers(repo, [(repo[tempnode], ())], operation="uncommit")
+    return newnode
+def _createtempcommit(ui, repo, old, match):
+    """ Creates a temporary commit for `uncommit --interative` which contains
+    the hunks which were selected by the user to uncommit.
+    """
+
+    pold = old.p1()
+    # The logic to interactively selecting something copied from
+    # cmdutil.revert()
+    diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+    diffopts.nodates = True
+    diffopts.git = True
+    diff = patch.diff(repo, pold.node(), old.node(), match, opts=diffopts)
+    originalchunks = patch.parsepatch(diff)
+    # XXX: The interactive selection is buggy and does not let you
+    # uncommit a removed file partially.
+    # TODO: wrap the operations in mercurial/patch.py and mercurial/crecord.py
+    # to add uncommit as an operation taking care of BC.
+    chunks, opts = cmdutil.recordfilter(repo.ui, originalchunks,
+                                        operation='discard')
+    if not chunks:
+        raise error.Abort(_("nothing selected to uncommit"))
+    fp = stringio()
+    for c in chunks:
+            c.write(fp)
+
+    fp.seek(0)
+    oldnode = node.hex(old.node())[:12]
+    message = 'temporary commit for uncommiting %s' % oldnode
+    tempnode = _patchtocommit(ui, repo, old, fp, message, oldnode)
+    return tempnode
+
+def _patchtocommit(ui, repo, old, fp, message=None, extras=None):
+    """ A function which will apply the patch to the working directory and
+    make a commit whose parents are same as that of old argument. The message
+    argument tells us whether to use the message of the old commit or a
+    different message which is passed. Returns the node of new commit made.
+    """
+    pold = old.p1()
+    parents = (old.p1().node(), old.p2().node())
+    date = old.date()
+    branch = old.branch()
+    user = old.user()
+    extra = old.extra()
+    if extras:
+        extra['uncommit_source'] = extras
+    if not message:
+        message = old.description()
+    store = patch.filestore()
+    try:
+        files = set()
+        try:
+            patch.patchrepo(ui, repo, pold, store, fp, 1, '',
+                            files=files, eolmode=None)
+        except patch.PatchError as err:
+            raise error.Abort(str(err))
+
+        finally:
+            del fp
+
+        memctx = context.memctx(repo, parents, message, files=files,
+                                filectxfn=store,
+                                user=user,
+                                date=date,
+                                branch=branch,
+                                extra=extra)
+        newcm = memctx.commit()
+    finally:
+        store.close()
+    return newcm
 
 def predecessormarkers(ctx):
     """yields the obsolete markers marking the given changeset as a successor"""



To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 added a comment.


  @pulkit Apparently it seems removing `-n` flag altogether will not interfere with the tests and they can be completed without that flag too . I have added inline comments please review.

INLINE COMMENTS

> test-uncommit-interactive.t:134
> +================
> +
> +  $ hg uncommit -i<<EOF

replaced hg amend --extract by hg uncommit -i

> test-uncommit-interactive.t:253
> +   3
> +
> +  $ hg uncommit -i<<EOF

Removed `-n` flag here

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 added a comment.


  @pulkit Can you please review this . I did not send a patch regarding `-n` flag because tests could be completed without it and also it would mean i would have to import large chunks of code.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
pulkit added a comment.


  The import looks fine to me. I will let someone else review the code because I authored this code in first place in evolve extension. Also it will be nice if you specify that the code is imported from evolve extension and also specify the commit hash from which you imported.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
taapas1128 added a comment.


  @pulkit I have updated the description .

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D5792: uncommit: added interactive mode(issue6062)

pulkit (Pulkit Goyal)
In reply to this post by pulkit (Pulkit Goyal)
pulkit added a comment.


  In https://phab.mercurial-scm.org/D5792#87115, @taapas1128 wrote:
 
  > @pulkit I have updated the description .
 
 
  Great thanks! As mentioned before I will let someone else review this :)

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D5792

To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
12