Skip to content

Commit

Permalink
Add bindings for the transaction API (#686)
Browse files Browse the repository at this point in the history
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
kim authored Mar 29, 2021
1 parent 64b8ba8 commit 8e24a2e
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 1 deletion.
27 changes: 27 additions & 0 deletions libgit2-sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub enum git_odb {}
pub enum git_odb_stream {}
pub enum git_odb_object {}
pub enum git_worktree {}
pub enum git_transaction {}

#[repr(C)]
pub struct git_revspec {
Expand Down Expand Up @@ -3914,6 +3915,32 @@ extern "C" {
wt: *mut git_worktree,
opts: *mut git_worktree_prune_options,
) -> c_int;

// Ref transactions
pub fn git_transaction_new(out: *mut *mut git_transaction, repo: *mut git_repository) -> c_int;
pub fn git_transaction_lock_ref(tx: *mut git_transaction, refname: *const c_char) -> c_int;
pub fn git_transaction_set_target(
tx: *mut git_transaction,
refname: *const c_char,
target: *const git_oid,
sig: *const git_signature,
msg: *const c_char,
) -> c_int;
pub fn git_transaction_set_symbolic_target(
tx: *mut git_transaction,
refname: *const c_char,
target: *const c_char,
sig: *const git_signature,
msg: *const c_char,
) -> c_int;
pub fn git_transaction_set_reflog(
tx: *mut git_transaction,
refname: *const c_char,
reflog: *const git_reflog,
) -> c_int;
pub fn git_transaction_remove(tx: *mut git_transaction, refname: *const c_char) -> c_int;
pub fn git_transaction_commit(tx: *mut git_transaction) -> c_int;
pub fn git_transaction_free(tx: *mut git_transaction);
}

pub fn init() {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub use crate::status::{StatusEntry, StatusIter, StatusOptions, StatusShow, Stat
pub use crate::submodule::{Submodule, SubmoduleUpdateOptions};
pub use crate::tag::Tag;
pub use crate::time::{IndexTime, Time};
pub use crate::transaction::Transaction;
pub use crate::tree::{Tree, TreeEntry, TreeIter, TreeWalkMode, TreeWalkResult};
pub use crate::treebuilder::TreeBuilder;
pub use crate::util::IntoCString;
Expand Down Expand Up @@ -687,6 +688,7 @@ mod submodule;
mod tag;
mod tagforeach;
mod time;
mod transaction;
mod tree;
mod treebuilder;
mod worktree;
Expand Down
11 changes: 10 additions & 1 deletion src/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::{Blame, BlameOptions, Reference, References, ResetType, Signature, Su
use crate::{Blob, BlobWriter, Branch, BranchType, Branches, Commit, Config, Index, Oid, Tree};
use crate::{Describe, IntoCString, Reflog, RepositoryInitMode, RevparseMode};
use crate::{DescribeOptions, Diff, DiffOptions, Odb, PackBuilder, TreeBuilder};
use crate::{Note, Notes, ObjectType, Revwalk, Status, StatusOptions, Statuses, Tag};
use crate::{Note, Notes, ObjectType, Revwalk, Status, StatusOptions, Statuses, Tag, Transaction};

/// An owned git repository, representing all state associated with the
/// underlying filesystem.
Expand Down Expand Up @@ -2953,6 +2953,15 @@ impl Repository {
Ok(Binding::from_raw(raw))
}
}

/// Create a new transaction
pub fn transaction<'a>(&'a self) -> Result<Transaction<'a>, Error> {
let mut raw = ptr::null_mut();
unsafe {
try_call!(raw::git_transaction_new(&mut raw, self.raw));
Ok(Binding::from_raw(raw))
}
}
}

impl Binding for Repository {
Expand Down
285 changes: 285 additions & 0 deletions src/transaction.rs
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)
))
}
}

0 comments on commit 8e24a2e

Please sign in to comment.