Skip to content

Commit 09743e2

Browse files
committed
Add an implementation of reference transactions
refs #1419
1 parent ff64e61 commit 09743e2

File tree

10 files changed

+692
-0
lines changed

10 files changed

+692
-0
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Table of Contents
7474
oid
7575
packing
7676
references
77+
transactions
7778
remotes
7879
repository
7980
revparse

docs/references.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ Example::
8888
.. autoclass:: pygit2.RefLogEntry
8989
:members:
9090

91+
Reference Transactions
92+
=======================
93+
94+
For atomic updates of multiple references, use transactions. See the
95+
:doc:`transactions` documentation for details.
96+
97+
Example::
98+
99+
# Update multiple refs atomically
100+
with repo.transaction() as txn:
101+
txn.lock_ref('refs/heads/master')
102+
txn.lock_ref('refs/heads/develop')
103+
txn.set_target('refs/heads/master', new_oid, message='Release')
104+
txn.set_target('refs/heads/develop', dev_oid, message='Continue dev')
105+
91106
Notes
92107
====================
93108

docs/transactions.rst

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
**********************************************************************
2+
Reference Transactions
3+
**********************************************************************
4+
5+
Reference transactions allow you to update multiple references atomically.
6+
All reference updates within a transaction either succeed together or fail
7+
together, ensuring repository consistency.
8+
9+
Basic Usage
10+
===========
11+
12+
Use the :meth:`Repository.transaction` method as a context manager. The
13+
transaction commits automatically when the context exits successfully, or
14+
rolls back if an exception is raised::
15+
16+
with repo.transaction() as txn:
17+
txn.lock_ref('refs/heads/master')
18+
txn.set_target('refs/heads/master', new_oid, message='Update master')
19+
20+
Atomic Multi-Reference Updates
21+
===============================
22+
23+
Transactions are useful when you need to update multiple references
24+
atomically::
25+
26+
# Swap two branches atomically
27+
with repo.transaction() as txn:
28+
txn.lock_ref('refs/heads/branch-a')
29+
txn.lock_ref('refs/heads/branch-b')
30+
31+
# Get current targets
32+
ref_a = repo.lookup_reference('refs/heads/branch-a')
33+
ref_b = repo.lookup_reference('refs/heads/branch-b')
34+
35+
# Swap them
36+
txn.set_target('refs/heads/branch-a', ref_b.target, message='Swap')
37+
txn.set_target('refs/heads/branch-b', ref_a.target, message='Swap')
38+
39+
Automatic Rollback
40+
==================
41+
42+
If an exception occurs during the transaction, changes are automatically
43+
rolled back::
44+
45+
try:
46+
with repo.transaction() as txn:
47+
txn.lock_ref('refs/heads/master')
48+
txn.set_target('refs/heads/master', new_oid)
49+
50+
# If this raises an exception, the ref update is rolled back
51+
validate_commit(new_oid)
52+
except ValidationError:
53+
# Master still points to its original target
54+
pass
55+
56+
Manual Commit
57+
=============
58+
59+
While the context manager is recommended, you can manually manage
60+
transactions::
61+
62+
from pygit2 import ReferenceTransaction
63+
64+
txn = ReferenceTransaction(repo)
65+
try:
66+
txn.lock_ref('refs/heads/master')
67+
txn.set_target('refs/heads/master', new_oid, message='Update')
68+
txn.commit()
69+
finally:
70+
del txn # Ensure transaction is freed
71+
72+
API Reference
73+
=============
74+
75+
Repository Methods
76+
------------------
77+
78+
.. automethod:: pygit2.Repository.transaction
79+
80+
The ReferenceTransaction Type
81+
------------------------------
82+
83+
.. autoclass:: pygit2.ReferenceTransaction
84+
:members:
85+
:special-members: __enter__, __exit__
86+
87+
Usage Notes
88+
===========
89+
90+
- Always lock a reference with :meth:`~ReferenceTransaction.lock_ref` before
91+
modifying it
92+
- Transactions operate on reference names, not Reference objects
93+
- Symbolic references can be updated with
94+
:meth:`~ReferenceTransaction.set_symbolic_target`
95+
- References can be deleted with :meth:`~ReferenceTransaction.remove`
96+
- The signature parameter defaults to the repository's configured identity
97+
98+
Thread Safety
99+
=============
100+
101+
Transactions are thread-local and must be used from the thread that created
102+
them. Attempting to use a transaction from a different thread raises
103+
:exc:`RuntimeError`::
104+
105+
# This is safe - each thread has its own transaction
106+
def thread1():
107+
with repo.transaction() as txn:
108+
txn.lock_ref('refs/heads/branch1')
109+
txn.set_target('refs/heads/branch1', oid1)
110+
111+
def thread2():
112+
with repo.transaction() as txn:
113+
txn.lock_ref('refs/heads/branch2')
114+
txn.set_target('refs/heads/branch2', oid2)
115+
116+
# Both threads can run concurrently without conflicts
117+
118+
Different threads can hold transactions simultaneously as long as they don't
119+
attempt to lock the same references. If two threads try to acquire locks in
120+
different orders, libgit2 will detect potential deadlocks and raise an error.

