Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'origin/storage-port'
Fintan Halpenny committed 3 years ago
commit d7b3da88a2ed2d59c170f02453bfd802f378d363
parent 69e815c
18 files changed +1897 -0
modified Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
members = [
  "git-ref-format",
+
  "git-storage",
  "git-trailers",
  "link-git",
  "radicle-git-ext",
added git-storage/Cargo.toml
@@ -0,0 +1,38 @@
+
[package]
+
name = "git-storage"
+
version = "0.1.0"
+
authors = ["Kim Altintop <kim@eagain.st>", "Fintan Halpenny <fintan.halpenny@gmail.com>"]
+
edition = "2021"
+
license = "GPL-3.0-or-later"
+

+
[dependencies]
+
async-trait = "0.1"
+
globset = "0.4"
+
libc = "0.2"
+
parking_lot = "0.12"
+
thiserror = "1"
+
either = "1.8.0"
+

+
[dependencies.deadpool]
+
version = "0.7"
+
default-features = false
+
features = ["managed"]
+

+
[dependencies.git2]
+
version = "0.15.0"
+
default-features = false
+
features = ["vendored-libgit2"]
+

+
[dependencies.git-ref-format]
+
path = "../git-ref-format"
+

+
[dependencies.libgit2-sys]
+
version = ">= 0.12.24"
+
default-features = false
+
features = ["vendored"]
+

+
[dependencies.radicle-git-ext]
+
path = "../radicle-git-ext"
+

+
[dependencies.radicle-std-ext]
+
path = "../radicle-std-ext"
added git-storage/src/backend.rs
@@ -0,0 +1,5 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
pub mod read;
+
pub mod write;
added git-storage/src/backend/read.rs
@@ -0,0 +1,160 @@
+
// 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 git_ext::{error::is_not_found_err, Oid};
+
use std_ext::result::ResultExt as _;
+

+
use crate::{
+
    glob,
+
    odb::{self, Blob, Commit, Object, Tag, Tree},
+
    refdb::{self, Reference, References, ReferencesGlob},
+
};
+

+
/// A read-only storage backend for accessing git's odb and refdb.
+
///
+
/// For access to the odb see [`odb::Read`].
+
/// For access to the refdb see [`refdb::Read`].
+
///
+
/// To construct the `Read` storage use [`Read::open`].
+
pub struct Read {
+
    pub(super) raw: git2::Repository,
+
}
+

+
impl Read {
+
    /// Open the [`Read`] storage using the `path` provided.
+
    ///
+
    /// The storage **must** exist already. To initialise the storage for the
+
    /// first time see [`crate::Write::open`].
+
    ///
+
    /// # Concurrency
+
    ///
+
    /// [`Read`] 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.
+
    /// However, if you need multiple [`Read`]s to be shared between
+
    /// threads, use a [`crate::Pool`] instead.
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, error::Init> {
+
        crate::init();
+

+
        let raw = git2::Repository::open(path)?;
+

+
        Ok(Self { raw })
+
    }
+
}
+

+
pub mod error {
+
    use thiserror::Error;
+

+
    use crate::refdb::error::ParseReference;
+

+
    #[derive(Debug, Error)]
+
    pub enum Init {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
    }
+

+
    #[derive(Debug, Error)]
+
    pub enum Identifier {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
    }
+

+
    #[derive(Debug, Error)]
+
    pub enum FindRef {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error(transparent)]
+
        Parse(#[from] ParseReference),
+
    }
+
}
+

+
// impls
+

+
impl<'a> refdb::Read for &'a Read {
+
    type FindRef = error::FindRef;
+
    type FindRefs = refdb::error::Iter;
+
    type FindRefOid = git2::Error;
+

+
    type References = References<'a>;
+

+
    fn find_reference<Ref>(&self, reference: Ref) -> Result<Option<Reference>, Self::FindRef>
+
    where
+
        Ref: AsRef<git_ref_format::RefStr>,
+
    {
+
        let reference = self
+
            .raw
+
            .find_reference(reference.as_ref().as_str())
+
            .map(Some)
+
            .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))?;
+
        Ok(reference.map(Reference::try_from).transpose()?)
+
    }
+

+
    fn find_references<Pat>(&self, reference: Pat) -> Result<Self::References, Self::FindRefs>
+
    where
+
        Pat: AsRef<git_ref_format::refspec::PatternStr>,
+
    {
+
        Ok(References {
+
            inner: ReferencesGlob {
+
                iter: self.raw.references()?,
+
                glob: glob::RefspecMatcher::from(reference.as_ref().to_owned()),
+
            },
+
        })
+
    }
+

+
    fn find_reference_oid<Ref>(&self, reference: Ref) -> Result<Option<Oid>, Self::FindRefOid>
+
    where
+
        Ref: AsRef<git_ref_format::RefStr>,
+
    {
+
        self.raw
+
            .refname_to_id(reference.as_ref().as_str())
+
            .map(|oid| Some(Oid::from(oid)))
+
            .or_matches(is_not_found_err, || Ok(None))
+
    }
+
}
+

+
impl odb::Read for Read {
+
    type FindObj = git2::Error;
+
    type FindBlob = git2::Error;
+
    type FindCommit = git2::Error;
+
    type FindTag = git2::Error;
+
    type FindTree = git2::Error;
+

+
    fn find_object(&self, oid: Oid) -> Result<Option<Object>, Self::FindObj> {
+
        self.raw
+
            .find_object(oid.into(), None)
+
            .map(Some)
+
            .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))
+
    }
+

