hgext3rd/evolve/cmdrewrite.py
changeset 2941 b0458b9e1b47
parent 2940 89b205e5271e
child 2942 10194206acc7
--- a/hgext3rd/evolve/cmdrewrite.py	Mon Sep 04 15:54:39 2017 +0200
+++ b/hgext3rd/evolve/cmdrewrite.py	Thu Sep 14 17:23:24 2017 +0530
@@ -24,6 +24,7 @@
     lock as lockmod,
     node,
     obsolete,
+    patch,
     phases,
     scmutil,
     util,
@@ -44,6 +45,7 @@
 commitopts = commands.commitopts
 commitopts2 = commands.commitopts2
 mergetoolopts = commands.mergetoolopts
+stringio = util.stringio
 
 # option added by evolve
 
@@ -234,6 +236,7 @@
 @eh.command(
     '^uncommit',
     [('a', 'all', None, _('uncommit all changes when no arguments given')),
+     ('i', 'interactive', False, _('interactive mode to uncommit (EXPERIMENTAL)')),
      ('r', 'rev', '', _('revert commit content to REV instead')),
      ] + commands.walkopts + commitopts + commitopts2 + commitopts3,
     _('[OPTION]... [NAME]'))
@@ -252,10 +255,16 @@
     revision. It still does not change the content of your file in the working
     directory.
 
+    .. container:: verbose
+
+       The --interactive option lets you select hunks interactively to uncommit.
+       You can uncommit parts of file using this option.
+
     Return 0 if changed files are uncommitted.
     """
 
     _resolveoptions(ui, opts) # process commitopts3
+    interactive = opts.get('interactive')
     wlock = lock = tr = None
     try:
         wlock = repo.wlock()
@@ -287,7 +296,11 @@
         # Recommit the filtered changeset
         tr = repo.transaction('uncommit')
         updatebookmarks = rewriteutil.bookmarksupdater(repo, old.node(), tr)
-        if True:
+        if interactive:
+            opts['all'] = True
+            match = scmutil.match(old, pats, opts)
+            newid = _interactiveuncommit(ui, repo, old, match)
+        else:
             newid = None
             includeorexclude = opts.get('include') or opts.get('exclude')
             if (pats or includeorexclude or opts.get('all')):
@@ -301,7 +314,7 @@
             if newid is None:
                 raise error.Abort(_('nothing to uncommit'),
                                   hint=_("use --all to uncommit all files"))
-        # Move local changes on filtered changeset
+
         obsolete.createmarkers(repo, [(old, (repo[newid],))])
         phases.retractboundary(repo, tr, oldphase, [newid])
         with repo.dirstate.parentchange():
@@ -315,6 +328,101 @@
     finally:
         lockmod.release(tr, lock, wlock)
 
+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], ())])
+    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
+
 @eh.command(
     '^fold|squash',
     [('r', 'rev', [], _("revision to fold")),