-
Notifications
You must be signed in to change notification settings - Fork 400
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <kim.altintop@gmail.com>
- Loading branch information
Showing
4 changed files
with
324 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,285 @@ | ||
use std::ffi::CString; | ||
use std::marker; | ||
|
||
use crate::{raw, util::Binding, Error, Oid, Reflog, Repository, Signature}; | ||
|
||
/// A structure representing a transactional update of a repository's references. | ||
/// | ||
/// Transactions work by locking loose refs for as long as the [`Transaction`] | ||
/// is held, and committing all changes to disk when [`Transaction::commit`] is | ||
/// called. Note that comitting is not atomic: if an operation fails, the | ||
/// transaction aborts, but previous successful operations are not rolled back. | ||
pub struct Transaction<'repo> { | ||
raw: *mut raw::git_transaction, | ||
_marker: marker::PhantomData<&'repo Repository>, | ||
} | ||
|
||
impl Drop for Transaction<'_> { | ||
fn drop(&mut self) { | ||
unsafe { raw::git_transaction_free(self.raw) } | ||
} | ||
} | ||
|
||
impl<'repo> Binding for Transaction<'repo> { | ||
type Raw = *mut raw::git_transaction; | ||
|
||
unsafe fn from_raw(ptr: *mut raw::git_transaction) -> Transaction<'repo> { | ||
Transaction { | ||
raw: ptr, | ||
_marker: marker::PhantomData, | ||
} | ||
} | ||
|
||
fn raw(&self) -> *mut raw::git_transaction { | ||
self.raw | ||
} | ||
} | ||
|
||
impl<'repo> Transaction<'repo> { | ||
/// Lock the specified reference by name. | ||
pub fn lock_ref(&mut self, refname: &str) -> Result<(), Error> { | ||
let refname = CString::new(refname).unwrap(); | ||
unsafe { | ||
try_call!(raw::git_transaction_lock_ref(self.raw, refname)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Set the target of the specified reference. | ||
/// | ||
/// The reference must have been locked via `lock_ref`. | ||
/// | ||
/// If `reflog_signature` is `None`, the [`Signature`] is read from the | ||
/// repository config. | ||
pub fn set_target( | ||
&mut self, | ||
refname: &str, | ||
target: Oid, | ||
reflog_signature: Option<&Signature<'_>>, | ||
reflog_message: &str, | ||
) -> Result<(), Error> { | ||
let refname = CString::new(refname).unwrap(); | ||
let reflog_message = CString::new(reflog_message).unwrap(); | ||
unsafe { | ||
try_call!(raw::git_transaction_set_target( | ||
self.raw, | ||
refname, | ||
target.raw(), | ||
reflog_signature.map(|s| s.raw()), | ||
reflog_message | ||
)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Set the target of the specified symbolic reference. | ||
/// | ||
/// The reference must have been locked via `lock_ref`. | ||
/// | ||
/// If `reflog_signature` is `None`, the [`Signature`] is read from the | ||
/// repository config. | ||
pub fn set_symbolic_target( | ||
&mut self, | ||
refname: &str, | ||
target: &str, | ||
reflog_signature: Option<&Signature<'_>>, | ||
reflog_message: &str, | ||
) -> Result<(), Error> { | ||
let refname = CString::new(refname).unwrap(); | ||
let target = CString::new(target).unwrap(); | ||
let reflog_message = CString::new(reflog_message).unwrap(); | ||
unsafe { | ||
try_call!(raw::git_transaction_set_symbolic_target( | ||
self.raw, | ||
refname, | ||
target, | ||
reflog_signature.map(|s| s.raw()), | ||
reflog_message | ||
)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Add a [`Reflog`] to the transaction. | ||
/// | ||
/// This commit the in-memory [`Reflog`] to disk when the transaction commits. | ||
/// Note that atomicty is **not* guaranteed: if the transaction fails to | ||
/// modify `refname`, the reflog may still have been comitted to disk. | ||
/// | ||
/// If this is combined with setting the target, that update won't be | ||
/// written to the log (ie. the `reflog_signature` and `reflog_message` | ||
/// parameters will be ignored). | ||
pub fn set_reflog(&mut self, refname: &str, reflog: Reflog) -> Result<(), Error> { | ||
let refname = CString::new(refname).unwrap(); | ||
unsafe { | ||
try_call!(raw::git_transaction_set_reflog( | ||
self.raw, | ||
refname, | ||
reflog.raw() | ||
)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Remove a reference. | ||
/// | ||
/// The reference must have been locked via `lock_ref`. | ||
pub fn remove(&mut self, refname: &str) -> Result<(), Error> { | ||
let refname = CString::new(refname).unwrap(); | ||
unsafe { | ||
try_call!(raw::git_transaction_remove(self.raw, refname)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Commit the changes from the transaction. | ||
/// | ||
/// The updates will be made one by one, and the first failure will stop the | ||
/// processing. | ||
pub fn commit(self) -> Result<(), Error> { | ||
unsafe { | ||
try_call!(raw::git_transaction_commit(self.raw)); | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::{Error, ErrorClass, ErrorCode, Oid, Repository}; | ||
|
||
#[test] | ||
fn smoke() { | ||
let (_td, repo) = crate::test::repo_init(); | ||
|
||
let mut tx = t!(repo.transaction()); | ||
|
||
t!(tx.lock_ref("refs/heads/main")); | ||
t!(tx.lock_ref("refs/heads/next")); | ||
|
||
t!(tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero")); | ||
t!(tx.set_symbolic_target( | ||
"refs/heads/next", | ||
"refs/heads/main", | ||
None, | ||
"set next to main", | ||
)); | ||
|
||
t!(tx.commit()); | ||
|
||
assert_eq!(repo.refname_to_id("refs/heads/main").unwrap(), Oid::zero()); | ||
assert_eq!( | ||
repo.find_reference("refs/heads/next") | ||
.unwrap() | ||
.symbolic_target() | ||
.unwrap(), | ||
"refs/heads/main" | ||
); | ||
} | ||
|
||
#[test] | ||
fn locks_same_repo_handle() { | ||
let (_td, repo) = crate::test::repo_init(); | ||
|
||
let mut tx1 = t!(repo.transaction()); | ||
t!(tx1.lock_ref("refs/heads/seen")); | ||
|
||
let mut tx2 = t!(repo.transaction()); | ||
assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked)) | ||
} | ||
|
||
#[test] | ||
fn locks_across_repo_handles() { | ||
let (td, repo1) = crate::test::repo_init(); | ||
let repo2 = t!(Repository::open(&td)); | ||
|
||
let mut tx1 = t!(repo1.transaction()); | ||
t!(tx1.lock_ref("refs/heads/seen")); | ||
|
||
let mut tx2 = t!(repo2.transaction()); | ||
assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked)) | ||
} | ||
|
||
#[test] | ||
fn drop_unlocks() { | ||
let (_td, repo) = crate::test::repo_init(); | ||
|
||
let mut tx = t!(repo.transaction()); | ||
t!(tx.lock_ref("refs/heads/seen")); | ||
drop(tx); | ||
|
||
let mut tx2 = t!(repo.transaction()); | ||
t!(tx2.lock_ref("refs/heads/seen")) | ||
} | ||
|
||
#[test] | ||
fn commit_unlocks() { | ||
let (_td, repo) = crate::test::repo_init(); | ||
|
||
let mut tx = t!(repo.transaction()); | ||
t!(tx.lock_ref("refs/heads/seen")); | ||
t!(tx.commit()); | ||
|
||
let mut tx2 = t!(repo.transaction()); | ||
t!(tx2.lock_ref("refs/heads/seen")); | ||
} | ||
|
||
#[test] | ||
fn prevents_non_transactional_updates() { | ||
let (_td, repo) = crate::test::repo_init(); | ||
let head = t!(repo.refname_to_id("HEAD")); | ||
|
||
let mut tx = t!(repo.transaction()); | ||
t!(tx.lock_ref("refs/heads/seen")); | ||
|
||
assert!(matches!( | ||
repo.reference("refs/heads/seen", head, true, "competing with lock"), | ||
Err(e) if e.code() == ErrorCode::Locked | ||
)); | ||
} | ||
|
||
#[test] | ||
fn remove() { | ||
let (_td, repo) = crate::test::repo_init(); | ||
let head = t!(repo.refname_to_id("HEAD")); | ||
let next = "refs/heads/next"; | ||
|
||
t!(repo.reference( | ||
next, | ||
head, | ||
true, | ||
"refs/heads/next@{0}: branch: Created from HEAD" | ||
)); | ||
|
||
{ | ||
let mut tx = t!(repo.transaction()); | ||
t!(tx.lock_ref(next)); | ||
t!(tx.remove(next)); | ||
t!(tx.commit()); | ||
} | ||
assert!(matches!(repo.refname_to_id(next), Err(e) if e.code() == ErrorCode::NotFound)) | ||
} | ||
|
||
#[test] | ||
fn must_lock_ref() { | ||
let (_td, repo) = crate::test::repo_init(); | ||
|
||
// 🤷 | ||
fn is_not_locked_err(e: &Error) -> bool { | ||
e.code() == ErrorCode::NotFound | ||
&& e.class() == ErrorClass::Reference | ||
&& e.message() == "the specified reference is not locked" | ||
} | ||
|
||
let mut tx = t!(repo.transaction()); | ||
assert!(matches!( | ||
tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero"), | ||
Err(e) if is_not_locked_err(&e) | ||
)) | ||
} | ||
} |