+
    fn find_blob(&self, oid: Oid) -> Result<Option<Blob>, Self::FindBlob> {
+
        self.raw
+
            .find_blob(oid.into())
+
            .map(Some)
+
            .or_matches(is_not_found_err, || Ok(None))
+
    }
+

+
    fn find_commit(&self, oid: Oid) -> Result<Option<Commit>, Self::FindCommit> {
+
        self.raw
+
            .find_commit(oid.into())
+
            .map(Some)
+
            .or_matches(is_not_found_err, || Ok(None))
+
    }
+

+
    fn find_tag(&self, oid: Oid) -> Result<Option<Tag>, Self::FindTag> {
+
        self.raw
+
            .find_tag(oid.into())
+
            .map(Some)
+
            .or_matches(is_not_found_err, || Ok(None))
+
    }
+

+
    fn find_tree(&self, oid: Oid) -> Result<Option<Tree>, Self::FindTree> {
+
        self.raw
+
            .find_tree(oid.into())
+
            .map(Some)
+
            .or_matches(is_not_found_err, || Ok(None))
+
    }
+
}
added git-storage/src/backend/write.rs
@@ -0,0 +1,574 @@
+
// 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, Oid};
+
use git_ref_format::{Qualified, RefStr, RefString};
+

+
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`].
+
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.
+
    /// However, if you need multiple [`Write`]s to be shared between
+
    /// threads, use a [`crate::Pool`] instead.
+
    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<git_ref_format::RefStr>,
+
    {
+
        self.read_only().find_reference(reference)
+
    }
+

+
    fn find_references<Pat>(&self, reference: Pat) -> Result<Self::References, Self::FindRefs>
+
    where
+
        Pat: AsRef<git_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<git_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<'b>(
+
        &self,
+
        tree: &odb::Tree,
+
        parents: &[&odb::Commit<'b>],
+
        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: git2::TreeBuilder) -> Result<Oid, Self::WriteTree> {
+
        builder.write().map(Oid::from)
+
    }
+
}
added git-storage/src/backend/write/error.rs
@@ -0,0 +1,80 @@
+
// 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 thiserror::Error;
+

+
use super::*;
+

+
#[derive(Debug, Error)]
+
pub enum Init {
+
    #[error(transparent)]
+
    Git(#[from] git2::Error),
+
}
+

+
#[derive(Debug, Error)]
+
pub enum Transaction {
+
    #[error("error determining if {old} is an ancestor of {new} in within {name}")]
+
    Ancestry {
+
        name: RefString,
+
        new: Oid,
+
        old: Oid,
+
        #[source]
+
        source: git2::Error,
+
    },
+
    #[error("error committing update for storage")]
+
    Commit {
+
        #[source]
+
        source: git2::Error,
+
    },
+
    #[error("error locking reference '{reference}' when attempting to update storage")]
+
    Lock {
+
        reference: RefString,
+
        #[source]
+
        source: git2::Error,
+
    },
+
    #[error("error obtaining signature for '{name}' '{email}'")]
+
    Signature {
+
        name: String,
+
        email: String,
+
        #[source]
+
        source: git2::Error,
+
    },
+
    #[error("error setting the direct reference '{reference}' to the target '{target}'")]
+
    SetDirect {
+
        reference: RefString,
+
        target: Oid,
+
        #[source]
+
        source: git2::Error,
+
    },
+
    #[error("error setting the symbolic reference '{reference}' to the target '{target}'")]
+
    SetSymbolic {
+
        reference: RefString,
+
        target: RefString,
+
        #[source]
+
        source: git2::Error,
+
    },
+
    #[error("error removing the reference '{reference}'")]
+
    Remove {
+
        reference: RefString,
+
        #[source]
+
        source: git2::Error,
+
    },
+
}
+

+
#[derive(Debug, Error)]
+
pub enum Update {
+
    #[error("non-fast-forward update of {name} (current: {cur}, new: {new})")]
+
    NonFF { name: RefString, new: Oid, cur: Oid },
+
    #[error(transparent)]
+
    FindRef(#[from] read::error::FindRef),
+
    #[error(transparent)]
+
    Git(#[from] git2::Error),
+
    #[error("the symref target {0} is itself a symref")]
+
    TargetSymbolic(RefString),
+
    #[error(transparent)]
+
    Transaction(#[from] Transaction),
+
    #[error("rejected changing type of reference '{0}' from symbolic to direct")]
+
    TypeChange(RefString),
+
}
added git-storage/src/glob.rs
@@ -0,0 +1,46 @@
+
// 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 git_ext as ext;
+
use git_ref_format::refspec::PatternString;
+

+
pub trait Pattern {
+
    fn matches<P: AsRef<Path>>(&self, path: P) -> bool;
+
}
+

+
impl Pattern for globset::GlobMatcher {
+
    fn matches<P: AsRef<Path>>(&self, path: P) -> bool {
+
        self.is_match(path)
+
    }
+
}
+

+
impl Pattern for globset::GlobSet {
+
    fn matches<P: AsRef<Path>>(&self, path: P) -> bool {
+
        self.is_match(path)
+
    }
+
}
+

+
#[derive(Clone, Debug)]
+
pub struct RefspecMatcher(globset::GlobMatcher);
+

+
impl From<ext::RefspecPattern> for RefspecMatcher {
+
    fn from(pat: ext::RefspecPattern) -> Self {
+
        Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher())
+
    }
+
}
+

+
impl From<PatternString> for RefspecMatcher {
+
    fn from(pat: PatternString) -> Self {
+
        Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher())
+
    }
+
}
+

+
impl Pattern for RefspecMatcher {
+
    fn matches<P: AsRef<Path>>(&self, path: P) -> bool {
+
        self.0.is_match(path)
+
    }
+
}
added git-storage/src/lib.rs
@@ -0,0 +1,85 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
//! # `git-storage`
+
//!
+
//! This crate provides access to git [references][refs] and [objects][objs].
+
//!
+
//! To first initialise the storage use [`Write::open`].
+
//!
+
//! After the storage is initialised, use [`Write::open`] or [`Read::open`] for
+
//! read-write or read-only access to the underlying storage. These structs will
+
//! implement the traits below depending on their access levels.
+
//!
+
//! ## Read-only access
+
//!
+
//! * [`refdb::Read`]
+
//! * [`odb::Read`]
+
//!
+
//! ## Read-write access
+
//!
+
//! * [`refdb::Read`]
+
//! * [`refdb::Write`]
+
//! * [`odb::Read`]
+
//! * [`odb::Write`]
+
//!
+
//! ## Concurrency
+
//!
+
//!
+
//! [`Read`] and [`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.
+
//! However, if you need multiple [`Read`]/[`Write`]s to be shared between
+
//! threads, use a [`Pool`] instead.
+
//!
+
//! [refs]: https://git-scm.com/book/en/v2/Git-Internals-Git-References
+
//! [objs]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
+

