Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Table of Contents
oid
packing
references
transactions
remotes
repository
revparse
Expand Down
15 changes: 15 additions & 0 deletions docs/references.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ Example::
.. autoclass:: pygit2.RefLogEntry
:members:

Reference Transactions
=======================

For atomic updates of multiple references, use transactions. See the
:doc:`transactions` documentation for details.

Example::

# Update multiple refs atomically
with repo.transaction() as txn:
txn.lock_ref('refs/heads/master')
txn.lock_ref('refs/heads/develop')
txn.set_target('refs/heads/master', new_oid, message='Release')
txn.set_target('refs/heads/develop', dev_oid, message='Continue dev')

Notes
====================

Expand Down
120 changes: 120 additions & 0 deletions docs/transactions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
**********************************************************************
Reference Transactions
**********************************************************************

Reference transactions allow you to update multiple references atomically.
All reference updates within a transaction either succeed together or fail
together, ensuring repository consistency.

Basic Usage
===========

Use the :meth:`Repository.transaction` method as a context manager. The
transaction commits automatically when the context exits successfully, or
rolls back if an exception is raised::

with repo.transaction() as txn:
txn.lock_ref('refs/heads/master')
txn.set_target('refs/heads/master', new_oid, message='Update master')

Atomic Multi-Reference Updates
===============================

Transactions are useful when you need to update multiple references
atomically::

# Swap two branches atomically
with repo.transaction() as txn:
txn.lock_ref('refs/heads/branch-a')
txn.lock_ref('refs/heads/branch-b')

# Get current targets
ref_a = repo.lookup_reference('refs/heads/branch-a')
ref_b = repo.lookup_reference('refs/heads/branch-b')

# Swap them
txn.set_target('refs/heads/branch-a', ref_b.target, message='Swap')
txn.set_target('refs/heads/branch-b', ref_a.target, message='Swap')

Automatic Rollback
==================

If an exception occurs during the transaction, changes are automatically
rolled back::

try:
with repo.transaction() as txn:
txn.lock_ref('refs/heads/master')
txn.set_target('refs/heads/master', new_oid)

# If this raises an exception, the ref update is rolled back
validate_commit(new_oid)
except ValidationError:
# Master still points to its original target
pass

Manual Commit
=============

While the context manager is recommended, you can manually manage
transactions::

from pygit2 import ReferenceTransaction

txn = ReferenceTransaction(repo)
try:
txn.lock_ref('refs/heads/master')
txn.set_target('refs/heads/master', new_oid, message='Update')
txn.commit()
finally:
del txn # Ensure transaction is freed

API Reference
=============

Repository Methods
------------------

.. automethod:: pygit2.Repository.transaction

The ReferenceTransaction Type
------------------------------

.. autoclass:: pygit2.ReferenceTransaction
:members:
:special-members: __enter__, __exit__

Usage Notes
===========

- Always lock a reference with :meth:`~ReferenceTransaction.lock_ref` before
modifying it
- Transactions operate on reference names, not Reference objects
- Symbolic references can be updated with
:meth:`~ReferenceTransaction.set_symbolic_target`
- References can be deleted with :meth:`~ReferenceTransaction.remove`
- The signature parameter defaults to the repository's configured identity

Thread Safety
=============

Transactions are thread-local and must be used from the thread that created
them. Attempting to use a transaction from a different thread raises
:exc:`RuntimeError`::

# This is safe - each thread has its own transaction
def thread1():
with repo.transaction() as txn:
txn.lock_ref('refs/heads/branch1')
txn.set_target('refs/heads/branch1', oid1)

def thread2():
with repo.transaction() as txn:
txn.lock_ref('refs/heads/branch2')
txn.set_target('refs/heads/branch2', oid2)

# Both threads can run concurrently without conflicts

