diff -r 89b205e5271e -r b0458b9e1b47 hgext3rd/evolve/cmdrewrite.py --- 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")),