+
#[macro_use]
+
extern crate async_trait;
+

+
extern crate radicle_git_ext as git_ext;
+
extern crate radicle_std_ext as std_ext;
+

+
pub mod glob;
+

+
pub mod pool;
+
pub use pool::Pool;
+

+
pub mod refdb;
+
pub use refdb::{Applied, Reference, SymrefTarget, Target, Update, Updated};
+

+
pub mod odb;
+
pub use odb::{Blob, Commit, Object, Tag, Tree};
+

+
mod backend;
+
pub use backend::{
+
    read::{self, Read},
+
    write::{self, Write},
+
};
+

+
pub mod signature;
+

+
/// Initialise the git backend.
+
///
+
/// **SHOULD** be called before all accesses to git functionality.
+
pub fn init() {
+
    use libc::c_int;
+
    use libgit2_sys as raw_git;
+
    use std::sync::Once;
+

+
    static INIT: Once = Once::new();
+

+
    unsafe {
+
        INIT.call_once(|| {
+
            let ret =
+
                raw_git::git_libgit2_opts(raw_git::GIT_OPT_SET_MWINDOW_FILE_LIMIT as c_int, 256);
+
            if ret < 0 {
+
                panic!(
+
                    "error setting libgit2 option: {}",
+
                    git2::Error::last_error(ret).unwrap()
+
                )
+
            }
+
        })
+
    }
+
}
added git-storage/src/odb.rs
@@ -0,0 +1,97 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
//! The `odb` is separated into two traits: [`Read`] and [`Write`], providing
+
//! access to [git objects][objs].
+
//!
+
//! The [`Read`] trait provides functions for read-only access to the odb.
+
//! The [`Write`] trait provides functions for read and write access to the
+
//! odb, thus it implies the [`Read`] trait.
+
//!
+
//! The reason for separating these types of actions out is that one can infer
+
//! what kind of access a function has to the odb by looking at which trait it
+
//! is using.
+
//!
+
//! For implementations of these traits, this crate provides [`crate::Read`] and
+
//! [`crate::Write`] structs.
+
//!
+
//! [objs]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
+

+
// TODO: this doesn't abstract over the git2 types very well, but it's too much
+
// hassle to massage that right now.
+

+
use std::error::Error;
+

+
pub use git2::{Blob, Commit, Object, Tag, Tree};
+

+
use git_ext::Oid;
+

+
pub mod read;
+
pub use read::Read;
+

+
pub mod write;
+
pub use write::Write;
+

+
/// Find the [`Object`] corresponding to the given `oid`.
+
///
+
/// Will fail if the object does not exist.
+
pub fn object<S>(storage: &S, oid: Oid) -> Result<Object, S::FindObj>
+
where
+
    S: Read<FindObj = git2::Error>,
+
{
+
    storage
+
        .find_object(oid)?
+
        .ok_or_else(|| not_found("object", oid))
+
}
+

+
/// Find the [`Blob`] corresponding to the given `oid`.
+
///
+
/// Will fail if the blob does not exist.
+
pub fn blob<S>(storage: &S, oid: Oid) -> Result<Blob, S::FindBlob>
+
where
+
    S: Read<FindBlob = git2::Error>,
+
{
+
    storage
+
        .find_blob(oid)?
+
        .ok_or_else(|| not_found("blob", oid))
+
}
+

+
/// Find the [`Commit`] corresponding to the given `oid`.
+
///
+
/// Will fail if the commit does not exist.
+
pub fn commit<S>(storage: &S, oid: Oid) -> Result<Commit, S::FindCommit>
+
where
+
    S: Read<FindCommit = git2::Error>,
+
{
+
    storage
+
        .find_commit(oid)?
+
        .ok_or_else(|| not_found("commit", oid))
+
}
+

+
/// Find the [`Tag`] corresponding to the given `oid`.
+
///
+
/// Will fail if the tag does not exist.
+
pub fn tag<S>(storage: &S, oid: Oid) -> Result<Tag, S::FindTag>
+
where
+
    S: Read<FindTag = git2::Error>,
+
{
+
    storage.find_tag(oid)?.ok_or_else(|| not_found("tag", oid))
+
}
+

+
/// Find the [`Tree`] corresponding to the given `oid`.
+
///
+
/// Will fail if the tree does not exist.
+
pub fn tree<S>(storage: &S, oid: Oid) -> Result<Tree, S::FindTree>
+
where
+
    S: Read<FindTree = git2::Error>,
