diff --git a/hgext/amend.py b/hgext/amend.py --- a/hgext/amend.py +++ b/hgext/amend.py @@ -16,7 +16,15 @@ from mercurial import ( cmdutil, commands, + error, + merge, + obsolete, registrar, + repair, + rewriteutil, + scmutil, + state as statemod, + util, ) # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for @@ -48,6 +56,14 @@ ), (b's', b'secret', None, _(b'use the secret phase for committing')), (b'n', b'note', b'', _(b'store a note on the amend')), + ( + b'r', + b'rev', + b'', + _(b'amend into the specified revision (EXPERIMENTAL)'), + ), + (b'', b'abort', False, _(b'abort an interrupted "hg amend -r"')), + (b'', b'continue', False, _(b'continue an interrupted "hg amend -r"')), ] + cmdutil.walkopts + cmdutil.commitopts @@ -67,8 +83,130 @@ """ cmdutil.check_note_size(opts) - with repo.wlock(), repo.lock(): - if not opts.get('logfile'): - opts['message'] = opts.get('message') or repo[b'.'].description() - opts['amend'] = True - return commands._docommit(ui, repo, *pats, **opts) + action = cmdutil.check_at_most_one_arg(opts, 'abort', 'continue') + if action: + cmdutil.check_incompatible_arguments( + opts, + action, + [ + 'addremove', + 'edit', + 'interactive', + 'close_branch', + 'rev', + 'secret', + 'note', + 'include', + 'exclude', + 'message', + 'logfile', + 'date', + 'user', + 'currentdate', + 'currentuser', + ], + ) + + if opts.get('rev'): + return _amend_rev(ui, repo, *pats, **opts) + elif opts.get('continue'): + _continue_amend_rev(ui, repo) + elif opts.get('abort'): + _abort_amend_rev(ui, repo) + else: + with repo.wlock(), repo.lock(): + if not opts.get('logfile'): + opts['message'] = ( + opts.get('message') or repo[b'.'].description() + ) + opts['amend'] = True + return commands._docommit(ui, repo, *pats, **opts) + + +def _amend_rev(ui, repo, *pats, **opts): + # TODO: Add support for most of these + cmdutil.check_incompatible_arguments( + opts, + 'rev', + [ + 'addremove', + 'edit', + 'interactive', + 'close_branch', + 'secret', + 'note', + 'include', + 'exclude', + 'message', + 'logfile', + 'date', + 'user', + 'currentdate', + 'currentuser', + ], + ) + + if not obsolete.isenabled(repo, obsolete.createmarkersopt): + raise error.StateError( + _(b'--rev requires evolution.createmarkers to be enabled') + ) + + state = {} + state_store = statemod.cmdstate(repo, b'amend-state') + + with repo.wlock(), repo.lock(), util.acceptintervention( + repo.transaction(b'amend') + ), state_store.save_on_conflicts(1, state): + cmdutil.checkunfinished(repo) + target_ctx = scmutil.revsingle(repo, opts['rev']) + to_rewrite = repo.revs(b'%d::.', target_ctx.rev()) + rewriteutil.precheck(repo, to_rewrite, b'amend') + + try: + # Create a temporary commit of the working copy. + ret = commands._docommit( + ui, repo, message=b'temporary commit for "amend --rev"' + ) + if ret: + return ret + temp_ctx = repo[b'tip'] + state[b'target_node'] = target_ctx.node() + state[b'temp_node'] = temp_ctx.node() + except error.InterventionRequired: + raise + except Exception: + _do_abort_amend_rev(ui, repo, state) + raise + + +def _continue_amend_rev(ui, repo): + raise error.Abort(_(b'--continue is not yet implemented')) + + +def _abort_amend_rev(ui, repo): + with repo.wlock(), repo.lock(), repo.transaction(b'amend'): + state_store = statemod.cmdstate(repo, b'amend-state') + state = state_store.read() + _do_abort_amend_rev(ui, repo, state) + state_store.delete() + + +def _do_abort_amend_rev(ui, repo, state): + unfi = repo.unfiltered() + temp_node = state.get(b'temp_node') + if temp_node and temp_node in unfi: + temp_ctx = unfi[temp_node] + merge.clean_update(temp_ctx) + with repo.dirstate.parentchange(): + scmutil.movedirstate(repo, temp_ctx.p1()) + repair.delayedstrip(ui, repo, [temp_node]) + + +def extsetup(ui): + statemod.addunfinished( + b'amend', + fname=b'amend-state', + allowcommit=False, + abortfunc=_abort_amend_rev, + continuefunc=_continue_amend_rev, + ) diff --git a/mercurial/state.py b/mercurial/state.py --- a/mercurial/state.py +++ b/mercurial/state.py @@ -81,6 +81,25 @@ for chunk in cborutil.streamencode(data): fp.write(chunk) + @contextlib.contextmanager + def save_on_conflicts(self, version, data): + """Saves the state if an InterventionRequired exception occurs. + + This context manager saves the state if an InterventionRequired + exception occurs and deletes it any existing state if no exception + occurs. + + This context manager holds a reference to the data argument, not a copy + of it, so the caller can safely update the object until this context + manager exits. + """ + try: + yield + self.delete() + except error.InterventionRequired: + self.save(version, data) + raise + def _read(self): """reads the state file and returns a dictionary which contain data in the same format as it was before storing""" diff --git a/tests/test-amend-rev.t b/tests/test-amend-rev.t new file mode 100644 --- /dev/null +++ b/tests/test-amend-rev.t @@ -0,0 +1,58 @@ + + $ cat << EOF >> $HGRCPATH + > [extensions] + > amend= + > debugdrawdag=$TESTDIR/drawdag.py + > [experimental] + > evolution.createmarkers=True + > EOF + +Some simple setup + + $ hg init amend-into-grandparent + $ cd amend-into-grandparent + $ echo a > a + $ hg ci -Aqm 'add a' + $ echo a2 > a + $ hg ci -m 'modify a' + $ echo b > b + $ hg ci -Aqm 'add b' + $ hg log -G -T '{rev} {desc}' + @ 2 add b + | + o 1 modify a + | + o 0 add a + + +Fails if the working copy is clean + + $ hg amend -r 'desc("modify a")' + nothing changed + [1] + +Fails if evolution is not enabled + + $ echo a3 > a + $ hg amend -r 'desc("modify a")' --config experimental.evolution.createmarkers=False + abort: --rev requires evolution.createmarkers to be enabled + [20] + +Can amend into grandparent + + $ hg amend -r 'desc("modify a")' + $ hg log -G -T '{rev} {desc}' + @ 3 temporary commit for "amend --rev" (known-bad-output !) + | (known-bad-output !) + o 2 add b + | + o 1 modify a + | + o 0 add a + +Target commit has new content + $ hg cat -r 'desc("modify a")' a + a2 (known-bad-output !) + a3 (missing-correct-output !) +The working copy is clean and there is no unfinished operation + $ hg st -v