Skip to content

Commit 8e24a2e

Browse files
authored
Add bindings for the transaction API (#686)
Defined in `transaction.h`, this is essentially a way to place file locks on a set of refs, and update them in one (non-atomic) operation. Signed-off-by: Kim Altintop <[email protected]>
1 parent 64b8ba8 commit 8e24a2e

File tree

4 files changed

+324
-1
lines changed

4 files changed

+324
-1
lines changed

libgit2-sys/lib.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ pub enum git_odb {}
8989
pub enum git_odb_stream {}
9090
pub enum git_odb_object {}
9191
pub enum git_worktree {}
92+
pub enum git_transaction {}
9293

9394
#[repr(C)]
9495
pub struct git_revspec {
@@ -3914,6 +3915,32 @@ extern "C" {
39143915
wt: *mut git_worktree,
39153916
opts: *mut git_worktree_prune_options,
39163917
) -> c_int;
3918+
3919+
// Ref transactions
3920+
pub fn git_transaction_new(out: *mut *mut git_transaction, repo: *mut git_repository) -> c_int;
3921+
pub fn git_transaction_lock_ref(tx: *mut git_transaction, refname: *const c_char) -> c_int;
3922+
pub fn git_transaction_set_target(
3923+
tx: *mut git_transaction,
3924+
refname: *const c_char,
3925+
target: *const git_oid,
3926+
sig: *const git_signature,
3927+
msg: *const c_char,
3928+
) -> c_int;
3929+
pub fn git_transaction_set_symbolic_target(
3930+
tx: *mut git_transaction,
3931+
refname: *const c_char,
3932+
target: *const c_char,
3933+
sig: *const git_signature,
3934+
msg: *const c_char,
3935+
) -> c_int;
3936+
pub fn git_transaction_set_reflog(
3937+
tx: *mut git_transaction,
3938+
refname: *const c_char,
3939+
reflog: *const git_reflog,
3940+
) -> c_int;
3941+
pub fn git_transaction_remove(tx: *mut git_transaction, refname: *const c_char) -> c_int;
3942+
pub fn git_transaction_commit(tx: *mut git_transaction) -> c_int;
3943+
pub fn git_transaction_free(tx: *mut git_transaction);
39173944
}
39183945

39193946
pub fn init() {

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ pub use crate::status::{StatusEntry, StatusIter, StatusOptions, StatusShow, Stat
129129
pub use crate::submodule::{Submodule, SubmoduleUpdateOptions};
130130
pub use crate::tag::Tag;
131131
pub use crate::time::{IndexTime, Time};
132+
pub use crate::transaction::Transaction;
132133
pub use crate::tree::{Tree, TreeEntry, TreeIter, TreeWalkMode, TreeWalkResult};
133134
pub use crate::treebuilder::TreeBuilder;
134135
pub use crate::util::IntoCString;
@@ -687,6 +688,7 @@ mod submodule;
687688
mod tag;
688689
mod tagforeach;
689690
mod time;
691+
mod transaction;
690692
mod tree;
691693
mod treebuilder;
692694
mod worktree;

src/repo.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use crate::{Blame, BlameOptions, Reference, References, ResetType, Signature, Su
3232
use crate::{Blob, BlobWriter, Branch, BranchType, Branches, Commit, Config, Index, Oid, Tree};
3333
use crate::{Describe, IntoCString, Reflog, RepositoryInitMode, RevparseMode};
3434
use crate::{DescribeOptions, Diff, DiffOptions, Odb, PackBuilder, TreeBuilder};
35-
use crate::{Note, Notes, ObjectType, Revwalk, Status, StatusOptions, Statuses, Tag};
35+
use crate::{Note, Notes, ObjectType, Revwalk, Status, StatusOptions, Statuses, Tag, Transaction};
3636

3737
/// An owned git repository, representing all state associated with the
3838
/// underlying filesystem.
@@ -2953,6 +2953,15 @@ impl Repository {
29532953
Ok(Binding::from_raw(raw))
29542954
}
29552955
}
2956+
2957+
/// Create a new transaction
2958+
pub fn transaction<'a>(&'a self) -> Result<Transaction<'a>, Error> {
2959+
let mut raw = ptr::null_mut();
2960+
unsafe {
2961+
try_call!(raw::git_transaction_new(&mut raw, self.raw));
2962+
Ok(Binding::from_raw(raw))
2963+
}
2964+
}
29562965
}
29572966