+
{
+
    storage
+
        .find_tree(oid)?
+
        .ok_or_else(|| not_found("tree", oid))
+
}
+

+
fn not_found(kind: &str, oid: Oid) -> git2::Error {
+
    use git2::{ErrorClass::*, ErrorCode::*};
+

+
    git2::Error::new(NotFound, Object, format!("could not find {kind} {oid}"))
+
}
added git-storage/src/odb/read.rs
@@ -0,0 +1,49 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use super::*;
+

+
/// Read-only access to a git refdb.
+
///
+
/// See [`crate::Read`] for an implementation of this trait.
+
pub trait Read {
+
    /// The error type for finding an object in the refdb.
+
    type FindObj: Error + Send + Sync + 'static;
+

+
    /// The error type for finding a blob in the refdb.
+
    type FindBlob: Error + Send + Sync + 'static;
+

+
    /// The error type for finding a commit in the refdb.
+
    type FindCommit: Error + Send + Sync + 'static;
+

+
    /// The error type for finding a tag in the refdb.
+
    type FindTag: Error + Send + Sync + 'static;
+

+
    /// The error type for finding a tree in the refdb.
+
    type FindTree: Error + Send + Sync + 'static;
+

+
    /// Find the [`Object`] corresponding to the given `oid`.
+
    ///
+
    /// Returns `None` if the [`Object`] did not exist.
+
    fn find_object(&self, oid: Oid) -> Result<Option<Object>, Self::FindObj>;
+

+
    /// Find the [`Blob`] corresponding to the given `oid`.
+
    ///
+
    /// Returns `None` if the [`Blob`] did not exist.
+
    fn find_blob(&self, oid: Oid) -> Result<Option<Blob>, Self::FindBlob>;
+

+
    /// Find the [`Commit`] corresponding to the given `oid`.
+
    ///
+
    /// Returns `None` if the [`Commit`] did not exist.
+
    fn find_commit(&self, oid: Oid) -> Result<Option<Commit>, Self::FindCommit>;
+

+
    /// Find the [`Tag`] corresponding to the given `oid`.
+
    ///
+
    /// Returns `None` if the [`Tag`] did not exist.
+
    fn find_tag(&self, oid: Oid) -> Result<Option<Tag>, Self::FindTag>;
+

+
    /// Find the [`Object`] corresponding to the given `oid`.
+
    ///
+
    /// Returns `None` if the [`Tag`] did not exist.
+
    fn find_tree(&self, oid: Oid) -> Result<Option<Tree>, Self::FindTree>;
+
}
added git-storage/src/odb/write.rs
@@ -0,0 +1,66 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use std::error::Error;
+

+
use git_ext::Oid;
+
use git_ref_format::RefStr;
+

+
use super::{Commit, Object, Read, Tree};
+

+
/// Read-write access to a git odb.
+
///
+
/// See [`crate::Write`] for an implementation of this trait.
+
pub trait Write: Read {
+
    /// The error type for writing a blob to the odb.
+
    type WriteBlob: Error + Send + Sync + 'static;
+

+
    /// The error type for writing a commit to the odb.
+
    type WriteCommit: Error + Send + Sync + 'static;
+

+
    /// The error type for writing a tag to the odb.
+
    type WriteTag: Error + Send + Sync + 'static;
+

+
    /// The error type for writing a tree to the odb.
+
    type WriteTree: Error + Send + Sync + 'static;
+

+
    /// Write a [`super::Blob`] containing the `data` provided.
+
    fn write_blob(&self, data: &[u8]) -> Result<Oid, Self::WriteBlob>;
+

+
    /// Write a [`Commit`] that points to the given `tree` and has the provided
+
    /// `parents`.
+
    ///
+
    /// The signature of the [`Commit`] is expected to be provided by the
+
    /// implementor of the trait.
+
    ///
+
    /// The commit will not be associated with any reference. If this is
+
    /// required then you can use the [`Oid`] as the target for a
+
    /// [`crate::refdb::Update`].
+
    fn write_commit<'a>(
+
        &self,
+
        tree: &Tree,
+
        parents: &[&Commit<'a>],
+
        message: &str,
+
    ) -> Result<Oid, Self::WriteCommit>;
+

+
    /// Write a [`super::Tag`] that points to the given `target`.
+
    ///
+
    /// The signature of the [`super::Tag`] is expected to be provided by the
+
    /// implementor of the trait.
+
    ///
+
    /// No reference is created, however, the `name` is used for naming the
+
    /// [`super::Tag`] object.
+
    ///
+
    /// If a reference is required then you can use the [`Oid`] as the target
+
    /// for a [`crate::refdb::Update`].
+
    fn write_tag<R>(&self, name: R, target: &Object, message: &str) -> Result<Oid, Self::WriteTag>
+
    where
+
        R: AsRef<RefStr>;
+

+
    /// Write a [`super::Tree`] using the provided `builder`.
+
    // XXX: It's annoying that this exposes git2 but the effort of abstracting the
+
    // tree builder is too high
+
    // TODO: instead of passing the builder, we could use an enum of operations to
+
    // modify a builder
+
    fn write_tree(&self, builder: git2::TreeBuilder) -> Result<Oid, Self::WriteTree>;
+
}
added git-storage/src/pool.rs
@@ -0,0 +1,176 @@
+
// 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::{
+
    marker::PhantomData,
+
    ops::{Deref, DerefMut},
+
    path::PathBuf,
+
    sync::Arc,
+
};
+

+
use deadpool::managed::{self, Manager, Object, RecycleResult};
+
use parking_lot::RwLock;
+
use std_ext::Void;
+
use thiserror::Error;
+