pygit2/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@
366366
from .repository import Repository
367367
from .settings import Settings
368368
from .submodules import Submodule
369+
from .transaction import ReferenceTransaction
369370
from .utils import to_bytes, to_str
370371

371372
# Features
@@ -971,6 +972,8 @@ def clone_repository(
971972
'Settings',
972973
'submodules',
973974
'Submodule',
975+
'transaction',
976+
'ReferenceTransaction',
974977
'utils',
975978
'to_bytes',
976979
'to_str',

pygit2/_run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
'revert.h',
8282
'stash.h',
8383
'submodule.h',
84+
'transaction.h',
8485
'options.h',
8586
'callbacks.h', # Bridge from libgit2 to Python
8687
]

pygit2/decl/transaction.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
int git_transaction_new(git_transaction **out, git_repository *repo);
2+
int git_transaction_lock_ref(git_transaction *tx, const char *refname);
3+
int git_transaction_set_target(git_transaction *tx, const char *refname, const git_oid *target, const git_signature *sig, const char *msg);
4+
int git_transaction_set_symbolic_target(git_transaction *tx, const char *refname, const char *target, const git_signature *sig, const char *msg);
5+
int git_transaction_set_reflog(git_transaction *tx, const char *refname, const git_reflog *reflog);
6+
int git_transaction_remove(git_transaction *tx, const char *refname);
7+
int git_transaction_commit(git_transaction *tx);
8+
void git_transaction_free(git_transaction *tx);

pygit2/decl/types.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ typedef struct git_submodule git_submodule;
1212
typedef struct git_transport git_transport;
1313
typedef struct git_tree git_tree;
1414
typedef struct git_packbuilder git_packbuilder;
15+
typedef struct git_transaction git_transaction;
16+
typedef struct git_reflog git_reflog;
1517

1618
typedef int64_t git_off_t;
1719
typedef int64_t git_time_t;

pygit2/repository.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
from .references import References
7979
from .remotes import RemoteCollection
8080
from .submodules import SubmoduleCollection
81+
from .transaction import ReferenceTransaction
8182
from .utils import StrArray, to_bytes
8283

8384
if TYPE_CHECKING:
@@ -120,6 +121,7 @@ def _common_init(self) -> None:
120121
self.references = References(self)
121122
self.remotes = RemoteCollection(self)
122123
self.submodules = SubmoduleCollection(self)
124+
self._active_transaction = None
123125

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

360362
return (commit, reference) # type: ignore
361363

364+
def transaction(self) -> ReferenceTransaction:
365+
"""Create a new reference transaction.
366+
367+
Returns a context manager that commits all reference updates atomically
368+
when the context exits successfully, or performs no updates if an exception
369+
is raised.
370+
371+
Example::
372+
373+
with repo.transaction() as txn:
374+
txn.lock_ref('refs/heads/master')
375+
txn.set_target('refs/heads/master', new_oid, message='Update')
376+
"""
377+
txn = ReferenceTransaction(self)
378+
return txn
379+
362380
#
363381
# Checkout
364382
#

0 commit comments

Comments
 (0)