Files
radicle-git/git-storage/src/backend/write.rs
T
Fintan Halpenny f4a958010c fmt: remove .rustfmt.toml
The formatting configuration is not worth the maintenance of a nightly
version. Remove it and allow the default formatting to be used
instead.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
2024-03-13 14:11:36 +00:00

586 lines
18 KiB
Rust

// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::path::Path;
use either::Either;
use git_ext::{
error::is_not_found_err,
ref_format::{self, Qualified, RefStr, RefString},
Oid,
};
use crate::{
odb,
refdb::{
self, resolve,
write::{previous, Applied, Policy, SymrefTarget, Update, Updated},
Read as _, Reference, Target,
},
signature::UserInfo,
};
use super::read::{self, Read};
pub mod error;
/// A read-write storage backend for accessing git's odb and refdb.
///
/// For read-only access to the odb see [`odb::Read`].
/// For write access to the odb see [`odb::Write`].
///
/// For read-only access to the refdb see [`refdb::Read`].
/// For write access to the refdb see [`refdb::Write`].
///
/// To construct the `Write` storage use [`Read::open`].
#[derive(Debug)]
pub struct Write {
inner: Read,
info: UserInfo,
}
impl Write {
/// Open the [`Write`] storage, initialising it if it doesn't exist.
///
/// # Concurrency
///
/// [`Write`] can be sent between threads, but it can't be shared between
/// threads. _Some_ operations are safe to perform concurrently in much
/// the same way two `git` processes can access the same repository.
pub fn open<P: AsRef<Path>>(path: P, info: UserInfo) -> Result<Self, error::Init> {
crate::init();
let path = path.as_ref();
let raw = match git2::Repository::open_bare(path) {
Err(e) if is_not_found_err(&e) => {
let backend = git2::Repository::init_opts(
path,
git2::RepositoryInitOptions::new()
.bare(true)
.no_reinit(true)
.external_template(false),
)?;
Ok(backend)
}
Ok(repo) => Ok(repo),
Err(e) => Err(e),
}?;
Ok(Self {
inner: Read { raw },
info,
})
}
}
impl Write {
/// Return a read-only handle of the storage.
pub fn read_only(&self) -> &Read {
&self.inner
}
/// Return the [`UserInfo`] of the storage.
pub fn info(&self) -> &UserInfo {
&self.info
}
fn as_raw(&self) -> &git2::Repository {
&self.inner.raw
}
}
// refdb impls
impl<'a> refdb::Read for &'a Write {
type FindRef = <&'a Read as refdb::Read>::FindRef;
type FindRefs = <&'a Read as refdb::Read>::FindRefs;
type FindRefOid = <&'a Read as refdb::Read>::FindRefOid;
type References = <&'a Read as refdb::Read>::References;
fn find_reference<Ref>(&self, reference: Ref) -> Result<Option<Reference>, Self::FindRef>
where
Ref: AsRef<ref_format::RefStr>,
{
self.read_only().find_reference(reference)
}
fn find_references<Pat>(&self, reference: Pat) -> Result<Self::References, Self::FindRefs>
where
Pat: AsRef<ref_format::refspec::PatternStr>,
{
self.read_only().find_references(reference)
}
fn find_reference_oid<Ref>(&self, reference: Ref) -> Result<Option<Oid>, Self::FindRefOid>
where
Ref: AsRef<ref_format::RefStr>,
{
self.read_only().find_reference_oid(reference)
}
}
impl<'a> refdb::Write for &'a Write {
type UpdateError = error::Update;
fn update<'b, U>(&mut self, updates: U) -> Result<refdb::write::Applied<'b>, Self::UpdateError>
where
U: IntoIterator<Item = Update<'b>>,
{
let mut refdb = Transaction::new(self)?;
let mut applied = Applied::default();
for up in updates.into_iter() {
match up {
Update::Direct {
name,
target,
no_ff,
previous,
reflog,
} => match refdb.direct(name, target, no_ff, previous, reflog)? {
Either::Left(update) => applied.rejected.push(update),
Either::Right(updated) => applied.updated.push(updated),
},
Update::Symbolic {
name,
target,
type_change,
previous,
reflog,
} => match refdb.symbolic(name, target, type_change, previous, reflog)? {
Either::Left(update) => applied.rejected.push(update),
Either::Right(updated) => applied.updated.extend(updated),
},
Update::Remove { name, previous } => match refdb.remove(name, previous)? {
Either::Left(update) => applied.rejected.push(update),
Either::Right(updated) => applied.updated.push(updated),
},
}
}
refdb.commit()?;
Ok(applied)
}
}
/// An internal struct combining a [`Write`] and [`git2::Transaction`].
// TODO: include optional namespace
struct Transaction<'a> {
refdb: &'a Write,
txn: git2::Transaction<'a>,
}
impl<'a> Transaction<'a> {
pub fn new(refdb: &'a Write) -> Result<Self, git2::Error> {
let txn = refdb.inner.raw.transaction()?;
Ok(Self { refdb, txn })
}
/// Perform an [`Update::Direct`] within the [`Transaction`].
///
/// Steps:
/// 1. Get the state of the reference
/// a. if it did not exist create the source and destination references,
/// skip the next steps.
/// b. if it did, then follow the next steps.
///
/// 2. Guard against the [`previous::Edit`] value, if this fails then reject
/// the [`Update`].
///
/// 3. Check the fast-forward policy, either aborting,
/// rejecting, or accepting the update
pub fn direct<'b>(
&mut self,
name: Qualified<'b>,
target: Oid,
no_ff: Policy,
previous: previous::Edit,
reflog: String,
) -> Result<Either<Update<'b>, Updated>, error::Update> {
let prev = self.refdb.find_reference(&name)?;
let given = prev
.as_ref()
.map(|prev| resolve(self.refdb.as_raw(), prev))
.transpose()?;
if let Err(_err) = previous.guard(given) {
return Ok(Either::Left(Update::Direct {
name,
target,
no_ff,
previous,
reflog,
}));
}
let not_ff = match given {
Some(prev) => {
if !self.is_ff(&name, target, prev)? {
Some(prev)
} else {
None
}
}
None => None,
};
match not_ff {
// It wasn't an fast-forward so we check our policy
Some(cur) => match no_ff {
Policy::Abort => Err(error::Update::NonFF {
name: name.into(),
new: target,
cur,
}),
Policy::Reject => Ok(Either::Left(Update::Direct {
name,
target,
no_ff,
previous,
reflog,
})),
Policy::Allow => Ok(Either::Right(
self.direct_edit(&name, target, given, &reflog)?,
)),
},
// It was an fast-forward so we go ahead and make the edit
None => Ok(Either::Right(
self.direct_edit(&name, target, given, &reflog)?,
)),
}
}
/// Perform an [`Update::Symbolic`] within the [`Transaction`].
///
/// Steps:
/// 1. Get the state of the source reference
/// a. if it did not exist create the source and destination references,
/// skip the next steps.
/// b. if it did, then follow the next steps.
///
/// 2. Guard against the `type_change`, aborting or rejecting depending on
/// the [`Policy`].
///
/// 3. Get the state of the destination reference.
///
/// 4. Check the target of the desitination:
/// a. if it's direct then make the edit depending on the fast-forward
/// status.
/// b. if it's symbolic then this is an error.
pub fn symbolic<'b: 'a>(
&mut self,
name: Qualified<'b>,
target: SymrefTarget<'b>,
type_change: Policy,
previous: previous::Edit,
reflog: String,
) -> Result<Either<Update<'b>, Vec<Updated>>, error::Update> {
let src = self.refdb.find_reference(&name)?;
let prev = src
.as_ref()
.map(|src| refdb::resolve(self.refdb.as_raw(), src))
.transpose()?;
match src {
Some(src) => match src.target {
Target::Direct { .. } if matches!(type_change, Policy::Abort) => {
Err(error::Update::TypeChange(name.into()))
}
Target::Direct { .. } if matches!(type_change, Policy::Reject) => {
Ok(Either::Left(Update::Symbolic {
name,
target,
type_change,
previous,
reflog,
}))
}
_ => {
let dst = self.refdb.find_reference(&target.name)?;
match dst {
Some(dst) => match dst.target {
Target::Direct { oid: dst } => {
let is_ff = target.target != dst
&& self.is_ff(&target.name, target.target, dst)?;
Ok(Either::Right(
self.symbolic_edit(name, target, prev, &reflog, is_ff)?,
))
}
Target::Symbolic { .. } => Err(error::Update::TargetSymbolic(dst.name)),
},
None => Ok(Either::Right(
self.symbolic_edit(name, target, prev, &reflog, true)?,
)),
}
}
},
None => Ok(Either::Right(
self.symbolic_edit(name, target, prev, &reflog, true)?,
)),
}
}
/// Perform an [`Update::Remove`] within the [`Transaction`].
///
/// Steps:
/// 1. Get the state of the reference
///
/// 2. Guard against the [`previous::Remove`] value, if this fails then
/// reject the [`Update`].
///
/// 3. Remove the reference
///
/// # Panics
///
/// The `previous` SHOULD guard against the reference not existing, so this
/// will panic if the previous reference was missing AND passed the
/// `previous::guard`.
pub fn remove<'b>(
&mut self,
name: Qualified<'b>,
previous: previous::Remove,
) -> Result<Either<Update<'b>, Updated>, error::Update> {
let prev = self.refdb.find_reference(&name)?;
let given = prev
.as_ref()
.map(|prev| resolve(self.refdb.as_raw(), prev))
.transpose()?;
if let Err(_err) = previous.guard(given) {
Ok(Either::Left(Update::Remove { name, previous }))
} else {
match given {
None => {
panic!("BUG: the previous value for a reference to be removed was not given, but its existence SHOULD be guarded")
}
Some(previous) => Ok(Either::Right(self.remove_edit(name, previous)?)),
}
}
}
pub fn lock<R>(&mut self, reference: R) -> Result<(), error::Transaction>
where
R: AsRef<RefStr>,
{
let reference = reference.as_ref();
self.txn
.lock_ref(reference.as_str())
.map_err(|err| error::Transaction::Lock {
reference: reference.to_owned(),
source: err,
})
}
pub fn commit(self) -> Result<(), error::Transaction> {
self.txn
.commit()
.map_err(|err| error::Transaction::Commit { source: err })
}
pub fn direct_edit<R>(
&mut self,
reference: R,
target: Oid,
prev: Option<Oid>,
reflog: &str,
) -> Result<Updated, error::Transaction>
where
R: AsRef<RefStr>,
{
let reference = reference.as_ref();
self.lock(reference)?;
let info = self.refdb.info();
let sig = info
.signature()
.map_err(|err| error::Transaction::Signature {
name: info.name.to_owned(),
email: info.email.to_owned(),
source: err,
})?;
self.txn
.set_target(reference.as_str(), target.into(), Some(&sig), reflog)
.map_err(|err| error::Transaction::SetDirect {
reference: reference.to_owned(),
target,
source: err,
})?;
Ok(Updated::Direct {
name: reference.to_owned(),
target,
previous: prev,
})
}
pub fn symbolic_edit<R>(
&mut self,
reference: R,
target: SymrefTarget<'a>,
prev: Option<Oid>,
reflog: &str,
is_ff: bool,
) -> Result<Vec<Updated>, error::Transaction>
where
R: AsRef<RefStr>,
{
let reference = reference.as_ref();
self.lock(reference)?;
self.lock(&target.name)?;
let SymrefTarget {
name: dst,
target: dst_target,
} = target;
let mut edits = Vec::with_capacity(2);
let info = self.refdb.info();
let sig = info
.signature()
.map_err(|err| error::Transaction::Signature {
name: info.name.to_owned(),
email: info.email.to_owned(),
source: err,
})?;
if is_ff {
let direct = self.direct_edit(&dst, dst_target, prev, reflog)?;
edits.push(direct);
}
self.txn
.set_symbolic_target(reference.as_str(), dst.as_str(), Some(&sig), reflog)
.map_err(|err| error::Transaction::SetSymbolic {
reference: reference.to_owned(),
target: dst.clone().into(),
source: err,
})?;
edits.push(Updated::Symbolic {
name: reference.to_owned(),
target: dst.into(),
previous: prev,
});
Ok(edits)
}
pub fn remove_edit<R>(
&mut self,
reference: R,
previous: Oid,
) -> Result<Updated, error::Transaction>
where
R: AsRef<RefStr>,
{
let reference = reference.as_ref();
self.lock(reference)?;
self.txn
.remove(reference.as_str())
.map_err(|err| error::Transaction::Remove {
reference: reference.to_owned(),
source: err,
})?;
Ok(Updated::Removed {
name: reference.to_owned(),
previous,
})
}
fn is_ff<R>(&self, name: R, target: Oid, prev: Oid) -> Result<bool, error::Transaction>
where
R: AsRef<RefStr>,
{
self.refdb
.inner
.raw
.graph_descendant_of(target.into(), prev.into())
.map_err(|err| error::Transaction::Ancestry {
name: name.as_ref().to_owned(),
new: target,
old: prev,
source: err,
})
}
}
// odb impls
impl odb::Read for Write {
type FindObj = <Read as odb::Read>::FindObj;
type FindBlob = <Read as odb::Read>::FindBlob;
type FindCommit = <Read as odb::Read>::FindCommit;
type FindTag = <Read as odb::Read>::FindTag;
type FindTree = <Read as odb::Read>::FindTree;
fn find_object(&self, oid: Oid) -> Result<Option<crate::Object>, Self::FindObj> {
self.read_only().find_object(oid)
}
fn find_blob(&self, oid: Oid) -> Result<Option<git2::Blob>, Self::FindBlob> {
self.read_only().find_blob(oid)
}
fn find_commit(&self, oid: Oid) -> Result<Option<git2::Commit>, Self::FindCommit> {
self.read_only().find_commit(oid)
}
fn find_tag(&self, oid: Oid) -> Result<Option<git2::Tag>, Self::FindTag> {
self.read_only().find_tag(oid)
}
fn find_tree(&self, oid: Oid) -> Result<Option<git2::Tree>, Self::FindTree> {
self.read_only().find_tree(oid)
}
}
impl odb::Write for Write {
type WriteBlob = git2::Error;
type WriteCommit = git2::Error;
type WriteTag = git2::Error;
type WriteTree = git2::Error;
fn write_blob(&self, data: &[u8]) -> Result<Oid, Self::WriteBlob> {
self.as_raw().blob(data).map(Oid::from)
}
fn write_commit(
&self,
tree: &odb::Tree,
parents: &[&odb::Commit<'_>],
message: &str,
) -> Result<Oid, Self::WriteCommit> {
let author = self.info.signature()?;
self.as_raw()
.commit(None, &author, &author, message, tree, parents)
.map(Oid::from)
}
fn write_tag<R>(
&self,
name: R,
target: &odb::Object,
message: &str,
) -> Result<Oid, Self::WriteTag>
where
R: AsRef<RefStr>,
{
let tagger = self.info.signature()?;
self.as_raw()
.tag_annotation_create(name.as_ref().as_str(), target, &tagger, message)
.map(Oid::from)
}
fn write_tree(&self, builder: odb::TreeBuilder) -> Result<Oid, Self::WriteTree> {
let repo = self.as_raw();
let mut tree = repo.treebuilder(None)?;
for entry in builder.iter() {
match entry {
odb::TreeEntry::Insert {
name,
oid,
filemode,
} => tree.insert(name, (*oid).into(), *filemode).map(|_| ())?,
odb::TreeEntry::Remove { name } => tree.remove(name)?,
}
}
tree.write().map(Oid::from)
}
}