+
use crate::{read, signature::UserInfo, write, Read, Write};
+

+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum InitError {
+
    #[error(transparent)]
+
    Read(#[from] read::error::Init),
+

+
    #[error(transparent)]
+
    Write(#[from] write::error::Init),
+
}
+

+
pub type Pool<S> = deadpool::managed::Pool<S, InitError>;
+
pub type PoolError = managed::PoolError<InitError>;
+

+
#[async_trait]
+
pub trait Pooled<S: Send> {
+
    async fn get(&self) -> Result<PooledRef<S>, PoolError>;
+
}
+

+
#[async_trait]
+
impl<S: Send> Pooled<S> for Pool<S> {
+
    async fn get(&self) -> Result<PooledRef<S>, PoolError> {
+
        self.get().await.map(PooledRef::from)
+
    }
+
}
+

+
/// A reference to a pooled storage.
+
///
+
/// The `S` parameter can be filled by [`Write`] for read-write access or
+
/// [`Read`] for read-only access.
+
pub struct PooledRef<S>(Object<S, InitError>);
+

+
impl<S> Deref for PooledRef<S> {
+
    type Target = S;
+

+
    fn deref(&self) -> &Self::Target {
+
        self.0.deref()
+
    }
+
}
+

+
impl<S> DerefMut for PooledRef<S> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        self.0.deref_mut()
+
    }
+
}
+

+
impl<S> AsRef<S> for PooledRef<S> {
+
    fn as_ref(&self) -> &S {
+
        self
+
    }
+
}
+

+
impl<S> AsMut<S> for PooledRef<S> {
+
    fn as_mut(&mut self) -> &mut S {
+
        self
+
    }
+
}
+

+
impl AsRef<Read> for PooledRef<Write> {
+
    fn as_ref(&self) -> &Read {
+
        self.0.read_only()
+
    }
+
}
+

+
impl<S> From<Object<S, InitError>> for PooledRef<S> {
+
    fn from(obj: Object<S, InitError>) -> Self {
+
        Self(obj)
+
    }
+
}
+

+
#[derive(Clone)]
+
pub struct Initialised(Arc<RwLock<bool>>);
+

+
impl Initialised {
+
    pub fn no() -> Self {
+
        Self(Arc::new(RwLock::new(false)))
+
    }
+
}
+

+
pub struct Writer {
+
    init: Initialised,
+
}
+

+
#[derive(Clone)]
+
pub struct Config<W> {
+
    root: PathBuf,
+
    info: UserInfo,
+
    write: W,
+
}
+

+
pub type ReadConfig = Config<PhantomData<Void>>;
+
pub type WriteConfig = Config<Writer>;
+

+
impl ReadConfig {
+
    pub fn new(root: PathBuf, info: UserInfo) -> Self {
+
        Config {
+
            root,
+
            info,
+
            write: PhantomData,
+
        }
+
    }
+

+
    pub fn write(self, init: Initialised) -> WriteConfig {
+
        Config {
+
            root: self.root,
+
            info: self.info,
+
            write: Writer { init },
+
        }
+
    }
+
}
+

+
#[async_trait]
+
impl Manager<Read, InitError> for ReadConfig {
+
    async fn create(&self) -> Result<Read, InitError> {
+
        Read::open(&self.root).map_err(InitError::from)
+
    }
+

+
    async fn recycle(&self, _: &mut Read) -> RecycleResult<InitError> {
+
        Ok(())
+
    }
+
}
+

+
impl WriteConfig {
+
    pub fn new(root: PathBuf, info: UserInfo, init: Initialised) -> Self {
+
        Self {
+
            root,
+
            info,
+
            write: Writer { init },
+
        }
+
    }
+

+
    fn mk_storage(&self) -> Result<Write, InitError> {
+
        Write::open(&self.root, self.info.clone()).map_err(InitError::from)
+
    }
+
}
+

+
#[async_trait]
+
impl Manager<Write, InitError> for WriteConfig {
+
    async fn create(&self) -> Result<Write, InitError> {
+
        let initialised = self.write.init.0.read();
+
        if *initialised {
+
            self.mk_storage()
+
        } else {
+
            drop(initialised);
+
            let mut initialised = self.write.init.0.write();
+
            self.mk_storage()
+
                .map(|storage| {
+
                    *initialised = true;
+
                    storage
+
                })
+
                .map_err(InitError::from)
+
        }
+
    }
+

+
    async fn recycle(&self, _: &mut Write) -> RecycleResult<InitError> {
+
        Ok(())
+
    }
+
}
added git-storage/src/refdb.rs
@@ -0,0 +1,125 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
//! The `refdb` is separated into two traits: [`Read`] and [`Write`], providing
+
//! access to [git references][refs].
+
//!
+
//! The [`Read`] trait provides functions for read-only access to the refdb.
+
//! The [`Write`] trait provides functions for read and write access to the
+
//! refdb, thus it implies the [`Read`] trait.
+
//!
+
//! The reason for separating these types of actions out is that one can infer
+
//! what kind of access a function has to the refdb by looking at which trait it
+
//! is using.
+
//!
+
//! For implementations of these traits, this crate provides [`crate::Read`] and
+
//! [`crate::Write`] structs.
+
//!
+
//! [refs]: https://git-scm.com/book/en/v2/Git-Internals-Git-References
+

+
use std::fmt::Debug;
+

+
use git_ext::Oid;
+
use git_ref_format::RefString;
+

+
pub mod iter;
+
pub use iter::{References, ReferencesGlob};
+

+
pub mod read;
+
pub use read::Read;
+

