typing: add a transaction protocol
This allow us to remove the "external" import from
mercurial/interfaces/dirstate.py, cutting one of the circular import route.
--- a/hgext/git/dirstate.py Fri Feb 07 16:40:49 2025 +0100
+++ b/hgext/git/dirstate.py Fri Feb 07 13:48:50 2025 +0100
@@ -13,7 +13,10 @@
Tuple,
)
-from mercurial.interfaces.types import MatcherT
+from mercurial.interfaces.types import (
+ MatcherT,
+ TransactionT,
+)
from mercurial.node import sha1nodeconstants
from mercurial import (
dirstatemap,
@@ -317,7 +320,7 @@
) -> None:
raise NotImplementedError
- def write(self, tr: Optional[intdirstate.TransactionT]) -> None:
+ def write(self, tr: Optional[TransactionT]) -> None:
# TODO: call parent change callbacks
if tr:
@@ -456,7 +459,7 @@
self._plchangecallbacks[category] = callback
def setbranch(
- self, branch: bytes, transaction: Optional[intdirstate.TransactionT]
+ self, branch: bytes, transaction: Optional[TransactionT]
) -> None:
raise error.Abort(
b'git repos do not support branches. try using bookmarks'
--- a/mercurial/dirstate.py Fri Feb 07 16:40:49 2025 +0100
+++ b/mercurial/dirstate.py Fri Feb 07 13:48:50 2025 +0100
@@ -24,7 +24,10 @@
)
from .i18n import _
-from .interfaces.types import MatcherT
+from .interfaces.types import (
+ MatcherT,
+ TransactionT,
+)
from hgdemandimport import tracing
@@ -669,7 +672,7 @@
return self._map.setparents(p1, p2, fold_p2=fold_p2)
def setbranch(
- self, branch: bytes, transaction: Optional[intdirstate.TransactionT]
+ self, branch: bytes, transaction: Optional[TransactionT]
) -> None:
self.__class__._branch.set(self, encoding.fromlocal(branch))
if transaction is not None:
@@ -1102,7 +1105,7 @@
on_abort,
)
- def write(self, tr: Optional[intdirstate.TransactionT]) -> None:
+ def write(self, tr: Optional[TransactionT]) -> None:
if not self._dirty:
return
# make sure we don't request a write of invalidated content
--- a/mercurial/interfaces/dirstate.py Fri Feb 07 16:40:49 2025 +0100
+++ b/mercurial/interfaces/dirstate.py Fri Feb 07 13:48:50 2025 +0100
@@ -20,13 +20,10 @@
if typing.TYPE_CHECKING:
# Almost all mercurial modules are only imported in the type checking phase
# to avoid circular imports
- from .. import (
- transaction as txnmod,
- )
-
from . import (
matcher,
status as istatus,
+ transaction,
)
# TODO: finish adding type hints
@@ -55,8 +52,7 @@
StatusReturnT = Tuple[Any, istatus.Status, Any]
"""The return type of dirstate.status()."""
- # TODO: probably doesn't belong here.
- TransactionT = txnmod.transaction
+ TransactionT = transaction.ITransaction
"""The type for a transaction used with dirstate.
This is meant to help callers avoid having to remember to delay the import
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/interfaces/transaction.py Fri Feb 07 13:48:50 2025 +0100
@@ -0,0 +1,253 @@
+# transaction.py - simple journaling scheme for mercurial
+#
+# This transaction scheme is intended to gracefully handle program
+# errors and interruptions. More serious failures like system crashes
+# can be recovered with an fsck-like tool. As the whole repository is
+# effectively log-structured, this should amount to simply truncating
+# anything that isn't referenced in the changelog.
+#
+# Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import annotations
+
+import abc
+
+from typing import (
+ Callable,
+ Collection,
+ List,
+ Optional,
+ Protocol,
+ Tuple,
+ Union,
+)
+
+from ._basetypes import (
+ CallbackCategoryT,
+ HgPathT,
+ VfsKeyT,
+)
+
+JournalEntryT = Tuple[HgPathT, int]
+
+
+class ITransaction(Protocol):
+ @property
+ @abc.abstractmethod
+ def finalized(self) -> bool:
+ ...
+
+ @abc.abstractmethod
+ def startgroup(self) -> None:
+ """delay registration of file entry
+
+ This is used by strip to delay vision of strip offset. The transaction
+ sees either none or all of the strip actions to be done."""
+
+ @abc.abstractmethod
+ def endgroup(self) -> None:
+ """apply delayed registration of file entry.
+
+ This is used by strip to delay vision of strip offset. The transaction
+ sees either none or all of the strip actions to be done."""
+
+ @abc.abstractmethod
+ def add(self, file: HgPathT, offset: int) -> None:
+ """record the state of an append-only file before update"""
+
+ @abc.abstractmethod
+ def addbackup(
+ self,
+ file: HgPathT,
+ hardlink: bool = True,
+ location: VfsKeyT = b'',
+ for_offset: Union[bool, int] = False,
+ ) -> None:
+ """Adds a backup of the file to the transaction
+
+ Calling addbackup() creates a hardlink backup of the specified file
+ that is used to recover the file in the event of the transaction
+ aborting.
+
+ * `file`: the file path, relative to .hg/store
+ * `hardlink`: use a hardlink to quickly create the backup
+
+ If `for_offset` is set, we expect a offset for this file to have been
+ previously recorded
+ """
+
+ @abc.abstractmethod
+ def registertmp(self, tmpfile: HgPathT, location: VfsKeyT = b'') -> None:
+ """register a temporary transaction file
+
+ Such files will be deleted when the transaction exits (on both
+ failure and success).
+ """
+
+ @abc.abstractmethod
+ def addfilegenerator(
+ self,
+ genid: bytes,
+ filenames: Collection[HgPathT],
+ genfunc: Callable,
+ order: int = 0,
+ location: VfsKeyT = b'',
+ post_finalize: bool = False,
+ ) -> None:
+ """add a function to generates some files at transaction commit
+
+ The `genfunc` argument is a function capable of generating proper
+ content of each entry in the `filename` tuple.
+
+ At transaction close time, `genfunc` will be called with one file
+ object argument per entries in `filenames`.
+
+ The transaction itself is responsible for the backup, creation and
+ final write of such file.
+
+ The `genid` argument is used to ensure the same set of file is only
+ generated once. Call to `addfilegenerator` for a `genid` already
+ present will overwrite the old entry.
+
+ The `order` argument may be used to control the order in which multiple
+ generator will be executed.
+
+ The `location` arguments may be used to indicate the files are located
+ outside of the the standard directory for transaction. It should match
+ one of the key of the `transaction.vfsmap` dictionary.
+
+ The `post_finalize` argument can be set to `True` for file generation
+ that must be run after the transaction has been finalized.
+ """
+
+ @abc.abstractmethod
+ def removefilegenerator(self, genid: bytes) -> None:
+ """reverse of addfilegenerator, remove a file generator function"""
+
+ @abc.abstractmethod
+ def findoffset(self, file: HgPathT) -> Optional[int]:
+ ...
+
+ @abc.abstractmethod
+ def readjournal(self) -> List[JournalEntryT]:
+ ...
+
+ @abc.abstractmethod
+ def replace(self, file: HgPathT, offset: int) -> None:
+ """
+ replace can only replace already committed entries
+ that are not pending in the queue
+ """
+
+ @abc.abstractmethod
+ def nest(self, name: bytes = b'<unnamed>') -> ITransaction:
+ ...
+
+ @abc.abstractmethod
+ def release(self) -> None:
+ ...
+
+ @abc.abstractmethod
+ def running(self) -> bool:
+ ...
+
+ @abc.abstractmethod
+ def addpending(
+ self,
+ category: CallbackCategoryT,
+ callback: Callable[[ITransaction], None],
+ ) -> None:
+ """add a callback to be called when the transaction is pending
+
+ The transaction will be given as callback's first argument.
+
+ Category is a unique identifier to allow overwriting an old callback
+ with a newer callback.
+ """
+
+ @abc.abstractmethod
+ def writepending(self) -> bool:
+ """write pending file to temporary version
+
+ This is used to allow hooks to view a transaction before commit"""
+
+ @abc.abstractmethod
+ def hasfinalize(self, category: CallbackCategoryT) -> bool:
+ """check is a callback already exist for a category"""
+
+ @abc.abstractmethod
+ def addfinalize(
+ self,
+ category: CallbackCategoryT,
+ callback: Callable[[ITransaction], None],
+ ) -> None:
+ """add a callback to be called when the transaction is closed
+
+ The transaction will be given as callback's first argument.
+
+ Category is a unique identifier to allow overwriting old callbacks with
+ newer callbacks.
+ """
+
+ @abc.abstractmethod
+ def addpostclose(
+ self,
+ category: CallbackCategoryT,
+ callback: Callable[[ITransaction], None],
+ ) -> None:
+ """add or replace a callback to be called after the transaction closed
+
+ The transaction will be given as callback's first argument.
+
+ Category is a unique identifier to allow overwriting an old callback
+ with a newer callback.
+ """
+
+ @abc.abstractmethod
+ def getpostclose(
+ self, category: CallbackCategoryT
+ ) -> Optional[Callable[[ITransaction], None]]:
+ """return a postclose callback added before, or None"""
+
+ @abc.abstractmethod
+ def addabort(
+ self,
+ category: CallbackCategoryT,
+ callback: Callable[[ITransaction], None],
+ ) -> None:
+ """add a callback to be called when the transaction is aborted.
+
+ The transaction will be given as the first argument to the callback.
+
+ Category is a unique identifier to allow overwriting an old callback
+ with a newer callback.
+ """
+
+ @abc.abstractmethod
+ def addvalidator(
+ self,
+ category: CallbackCategoryT,
+ callback: Callable[[ITransaction], None],
+ ) -> None:
+ """adds a callback to be called when validating the transaction.
+
+ The transaction will be given as the first argument to the callback.
+
+ callback should raise exception if to abort transaction"""
+
+ @abc.abstractmethod
+ def close(self) -> None:
+ '''commit the transaction'''
+
+ @abc.abstractmethod
+ def abort(self) -> None:
+ """abort the transaction (generally called on error, or when the
+ transaction is not explicitly committed before going out of
+ scope)"""
+
+ @abc.abstractmethod
+ def add_journal(self, vfs_id: VfsKeyT, path: HgPathT) -> None:
+ ...
--- a/mercurial/interfaces/types.py Fri Feb 07 16:40:49 2025 +0100
+++ b/mercurial/interfaces/types.py Fri Feb 07 13:48:50 2025 +0100
@@ -21,6 +21,8 @@
from . import (
matcher,
+ transaction,
)
MatcherT = matcher.IMatcher
+TransactionT = transaction.ITransaction
--- a/mercurial/transaction.py Fri Feb 07 16:40:49 2025 +0100
+++ b/mercurial/transaction.py Fri Feb 07 13:48:50 2025 +0100
@@ -21,7 +21,6 @@
Collection,
List,
Optional,
- Tuple,
Union,
)
@@ -29,6 +28,7 @@
from .interfaces.types import (
CallbackCategoryT,
HgPathT,
+ TransactionT,
VfsKeyT,
)
from . import (
@@ -38,6 +38,7 @@
util,
)
from .utils import stringutil
+from .interfaces import transaction as itxn
version = 2
@@ -45,8 +46,6 @@
GEN_GROUP_PRE_FINALIZE = b'prefinalize'
GEN_GROUP_POST_FINALIZE = b'postfinalize'
-JournalEntryT = Tuple[HgPathT, int]
-
def active(func):
def _active(self, *args, **kwds):
@@ -240,7 +239,7 @@
pass
-class transaction(util.transactional):
+class transaction(util.transactional, itxn.ITransaction):
def __init__(
self,
report,
@@ -573,7 +572,7 @@
return self._offsetmap.get(file)
@active
- def readjournal(self) -> List[JournalEntryT]:
+ def readjournal(self) -> List[itxn.JournalEntryT]:
self._file.seek(0)
entries = []
for l in self._file.readlines():
@@ -604,7 +603,7 @@
self._file.flush()
@active
- def nest(self, name: bytes = b'<unnamed>') -> transaction:
+ def nest(self, name: bytes = b'<unnamed>') -> TransactionT:
self._count += 1
self._usages += 1
self._names.append(name)
@@ -625,7 +624,7 @@
def addpending(
self,
category: CallbackCategoryT,
- callback: Callable[[transaction], None],
+ callback: Callable[[TransactionT], None],
) -> None:
"""add a callback to be called when the transaction is pending
@@ -658,7 +657,7 @@
def addfinalize(
self,
category: CallbackCategoryT,
- callback: Callable[[transaction], None],
+ callback: Callable[[TransactionT], None],
) -> None:
"""add a callback to be called when the transaction is closed
@@ -673,7 +672,7 @@
def addpostclose(
self,
category: CallbackCategoryT,
- callback: Callable[[transaction], None],
+ callback: Callable[[TransactionT], None],
) -> None:
"""add or replace a callback to be called after the transaction closed
@@ -688,7 +687,7 @@
def getpostclose(
self,
category: CallbackCategoryT,
- ) -> Optional[Callable[[transaction], None]]:
+ ) -> Optional[Callable[[TransactionT], None]]:
"""return a postclose callback added before, or None"""
return self._postclosecallback.get(category, None)
@@ -696,7 +695,7 @@
def addabort(
self,
category: CallbackCategoryT,
- callback: Callable[[transaction], None],
+ callback: Callable[[TransactionT], None],
) -> None:
"""add a callback to be called when the transaction is aborted.
@@ -711,7 +710,7 @@
def addvalidator(
self,
category: CallbackCategoryT,
- callback: Callable[[transaction], None],
+ callback: Callable[[TransactionT], None],
) -> None:
"""adds a callback to be called when validating the transaction.