--- 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")),