+
pub mod write;
+
pub use write::{previous, Applied, Policy, SymrefTarget, Update, Updated, Write};
+

+
/// A read-only structure representing a git reference.
+
///
+
/// References can be constructed by using the [`Read`] functions:
+
///   * [`Read::find_reference`]
+
///   * [`Read::find_references`]
+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct Reference {
+
    /// The name of the reference.
+
    pub name: RefString,
+
    /// The target of the reference.
+
    pub target: Target,
+
}
+

+
impl<'a> TryFrom<git2::Reference<'a>> for Reference {
+
    type Error = error::ParseReference;
+

+
    fn try_from(value: git2::Reference) -> Result<Self, Self::Error> {
+
        use error::ParseReference;
+

+
        let name = value
+
            .name()
+
            .ok_or(ParseReference::InvalidUtf8)
+
            .and_then(|name| RefString::try_from(name).map_err(ParseReference::from))?;
+
        let target = match value.target() {
+
            None => {
+
                let name = value
+
                    .symbolic_target()
+
                    .ok_or(ParseReference::InvalidUtf8)
+
                    .and_then(|name| RefString::try_from(name).map_err(ParseReference::from))?;
+
                name.into()
+
            },
+
            Some(oid) => oid.into(),
+
        };
+

+
        Ok(Reference { name, target })
+
    }
+
}
+

+
/// The target a reference points to.
+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub enum Target {
+
    /// The reference points directly at an `oid`.
+
    Direct { oid: Oid },
+
    /// The reference is symbolic and points to another reference.
+
    Symbolic { name: RefString },
+
}
+

+
impl From<Oid> for Target {
+
    fn from(oid: Oid) -> Self {
+
        Self::Direct { oid }
+
    }
+
}
+

+
impl From<git2::Oid> for Target {
+
    fn from(oid: git2::Oid) -> Self {
+
        Oid::from(oid).into()
+
    }
+
}
+

+
impl From<RefString> for Target {
+
    fn from(name: RefString) -> Self {
+
        Self::Symbolic { name }
+
    }
+
}
+

+
pub mod error {
+
    use thiserror::Error;
+

+
    #[derive(Debug, Error)]
+
    pub enum ParseReference {
+
        #[error("reference name did contain valid UTF-8 bytes")]
+
        InvalidUtf8,
+
        #[error(transparent)]
+
        RefString(#[from] git_ref_format::Error),
+
    }
+

+
    #[derive(Debug, Error)]
+
    pub enum Iter {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error(transparent)]
+
        Parse(#[from] ParseReference),
+
    }
+
}
+

+
/// Utility function for resolving a [`Reference`] to its [`Oid`]
+
pub(crate) fn resolve(repo: &git2::Repository, r: &Reference) -> Result<Oid, git2::Error> {
+
    match &r.target {
+
        Target::Direct { oid } => Ok(*oid),
+
        Target::Symbolic { name } => repo.refname_to_id(name.as_str()).map(Oid::from),
+
    }
+
}
added git-storage/src/refdb/iter.rs
@@ -0,0 +1,49 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
//! Iterator adaptors over [`git2::References`].
+

+
use std::fmt::Debug;
+

+
use crate::glob;
+

+
use super::{error, Reference};
+

+
/// Iterator for [`Reference`]s where the inner [`glob::Pattern`] supplied
+
/// filters out any non-matching reference names.
+
pub struct References<'a> {
+
    pub(crate) inner: ReferencesGlob<'a, glob::RefspecMatcher>,
+
}
+

+
impl<'a> Iterator for References<'a> {
+
    type Item = Result<Reference, error::Iter>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.inner.next()
+
    }
+
}
+

+
pub struct ReferencesGlob<'a, G: glob::Pattern + Debug> {
+
    pub(crate) iter: git2::References<'a>,
+
    pub(crate) glob: G,
+
}
+

+
impl<'a, G: glob::Pattern + Debug> Iterator for ReferencesGlob<'a, G> {
+
    type Item = Result<Reference, error::Iter>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        for reference in &mut self.iter {
+
            match reference {
+
                Ok(reference) => match reference.name() {
+
                    Some(name) if self.glob.matches(name) => {
+
                        return Some(Reference::try_from(reference).map_err(error::Iter::from))
+
                    },
+
                    _ => continue,
+
                },
+

+
                Err(e) => return Some(Err(e.into())),
+
            }
+
        }
+
        None
+
    }
+
}
added git-storage/src/refdb/read.rs
@@ -0,0 +1,43 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use std::error::Error;
+

+
use git_ext::Oid;
+
use git_ref_format::{refspec, RefStr};
+

+
use super::Reference;
+

+
/// Read-only access to a git refdb.
+
///
+
/// See [`crate::Read`] for an implementation of this trait.
+
pub trait Read {
+
    /// The error type for finding a reference in the refdb.
+
    type FindRef: Error + Send + Sync + 'static;
+

+
    /// The error type for finding references in the refdb.
+
    type FindRefs: Error + Send + Sync + 'static;
+

+
    /// The error type for finding reference Oid in the refdb.
+
    type FindRefOid: Error + Send + Sync + 'static;
+

+
    /// Iterator for references returned by `find_references`.
+
    type References: Iterator<Item = Result<Reference, Self::FindRefs>>;
+

+
    /// Find the reference that corresponds to `name`. If the reference does not
+
    /// exist, then `None` is returned.
+
    fn find_reference<Ref>(&self, name: Ref) -> Result<Option<Reference>, Self::FindRef>
+
    where
+
        Ref: AsRef<RefStr>;
+

+
    /// Find the references that match `pattern`.
+
    fn find_references<Pat>(&self, pattern: Pat) -> Result<Self::References, Self::FindRefs>
+
    where
+
        Pat: AsRef<refspec::PatternStr>;
+

+
    /// Find the [`Oid`] for the reference that corresponds to `name`. If the
+
    /// reference does not exist, then `None` is returned.
+
    fn find_reference_oid<Ref>(&self, name: Ref) -> Result<Option<Oid>, Self::FindRefOid>
+
    where
+
        Ref: AsRef<RefStr>;
+
}
added git-storage/src/refdb/write.rs
@@ -0,0 +1,152 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use std::error::Error;
+

