| 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) |
| )) |
| } |
| } |