summaryrefslogtreecommitdiffstats
path: root/extra/git2/src/transaction.rs
diff options
context:
space:
mode:
Diffstat (limited to 'extra/git2/src/transaction.rs')
-rw-r--r--extra/git2/src/transaction.rs285
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)
+ ))
+ }
+}