state.py
385 lines
| 12.0 KiB
| text/x-python
|
PythonLexer
/ mercurial / state.py
|
r38115 | # state.py - writing and reading state files in Mercurial | ||
# | ||||
# Copyright 2018 Pulkit Goyal <pulkitmgoyal@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
""" | ||||
This file contains class to wrap the state for commands and other | ||||
related logic. | ||||
All the data related to the command state is stored as dictionary in the object. | ||||
The class has methods using which the data can be stored to disk in a file under | ||||
.hg/ directory. | ||||
|
r41171 | We store the data on disk in cbor, for which we use the CBOR format to encode | ||
the data. | ||||
|
r38115 | """ | ||
from __future__ import absolute_import | ||||
|
r45731 | import contextlib | ||
|
r42729 | from .i18n import _ | ||
|
r38115 | from . import ( | ||
|
r38118 | error, | ||
|
r44212 | pycompat, | ||
|
r38115 | util, | ||
) | ||||
|
r43346 | from .utils import cborutil | ||
|
r44212 | if pycompat.TYPE_CHECKING: | ||
|
r44101 | from typing import ( | ||
Any, | ||||
Dict, | ||||
) | ||||
for t in (Any, Dict): | ||||
assert t | ||||
|
r38115 | |||
class cmdstate(object): | ||||
"""a wrapper class to store the state of commands like `rebase`, `graft`, | ||||
`histedit`, `shelve` etc. Extensions can also use this to write state files. | ||||
All the data for the state is stored in the form of key-value pairs in a | ||||
dictionary. | ||||
The class object can write all the data to a file in .hg/ directory and | ||||
can populate the object data reading that file. | ||||
Uses cbor to serialize and deserialize data while writing and reading from | ||||
disk. | ||||
""" | ||||
|
r38162 | def __init__(self, repo, fname): | ||
|
r46554 | """repo is the repo object | ||
|
r38115 | fname is the file name in which data should be stored in .hg directory | ||
""" | ||||
self._repo = repo | ||||
self.fname = fname | ||||
|
r38116 | def read(self): | ||
|
r44101 | # type: () -> Dict[bytes, Any] | ||
|
r38116 | """read the existing state file and return a dict of data stored""" | ||
return self._read() | ||||
|
r38115 | |||
|
r38118 | def save(self, version, data): | ||
|
r38115 | """write all the state data stored to .hg/<filename> file | ||
we use third-party library cbor to serialize data to write in the file. | ||||
""" | ||||
|
r38118 | if not isinstance(version, int): | ||
|
r43346 | raise error.ProgrammingError( | ||
|
r43387 | b"version of state file should be an integer" | ||
|
r43346 | ) | ||
|
r38118 | |||
|
r43347 | with self._repo.vfs(self.fname, b'wb', atomictemp=True) as fp: | ||
fp.write(b'%d\n' % version) | ||||
|
r39482 | for chunk in cborutil.streamencode(data): | ||
fp.write(chunk) | ||||
|
r38115 | |||
def _read(self): | ||||
"""reads the state file and returns a dictionary which contain | ||||
data in the same format as it was before storing""" | ||||
|
r43347 | with self._repo.vfs(self.fname, b'rb') as fp: | ||
|
r38118 | try: | ||
|
r38141 | int(fp.readline()) | ||
|
r38118 | except ValueError: | ||
|
r43346 | raise error.CorruptedState( | ||
|
r43387 | b"unknown version of state file found" | ||
|
r43346 | ) | ||
|
r39482 | |||
return cborutil.decodeall(fp.read())[0] | ||||
|
r38115 | |||
def delete(self): | ||||
"""drop the state file if exists""" | ||||
util.unlinkpath(self._repo.vfs.join(self.fname), ignoremissing=True) | ||||
def exists(self): | ||||
"""check whether the state file exists or not""" | ||||
return self._repo.vfs.exists(self.fname) | ||||
|
r42729 | |||
|
r43346 | |||
|
r42730 | class _statecheck(object): | ||
"""a utility class that deals with multistep operations like graft, | ||||
|
r46554 | histedit, bisect, update etc and check whether such commands | ||
are in an unfinished conditition or not and return appropriate message | ||||
and hint. | ||||
It also has the ability to register and determine the states of any new | ||||
multistep operation or multistep command extension. | ||||
|
r42730 | """ | ||
|
r43346 | def __init__( | ||
self, | ||||
opname, | ||||
fname, | ||||
clearable, | ||||
allowcommit, | ||||
reportonly, | ||||
continueflag, | ||||
stopflag, | ||||
|
r45731 | childopnames, | ||
|
r43346 | cmdmsg, | ||
cmdhint, | ||||
statushint, | ||||
abortfunc, | ||||
continuefunc, | ||||
): | ||||
|
r42730 | self._opname = opname | ||
self._fname = fname | ||||
self._clearable = clearable | ||||
self._allowcommit = allowcommit | ||||
|
r42734 | self._reportonly = reportonly | ||
self._continueflag = continueflag | ||||
self._stopflag = stopflag | ||||
|
r45731 | self._childopnames = childopnames | ||
self._delegating = False | ||||
|
r42734 | self._cmdmsg = cmdmsg | ||
|
r42730 | self._cmdhint = cmdhint | ||
|
r42732 | self._statushint = statushint | ||
|
r42784 | self.abortfunc = abortfunc | ||
|
r42831 | self.continuefunc = continuefunc | ||
|
r42732 | |||
def statusmsg(self): | ||||
"""returns the hint message corresponding to the command for | ||||
hg status --verbose | ||||
""" | ||||
if not self._statushint: | ||||
|
r43346 | hint = _( | ||
|
r43347 | b'To continue: hg %s --continue\n' | ||
b'To abort: hg %s --abort' | ||||
|
r43346 | ) % (self._opname, self._opname) | ||
|
r42732 | if self._stopflag: | ||
|
r43346 | hint = hint + ( | ||
|
r43347 | _(b'\nTo stop: hg %s --stop') % (self._opname) | ||
|
r43346 | ) | ||
|
r42732 | return hint | ||
return self._statushint | ||||
|
r42730 | |||
def hint(self): | ||||
|
r42732 | """returns the hint message corresponding to an interrupted | ||
operation | ||||
""" | ||||
|
r42730 | if not self._cmdhint: | ||
|
r45774 | if not self._stopflag: | ||
return _(b"use 'hg %s --continue' or 'hg %s --abort'") % ( | ||||
self._opname, | ||||
self._opname, | ||||
) | ||||
else: | ||||
|
r45804 | return _( | ||
b"use 'hg %s --continue', 'hg %s --abort', " | ||||
b"or 'hg %s --stop'" | ||||
|
r46554 | ) % ( | ||
self._opname, | ||||
self._opname, | ||||
self._opname, | ||||
) | ||||
|
r45774 | |||
|
r42730 | return self._cmdhint | ||
def msg(self): | ||||
"""returns the status message corresponding to the command""" | ||||
if not self._cmdmsg: | ||||
|
r43347 | return _(b'%s in progress') % (self._opname) | ||
|
r42730 | return self._cmdmsg | ||
|
r42733 | def continuemsg(self): | ||
|
r47856 | """returns appropriate continue message corresponding to command""" | ||
|
r43347 | return _(b'hg %s --continue') % (self._opname) | ||
|
r42733 | |||
|
r42730 | def isunfinished(self, repo): | ||
|
r42732 | """determines whether a multi-step operation is in progress | ||
or not | ||||
""" | ||||
|
r43347 | if self._opname == b'merge': | ||
|
r42732 | return len(repo[None].parents()) > 1 | ||
|
r45731 | elif self._delegating: | ||
return False | ||||
|
r42732 | else: | ||
return repo.vfs.exists(self._fname) | ||||
|
r42730 | |||
|
r43346 | |||
|
r42730 | # A list of statecheck objects for multistep operations like graft. | ||
_unfinishedstates = [] | ||||
|
r45731 | _unfinishedstatesbyname = {} | ||
|
r42730 | |||
|
r43346 | |||
def addunfinished( | ||||
opname, | ||||
fname, | ||||
clearable=False, | ||||
allowcommit=False, | ||||
reportonly=False, | ||||
continueflag=False, | ||||
stopflag=False, | ||||
|
r45731 | childopnames=None, | ||
|
r43347 | cmdmsg=b"", | ||
cmdhint=b"", | ||||
statushint=b"", | ||||
|
r43346 | abortfunc=None, | ||
continuefunc=None, | ||||
): | ||||
|
r42730 | """this registers a new command or operation to unfinishedstates | ||
|
r42734 | opname is the name the command or operation | ||
fname is the file name in which data should be stored in .hg directory. | ||||
It is None for merge command. | ||||
clearable boolean determines whether or not interrupted states can be | ||||
cleared by running `hg update -C .` which in turn deletes the | ||||
state file. | ||||
allowcommit boolean decides whether commit is allowed during interrupted | ||||
state or not. | ||||
reportonly flag is used for operations like bisect where we just | ||||
need to detect the operation using 'hg status --verbose' | ||||
continueflag is a boolean determines whether or not a command supports | ||||
`--continue` option or not. | ||||
stopflag is a boolean that determines whether or not a command supports | ||||
--stop flag | ||||
|
r45731 | childopnames is a list of other opnames this op uses as sub-steps of its | ||
own execution. They must already be added. | ||||
|
r42734 | 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 | ||||
message of the format "To continue: hg cmdname --continue | ||||
To abort: hg cmdname --abort" is not desired. | ||||
statushint is used to pass a different status message in case standard | ||||
message of the format ('To continue: hg cmdname --continue' | ||||
'To abort: hg cmdname --abort') is not desired | ||||
|
r42784 | abortfunc stores the function required to abort an unfinished state. | ||
|
r42831 | continuefunc stores the function required to finish an interrupted | ||
operation. | ||||
|
r42730 | """ | ||
|
r45731 | childopnames = childopnames or [] | ||
|
r43346 | statecheckobj = _statecheck( | ||
opname, | ||||
fname, | ||||
clearable, | ||||
allowcommit, | ||||
reportonly, | ||||
continueflag, | ||||
stopflag, | ||||
|
r45731 | childopnames, | ||
|
r43346 | cmdmsg, | ||
cmdhint, | ||||
statushint, | ||||
abortfunc, | ||||
continuefunc, | ||||
) | ||||
|
r45731 | |||
|
r43347 | if opname == b'merge': | ||
|
r42732 | _unfinishedstates.append(statecheckobj) | ||
else: | ||||
|
r45731 | # 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) | ||||
) | ||||
|
r42732 | _unfinishedstates.insert(0, statecheckobj) | ||
|
r42730 | |||
|
r45731 | if opname in _unfinishedstatesbyname: | ||
raise error.ProgrammingError(_(b'op %s registered twice') % opname) | ||||
_unfinishedstatesbyname[opname] = statecheckobj | ||||
def _getparentandchild(opname, childopname): | ||||
p = _unfinishedstatesbyname.get(opname, None) | ||||
if not p: | ||||
raise error.ProgrammingError(_(b'unknown op %s') % opname) | ||||
if childopname not in p._childopnames: | ||||
raise error.ProgrammingError( | ||||
_(b'op %s does not delegate to %s') % (opname, childopname) | ||||
) | ||||
c = _unfinishedstatesbyname[childopname] | ||||
return p, c | ||||
@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(...) | ||||
... | ||||
""" | ||||
p, c = _getparentandchild(opname, childopname) | ||||
if p._delegating: | ||||
raise error.ProgrammingError( | ||||
_(b'cannot delegate from op %s recursively') % opname | ||||
) | ||||
p._delegating = True | ||||
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 | ||||
finally: | ||||
p._delegating = False | ||||
def ischildunfinished(repo, opname, childopname): | ||||
"""Returns true if both opname and childopname are unfinished.""" | ||||
p, c = _getparentandchild(opname, childopname) | ||||
return (p._delegating or p.isunfinished(repo)) and c.isunfinished(repo) | ||||
def continuechild(ui, repo, opname, childopname): | ||||
"""Checks that childopname is in progress, and continues it.""" | ||||
p, c = _getparentandchild(opname, childopname) | ||||
if not ischildunfinished(repo, opname, childopname): | ||||
raise error.ProgrammingError( | ||||
_(b'child op %s of parent %s is not unfinished') | ||||
% (childopname, opname) | ||||
) | ||||
if not c.continuefunc: | ||||
raise error.ProgrammingError( | ||||
_(b'op %s has no continue function') % childopname | ||||
) | ||||
return c.continuefunc(ui, repo) | ||||
|
r43346 | |||
|
r42730 | addunfinished( | ||
|
r43347 | b'update', | ||
fname=b'updatestate', | ||||
|
r43346 | clearable=True, | ||
|
r43347 | cmdmsg=_(b'last update was interrupted'), | ||
cmdhint=_(b"use 'hg update' to get a consistent checkout"), | ||||
statushint=_(b"To continue: hg update ."), | ||||
|
r42730 | ) | ||
|
r42732 | addunfinished( | ||
|
r43347 | b'bisect', | ||
fname=b'bisect.state', | ||||
|
r43346 | allowcommit=True, | ||
reportonly=True, | ||||
statushint=_( | ||||
|
r43347 | b'To mark the changeset good: hg bisect --good\n' | ||
b'To mark the changeset bad: hg bisect --bad\n' | ||||
b'To abort: hg bisect --reset\n' | ||||
|
r43346 | ), | ||
|
r42732 | ) | ||
|
r42731 | |||
|
r43346 | |||
|
r42731 | def getrepostate(repo): | ||
# experimental config: commands.status.skipstates | ||||
|
r43347 | skip = set(repo.ui.configlist(b'commands', b'status.skipstates')) | ||
|
r42732 | for state in _unfinishedstates: | ||
if state._opname in skip: | ||||
|
r42731 | continue | ||
|
r42732 | if state.isunfinished(repo): | ||
return (state._opname, state.statusmsg()) | ||||