diff options
Diffstat (limited to 'extra/git2/src/transaction.rs')
-rw-r--r-- | extra/git2/src/transaction.rs | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/extra/git2/src/transaction.rs b/extra/git2/src/transaction.rs new file mode 100644 index 000000000..4f661f1d4 --- /dev/null +++ b/extra/git2/src/transaction.rs @@ -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 committing 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 atomicity is **not* guaranteed: if the transaction fails to + /// modify `refname`, the reflog may still have been committed to disk. + /// + /// If this is combined with setting the target, that update won't be + /// written to the log (i.e. 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) + )) + } +} |