+
use git_ext::Oid;
+
use git_ref_format::{Qualified, RefString};
+

+
use super::read::Read;
+

+
pub mod previous;
+

+
/// Read-write access to a git refdb.
+
///
+
/// See [`crate::Write`] for an implementation of this trait.
+
pub trait Write: Read {
+
    type UpdateError: Error + Send + Sync + 'static;
+

+
    /// Apply the provided `updates` to the refdb.
+
    ///
+
    /// The implementation of `update` is expected to be transactional. All
+
    /// successful updates are returned in [`Applied::updated`] while rejected
+
    /// updates should be returned in [`Applied::rejected`].
+
    fn update<'a, U>(&mut self, updates: U) -> Result<Applied<'a>, Self::UpdateError>
+
    where
+
        U: IntoIterator<Item = Update<'a>>;
+
}
+

+
/// Perform an update to a reference in the refdb.
+
#[derive(Debug, Clone)]
+
pub enum Update<'a> {
+
    /// Update a direct reference, i.e. one that points directly to an [`Oid`].
+
    Direct {
+
        /// The `name` of the reference.
+
        name: Qualified<'a>,
+
        /// The `target` to point the reference at.
+
        target: Oid,
+
        /// Policy to apply when an [`Update`] would not apply as a
+
        /// fast-forward.
+
        ///
+
        /// An update is a fast-forward iff:
+
        ///
+
        /// 1. A ref with the same name already exists
+
        /// 2. The ref is a direct ref, and the update is a [`Update::Direct`]
+
        /// 3. Both the existing and the update [`Oid`] point to a commit
+
        ///    object without peeling
+
        /// 4. The existing commit is an ancestor of the update commit
+
        ///
+
        /// or:
+
        ///
+
        /// 1. A ref with the same name does not already exist
+
        no_ff: Policy,
+
        /// The expectation of the previous value for the reference before
+
        /// making the update. This allows the update to be rejected in
+
        /// the case of a concurrent modification the reference.
+
        previous: previous::Edit,
+
        /// The [`reflog`][reflog] entry for the update.
+
        ///
+
        /// [reflog]: https://git-scm.com/docs/git-reflog
+
        // TODO: we may want to force the creation of a reflog entry for specific references.
+
        reflog: String,
+
    },
+
    Symbolic {
+
        /// The `name` of the reference.
+
        name: Qualified<'a>,
+
        /// The `target` to point the reference at. Currently, only supports
+
        /// one-level deep.
+
        target: SymrefTarget<'a>,
+
        /// Policy to apply when the ref already exists, but is a direct ref
+
        /// before the update.
+
        type_change: Policy,
+
        /// The expectation of the previous value for the reference before
+
        /// making the update. This allows the update to be rejected in
+
        /// the case of a concurrent modification the reference.
+
        previous: previous::Edit,
+
        /// The [`reflog`][reflog] entry for the update.
+
        ///
+
        /// [reflog]: https://git-scm.com/docs/git-reflog
+
        reflog: String,
+
    },
+
    Remove {
+
        /// The `name` of the reference.
+
        name: Qualified<'a>,
+
        /// The expectation of the previous value for the reference before
+
        /// making the update. This allows the update to be rejected in
+
        /// the case of a concurrent modification the reference.
+
        previous: previous::Remove,
+
    },
+
}
+

+
/// A target of a [symbolic reference][symref].
+
///
+
/// [symref]: https://git-scm.com/docs/git-symbolic-ref
+
#[derive(Debug, Clone)]
+
pub struct SymrefTarget<'a> {
+
    /// The `name` of the symbolic reference.
+
    pub name: Qualified<'a>,
+
    /// The underlying `target` of the symbolic reference.
+
    pub target: Oid,
+
}
+

+
/// The successful result of an [`Update`] applied to the refdb.
+
#[derive(Debug, Clone)]
+
pub enum Updated {
+
    /// The [`Update::Direct`] was succesful.
+
    Direct {
+
        /// The `name` of the reference that was updated.
+
        name: RefString,
+
        /// The new `target` of the reference that was updated.
+
        target: Oid,
+
        /// The previous target of the reference, if it existed.
+
        previous: Option<Oid>,
+
    },
+
    /// The [`Update::Symbolic`] was succesful.
+
    Symbolic {
+
        /// The `name` of the reference that was updated.
+
        name: RefString,
+
        /// The new `target` of the reference that was updated.
+
        target: RefString,
+
        /// The previous peeled target of the reference, if it existed.
+
        previous: Option<Oid>,
+
    },
+
    /// The [`Update::Remove`] was succesful.
+
    Removed {
+
        /// The `name` of the reference that was removed.
+
        name: RefString,
+
        /// The old `target` of the reference that was removed.
+
        previous: Oid,
+
    },
+
}
+

+
/// The outcome of running a [`Write::update`].
+
#[derive(Clone, Default)]
+
pub struct Applied<'a> {
+
    /// Any [`Update`]s that were rejected due to their [`previous::Edit`],
+
    /// [`previous::Remove`], or [`Policy`].