Different threads can hold transactions simultaneously as long as they don't
attempt to lock the same references. If two threads try to acquire locks in
different orders, libgit2 will detect potential deadlocks and raise an error.
3 changes: 3 additions & 0 deletions pygit2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@
from .repository import Repository
from .settings import Settings
from .submodules import Submodule
from .transaction import ReferenceTransaction
from .utils import to_bytes, to_str

# Features
Expand Down Expand Up @@ -971,6 +972,8 @@ def clone_repository(
'Settings',
'submodules',
'Submodule',
'transaction',
'ReferenceTransaction',
'utils',
'to_bytes',
'to_str',
Expand Down
5 changes: 5 additions & 0 deletions pygit2/_libgit2/ffi.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,17 @@ class GitRemoteC:
class GitReferenceC:
pass

class GitTransactionC:
pass

def string(a: char_pointer) -> bytes: ...
@overload
def new(a: Literal['git_repository **']) -> _Pointer[GitRepositoryC]: ...
@overload
def new(a: Literal['git_remote **']) -> _Pointer[GitRemoteC]: ...
@overload
def new(a: Literal['git_transaction **']) -> _Pointer[GitTransactionC]: ...
@overload
def new(a: Literal['git_repository_init_options *']) -> GitRepositoryInitOptionsC: ...
@overload
def new(a: Literal['git_submodule_update_options *']) -> GitSubmoduleUpdateOptionsC: ...
Expand Down
1 change: 1 addition & 0 deletions pygit2/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
'revert.h',
'stash.h',
'submodule.h',
'transaction.h',
'options.h',
'callbacks.h', # Bridge from libgit2 to Python
]
Expand Down
8 changes: 8 additions & 0 deletions pygit2/decl/transaction.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
int git_transaction_new(git_transaction **out, git_repository *repo);
int git_transaction_lock_ref(git_transaction *tx, const char *refname);
int git_transaction_set_target(git_transaction *tx, const char *refname, const git_oid *target, const git_signature *sig, const char *msg);
int git_transaction_set_symbolic_target(git_transaction *tx, const char *refname, const char *target, const git_signature *sig, const char *msg);
int git_transaction_set_reflog(git_transaction *tx, const char *refname, const git_reflog *reflog);
int git_transaction_remove(git_transaction *tx, const char *refname);
int git_transaction_commit(git_transaction *tx);
void git_transaction_free(git_transaction *tx);
2 changes: 2 additions & 0 deletions pygit2/decl/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ typedef struct git_submodule git_submodule;
typedef struct git_transport git_transport;
typedef struct git_tree git_tree;
typedef struct git_packbuilder git_packbuilder;
typedef struct git_transaction git_transaction;
typedef struct git_reflog git_reflog;

typedef int64_t git_off_t;
typedef int64_t git_time_t;
Expand Down
18 changes: 18 additions & 0 deletions pygit2/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
from .references import References
from .remotes import RemoteCollection
from .submodules import SubmoduleCollection
from .transaction import ReferenceTransaction
from .utils import StrArray, to_bytes

if TYPE_CHECKING:
Expand Down Expand Up @@ -120,6 +121,7 @@ def _common_init(self) -> None:
self.references = References(self)
self.remotes = RemoteCollection(self)
self.submodules = SubmoduleCollection(self)
self._active_transaction = None

# Get the pointer as the contents of a buffer and store it for
# later access
Expand Down Expand Up @@ -359,6 +361,22 @@ def resolve_refish(self, refish: str) -> tuple[Commit, Reference]:

return (commit, reference) # type: ignore

def transaction(self) -> ReferenceTransaction:
"""Create a new reference transaction.

Returns a context manager that commits all reference updates atomically
when the context exits successfully, or performs no updates if an exception
is raised.

Example::

with repo.transaction() as txn:
txn.lock_ref('refs/heads/master')
txn.set_target('refs/heads/master', new_oid, message='Update')
"""
txn = ReferenceTransaction(self)
return txn

#
# Checkout
#
Expand Down
Loading
Loading