29582967
impl Binding for Repository {

src/transaction.rs

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
use std::ffi::CString;
2+
use std::marker;
3+
4+
use crate::{raw, util::Binding, Error, Oid, Reflog, Repository, Signature};
5+
6+
/// A structure representing a transactional update of a repository's references.
7+
///
8+
/// Transactions work by locking loose refs for as long as the [`Transaction`]
9+
/// is held, and committing all changes to disk when [`Transaction::commit`] is
10+
/// called. Note that comitting is not atomic: if an operation fails, the
11+
/// transaction aborts, but previous successful operations are not rolled back.
12+
pub struct Transaction<'repo> {
13+
raw: *mut raw::git_transaction,
14+
_marker: marker::PhantomData<&'repo Repository>,
15+
}
16+
17+
impl Drop for Transaction<'_> {
18+
fn drop(&mut self) {
19+
unsafe { raw::git_transaction_free(self.raw) }
20+
}
21+
}
22+
23+
impl<'repo> Binding for Transaction<'repo> {
24+
type Raw = *mut raw::git_transaction;
25+
26+
unsafe fn from_raw(ptr: *mut raw::git_transaction) -> Transaction<'repo> {
27+
Transaction {
28+
raw: ptr,
29+
_marker: marker::PhantomData,
30+
}
31+
}
32+
33+
fn raw(&self) -> *mut raw::git_transaction {
34+
self.raw
35+
}
36+
}
37+
38+
impl<'repo> Transaction<'repo> {
39+
/// Lock the specified reference by name.
40+
pub fn lock_ref(&mut self, refname: &str) -> Result<(), Error> {
41+
let refname = CString::new(refname).unwrap();
42+
unsafe {
43+
try_call!(raw::git_transaction_lock_ref(self.raw, refname));
44+
}
45+
46+
Ok(())
47+
}
48+
49+
/// Set the target of the specified reference.
50+
///
51+
/// The reference must have been locked via `lock_ref`.
52+
///
53+
/// If `reflog_signature` is `None`, the [`Signature`] is read from the
54+
/// repository config.
55+
pub fn set_target(
56+
&mut self,
57+
refname: &str,
58+
target: Oid,
59+
reflog_signature: Option<&Signature<'_>>,
60+
reflog_message: &str,
61+
) -> Result<(), Error> {
62+
let refname = CString::new(refname).unwrap();
63+
let reflog_message = CString::new(reflog_message).unwrap();
64+
unsafe {
65+
try_call!(raw::git_transaction_set_target(
66+
self.raw,
67+
refname,
68+
target.raw(),
69+
reflog_signature.map(|s| s.raw()),
70+
reflog_message
71+
));
72+
}
73+
74+
Ok(())
75+
}
76+
77+
/// Set the target of the specified symbolic reference.
78+
///
79+
/// The reference must have been locked via `lock_ref`.
80+
///
81+
/// If `reflog_signature` is `None`, the [`Signature`] is read from the
82+
/// repository config.
83+
pub fn set_symbolic_target(
84+
&mut self,
85+
refname: &str,
86+
target: &str,
87+
reflog_signature: Option<&Signature<'_>>,
88+
reflog_message: &str,
89+
) -> Result<(), Error> {
90+
let refname = CString::new(refname).unwrap();
91+
let target = CString::new(target).unwrap();
92+
let reflog_message = CString::new(reflog_message).unwrap();
93+
unsafe {
94+
try_call!(raw::git_transaction_set_symbolic_target(
95+
self.raw,
96+
refname,
97+
target,
98+
reflog_signature.map(|s| s.raw()),
99+
reflog_message
100+
));
101+
}
102+
103+
Ok(())
104+
}
105+
106+
/// Add a [`Reflog`] to the transaction.
107+
///
108+
/// This commit the in-memory [`Reflog`] to disk when the transaction commits.
109+
/// Note that atomicty is **not* guaranteed: if the transaction fails to
110+
/// modify `refname`, the reflog may still have been comitted to disk.
111+
///
112+
/// If this is combined with setting the target, that update won't be
113+
/// written to the log (ie. the `reflog_signature` and `reflog_message`
114+
/// parameters will be ignored).
115+
pub fn set_reflog(&mut self, refname: &str, reflog: Reflog) -> Result<(), Error> {
116+
let refname = CString::new(refname).unwrap();
117+
unsafe {
118+
try_call!(raw::git_transaction_set_reflog(
119+
self.raw,
120+
refname,
121+
reflog.raw()
122+
));
123+
}
124+
125+
Ok(())
126+
}
127+
128+
/// Remove a reference.
129+
///
130+
/// The reference must have been locked via `lock_ref`.
131+
pub fn remove(&mut self, refname: &str) -> Result<(), Error> {
132+
let refname = CString::new(refname).unwrap();
133+
unsafe {
134+
try_call!(raw::git_transaction_remove(self.raw, refname));
135+
}
136+
137+
Ok(())
138+
}
139+
140+
/// Commit the changes from the transaction.
141+
///
142+
/// The updates will be made one by one, and the first failure will stop the
143+
/// processing.
144+
pub fn commit(self) -> Result<(), Error> {
145+
unsafe {
146+
try_call!(raw::git_transaction_commit(self.raw));
147+
}
148+
Ok(())
149+
}
150+
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
use crate::{Error, ErrorClass, ErrorCode, Oid, Repository};
155+
156+
#[test]
157+
fn smoke() {
158+
let (_td, repo) = crate::test::repo_init();
159+
160+
let mut tx = t!(repo.transaction());
161+
162+
t!(tx.lock_ref("refs/heads/main"));
163+
t!(tx.lock_ref("refs/heads/next"));
164+
165+
t!(tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero"));
166+
t!(tx.set_symbolic_target(
167+
"refs/heads/next",
168+
"refs/heads/main",
169+
None,
170+
"set next to main",
171+
));
172+
173+
t!(tx.commit());
174+
175+
assert_eq!(repo.refname_to_id("refs/heads/main").unwrap(), Oid::zero());
176+
assert_eq!(
177+
repo.find_reference("refs/heads/next")
178+
.unwrap()
179+
.symbolic_target()
180+
.unwrap(),
181+
"refs/heads/main"
182+
);
183+
}
184+
185+
#[test]
186+
fn locks_same_repo_handle() {
187+
let (_td, repo) = crate::test::repo_init();
188+
189+
let mut tx1 = t!(repo.transaction());
190+
t!(tx1.lock_ref("refs/heads/seen"));
191+
192+
let mut tx2 = t!(repo.transaction());
193+
assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked))
194+
}
195+
196+
#[test]
197+
fn locks_across_repo_handles() {
198+
let (td, repo1) = crate::test::repo_init();
199+
let repo2 = t!(Repository::open(&td));
200+
201+
let mut tx1 = t!(repo1.transaction());
202+
t!(tx1.lock_ref("refs/heads/seen"));
203+
204+
let mut tx2 = t!(repo2.transaction());
205+
assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked))
206+
}
207+
208+
#[test]
209+
fn drop_unlocks() {
210+
let (_td, repo) = crate::test::repo_init();
211+
212+
let mut tx = t!(repo.transaction());
213+
t!(tx.lock_ref("refs/heads/seen"));
214+
drop(tx);
215+
216+
let mut tx2 = t!(repo.transaction());
217+
t!(tx2.lock_ref("refs/heads/seen"))
218+
}
219+
220+
#[test]
221+
fn commit_unlocks() {
222+
let (_td, repo) = crate::test::repo_init();
223+
224+
let mut tx = t!(repo.transaction());
225+
t!(tx.lock_ref("refs/heads/seen"));
226+
t!(tx.commit());
227+
228+
let mut tx2 = t!(repo.transaction());
229+
t!(tx2.lock_ref("refs/heads/seen"));
230+
}
231+
232+
#[test]
233+
fn prevents_non_transactional_updates() {
234+
let (_td, repo) = crate::test::repo_init();
235+
let head = t!(repo.refname_to_id("HEAD"));
236+
237+
let mut tx = t!(repo.transaction());
238+
t!(tx.lock_ref("refs/heads/seen"));
239+
240+
assert!(matches!(
241+
repo.reference("refs/heads/seen", head, true, "competing with lock"),
242+
Err(e) if e.code() == ErrorCode::Locked
243+
));
244+
}
245+
246+
#[test]
247+
fn remove() {
248+
let (_td, repo) = crate::test::repo_init();
249+
let head = t!(repo.refname_to_id("HEAD"));
250+
let next = "refs/heads/next";
251+
252+
t!(repo.reference(
253+
next,
254+
head,
255+
true,
256+
"refs/heads/next@{0}: branch: Created from HEAD"
257+
));
258+
259+
{
260+
let mut tx = t!(repo.transaction());
261+
t!(tx.lock_ref(next));
262+
t!(tx.remove(next));
263+
t!(tx.commit());
264+
}
265+
assert!(matches!(repo.refname_to_id(next), Err(e) if e.code() == ErrorCode::NotFound))
266+
}
267+
268+
#[test]
269+
fn must_lock_ref() {
270+
let (_td, repo) = crate::test::repo_init();
271+
272+
// 🤷
273+
fn is_not_locked_err(e: &Error) -> bool {
274+
e.code() == ErrorCode::NotFound
275+
&& e.class() == ErrorClass::Reference
276+
&& e.message() == "the specified reference is not locked"
277+
}
278+
279+
let mut tx = t!(repo.transaction());
280+
assert!(matches!(
281+
tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero"),
282+
Err(e) if is_not_locked_err(&e)
283+
))
284+
}
285+
}

0 commit comments

Comments
 (0)