+
    pub rejected: Vec<Update<'a>>,
+
    /// The successful [`Update`]s applied to the refdb.
+
    pub updated: Vec<Updated>,
+
}
+

+
/// The policy to use when guarding against fast-forwards in the case of
+
/// [`Update::Direct`] and type changes in the case of [`Update::Symbolic`].
+
#[derive(Clone, Copy, Debug)]
+
pub enum Policy {
+
    /// Abort the entire transaction.
+
    Abort,
+
    /// Reject this update, but continue the transaction.
+
    Reject,
+
    /// Allow the update.
+
    Allow,
+
}
added git-storage/src/refdb/write/previous.rs
@@ -0,0 +1,132 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
//! The [`Edit`] and [`Remove`] enums provide a mechanism for protecting updates
+
//! during concurrent writes to the refdb. The value is constructed with the
+
//! policy, sometimes providing the expected previous value of the [`Oid`]
+
//! target. If this policy is not satisfied then the update should be rejected.
+

+
use git_ext::Oid;
+

+
use thiserror::Error;
+

+
/// The expectation of the reference's state.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum Edit {
+
    /// No requirements are made towards the current value, and the new value is
+
    /// set unconditionally.
+
    Any,
+
    /// The reference must exist and may have any value.
+
    MustExist,
+
    /// Create the ref only, hence the reference must not exist.
+
    MustNotExist,
+
    /// The ref _must_ exist and have the given value.
+
    MustExistAndMatch(Oid),
+
    /// The ref _may_ exist and have the given value, or may not exist at all.
+
    MayExistAndMatch(Oid),
+
}
+

+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Error)]
+
pub enum EditError {
+
    #[error("the reference does not exist when it was expected to exist")]
+
    DoesNotExist,
+
    #[error("the reference does exist when it was expected not to exist")]
+
    DoesExist,
+
    #[error("the reference does not match - given: '{given}' expected: '{expected}'")]
+
    DoesNotMatch { given: Oid, expected: Oid },
+
}
+

+
impl Edit {
+
    /// Guard against the `given` value using this [`Edit`].
+
    ///
+
    /// The function will return [`Err`] if the `given` value does not pass the
+
    /// policy the policy of the [`Edit`].
+
    ///
+
    /// The `given` value is presumed non-existing if set to `None`, otherwise
+
    /// it is presumed to be existing.
+
    pub fn guard(&self, given: Option<Oid>) -> Result<(), EditError> {
+
        use EditError::*;
+

+
        match self {
+
            Self::Any => Ok(()),
+
            Self::MustExist => {
+
                if given.is_none() {
+
                    Err(DoesNotExist)
+
                } else {
+
                    Ok(())
+
                }
+
            },
+
            Self::MustNotExist => {
+
                if given.is_some() {
+
                    Err(DoesExist)
+
                } else {
+
                    Ok(())
+
                }
+
            },
+
            Self::MustExistAndMatch(expected) => match given {
+
                Some(given) if &given == expected => Ok(()),
+
                Some(given) => Err(DoesNotMatch {
+
                    given,
+
                    expected: *expected,
+
                }),
+
                None => Err(DoesNotExist),
+
            },
+
            Self::MayExistAndMatch(expected) => match given {
+
                Some(given) if &given == expected => Ok(()),
+
                Some(given) => Err(DoesNotMatch {
+
                    given,
+
                    expected: *expected,
+
                }),
+
                None => Ok(()),
+
            },
+
        }
+
    }
+
}
+

+
/// The expectation of the existing reference's state.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum Remove {
+
    /// The reference must exist and may have any value.
+
    MustExist,
+
    /// The ref _must_ exist and have the given value.
+
    MustExistAndMatch(Oid),
+
}
+

+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Error)]
+
pub enum RemoveError {
+
    #[error("the reference does not exist when it was expected to exist")]
+
    DoesNotExist,
+
    #[error("the reference does not match - given: '{given}' expected: '{expected}'")]
+
    DoesNotMatch { given: Oid, expected: Oid },
+
}
+

+
impl Remove {
+
    /// Guard against the `given` value using this [`Remove`].
+
    ///
+
    /// The function will return [`Err`] if the `given` value does not pass the
+
    /// policy the policy of the [`Remove`].
+
    ///
+
    /// The `given` value is presumed non-existing if set to `None`, otherwise
+
    /// it is presumed to be existing.
+
    pub fn guard(&self, given: Option<Oid>) -> Result<(), RemoveError> {
+
        use RemoveError::*;
+

+
        match self {
+
            Self::MustExist => {
+
                if given.is_none() {
+
                    Err(DoesNotExist)
+
                } else {
+
                    Ok(())
+
                }
+
            },
+
            Self::MustExistAndMatch(expected) => match given {
+
                Some(given) if &given == expected => Ok(()),
+
                Some(given) => Err(DoesNotMatch {
+
                    given,
+
                    expected: *expected,
+
                }),
+
                None => Err(DoesNotExist),
+
            },
+
        }
+
    }
+
}
added git-storage/src/signature.rs
@@ -0,0 +1,19 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+
pub struct UserInfo {
+
    /// Provided `name` of the user.
+
    pub name: String,
+
    /// Proivded `email` of the user. Note that this does not
+
    /// necessarily have to be an email, but will be used as the email
+
    /// field in the [`git2::Signature`].
+
    pub email: String,
+
}
+

+
impl UserInfo {
+
    /// Obtain the [`git2::Signature`] for this `UserInfo`.
+
    pub fn signature(&self) -> Result<git2::Signature, git2::Error> {
+
        git2::Signature::now(&self.name, &self.email)
+
    }
+
}