blob: 80cb4dfe0f444f619770ed3eded2980759afadd5 [file] [log] [blame] [edit]
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)
))
}
}