diff --git a/mercurial/state.py b/mercurial/state.py --- a/mercurial/state.py +++ b/mercurial/state.py @@ -16,6 +16,8 @@ from __future__ import absolute_import +import contextlib + from .i18n import _ from . import error, pycompat, util @@ -114,6 +116,7 @@ reportonly, continueflag, stopflag, + childopnames, cmdmsg, cmdhint, statushint, @@ -127,6 +130,7 @@ self._reportonly = reportonly self._continueflag = continueflag self._stopflag = stopflag + self._childopnames = childopnames self._cmdmsg = cmdmsg self._cmdhint = cmdhint self._statushint = statushint @@ -185,6 +189,7 @@ # A list of statecheck objects for multistep operations like graft. _unfinishedstates = [] +_unfinishedstatesbyname = {} def addunfinished( @@ -195,6 +200,7 @@ reportonly=False, continueflag=False, stopflag=False, + childopnames=[], cmdmsg=b'', cmdhint=b'', statushint=b'', @@ -217,6 +223,8 @@ `--continue` option or not. stopflag is a boolean that determines whether or not a command supports --stop flag + childopnames is a list of other opnames this op uses as sub-steps of its + own execution. They must already be added. cmdmsg is used to pass a different status message in case standard message of the format "abort: cmdname in progress" is not desired. cmdhint is used to pass a different hint message in case standard @@ -237,17 +245,69 @@ reportonly, continueflag, stopflag, + childopnames, cmdmsg, cmdhint, statushint, abortfunc, continuefunc, ) + if opname == b'merge': _unfinishedstates.append(statecheckobj) else: + # This check enforces that for any op 'foo' which depends on op 'bar', + # 'foo' comes before 'bar' in _unfinishedstates. This ensures that + # getrepostate() always returns the most specific applicable answer. + for childopname in childopnames: + if childopname not in _unfinishedstatesbyname: + raise error.ProgrammingError( + _(b'op %s depends on unknown op %s') % (opname, childopname) + ) + _unfinishedstates.insert(0, statecheckobj) + if opname in _unfinishedstatesbyname: + raise error.ProgrammingError(_(b'op %s registered twice') % opname) + _unfinishedstatesbyname[opname] = statecheckobj + + +@contextlib.contextmanager +def delegating(repo, opname, childopname): + """context wrapper for delegations from opname to childopname. + + requires that childopname was specified when opname was registered. + + Usage: + def my_command_foo_that_uses_rebase(...): + ... + with state.delegating(repo, 'foo', 'rebase'): + _run_rebase(...) + ... + """ + + s = _unfinishedstatesbyname[parentopname] + if not s: + raise error.ProgrammingError(_(b'unknown op %s') % opname) + if childopname not in s._childopnames: + raise error.ProgrammingError( + _(b'op %s does not delegate to %s') % (opname, childopname) + ) + c = _unfinishedstatesbyname[childopname] + + newskipstates = set(repo.ui.configlist(b'commands', b'status.skipstates')) + newskipstates.add(childopname) + with repo.ui.configoverride( + {(b'commands', b'status.skipstates'): newskipstates} + ): + try: + yield + except error.ConflictResolutionRequired as e: + # Rewrite conflict resolution advice for the parent opname. + if e.opname == childopname: + raise error.ConflictResolutionRequired(opname) + raise e + addunfinished( b'update',