Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src storage.rs
pub mod git;
pub mod refs;

use std::collections::{HashSet, hash_map};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::{fmt, io};

use nonempty::NonEmpty;
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub use crate::git::Oid;
use crypto::PublicKey;
pub use git::{Validation, Validations};

use crate::cob;
use crate::collections::RandomMap;
use crate::git::RefError;
use crate::git::canonical;
use crate::git::fmt::{Qualified, RefStr, RefString, refspec::PatternString, refspec::Refspec};
use crate::git::raw::ErrorExt as _;
use crate::identity::{Did, PayloadError, doc};
use crate::identity::{Doc, DocAt, DocError};
use crate::identity::{Identity, RepoId};
use crate::node::SyncedAt;
use crate::storage::git::NAMESPACES_GLOB;
use crate::storage::refs::{FeatureLevel, Refs, SignedRefs};

use self::refs::RefsAt;
use crate::git::UserInfo;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignedRefsInfo {
    /// Repositories with this set to `None` are ones that are seeded but not forked.
    None,
    /// Local signed refs, if any.
    Some(refs::SignedRefs),
    NeedsMigration,
}

impl SignedRefsInfo {
    pub(crate) fn new(
        result: Result<Option<SignedRefs>, refs::sigrefs::read::error::Read>,
    ) -> Result<Self, refs::sigrefs::read::error::Read> {
        Ok(match result {
            Ok(Some(refs))
                if refs.feature_level() >= FeatureLevel::LATEST && refs.parent().is_some() =>
            {
                SignedRefsInfo::Some(refs)
            }
            Ok(Some(_)) => SignedRefsInfo::NeedsMigration,
            Ok(None) => SignedRefsInfo::None,
            Err(refs::sigrefs::read::error::Read::Downgrade { .. }) => {
                SignedRefsInfo::NeedsMigration
            }
            Err(err) => return Err(err),
        })
    }
}

/// Basic repository information.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepositoryInfo {
    /// Repository identifier.
    pub rid: RepoId,
    /// Head of default branch.
    pub head: Oid,
    /// Identity document.
    pub doc: Doc,
    /// Information about local signed references.
    pub refs: SignedRefsInfo,
    /// Sync time of the repository.
    pub synced_at: Option<SyncedAt>,
}

/// Describes one or more namespaces.
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub enum Namespaces {
    /// All namespaces.
    #[default]
    All,
    /// The followed set of namespaces.
    Followed(HashSet<PublicKey>),
}

impl Namespaces {
    pub fn to_refspecs(&self) -> Vec<Refspec<PatternString, PatternString>> {
        match self {
            Namespaces::All => vec![Refspec {
                src: (*NAMESPACES_GLOB).clone(),
                dst: (*NAMESPACES_GLOB).clone(),
                force: true,
            }],
            Namespaces::Followed(pks) => pks
                .iter()
                .map(|pk| {
                    let ns = pk
                        .to_namespace()
                        .with_pattern(crate::git::fmt::refspec::STAR);
                    Refspec {
                        src: ns.clone(),
                        dst: ns,
                        force: true,
                    }
                })
                .collect(),
        }
    }
}

impl FromIterator<PublicKey> for Namespaces {
    fn from_iter<T: IntoIterator<Item = PublicKey>>(iter: T) -> Self {
        Self::Followed(iter.into_iter().collect())
    }
}

/// Output of [`WriteRepository::set_default_branch_to_canonical_head`].
pub struct SetHead {
    /// Old branch head.
    pub old: Option<Oid>,
    /// New branch head.
    pub new: Oid,
}

impl SetHead {
    /// Check if the head was updated.
    pub fn is_updated(&self) -> bool {
        self.old != Some(self.new)
    }
}

/// Repository error.
#[derive(Error, Debug)]
pub enum RepositoryError {
    #[error(transparent)]
    Storage(Box<Error>),
    #[error(transparent)]
    Store(#[from] cob::store::Error),
    #[error(transparent)]
    Doc(#[from] DocError),
    #[error(transparent)]
    Payload(#[from] PayloadError),
    #[error(transparent)]
    Git(#[from] crate::git::raw::Error),
    #[error(transparent)]
    Quorum(#[from] canonical::error::QuorumError),
    #[error(transparent)]
    Refs(Box<refs::Error>),
    #[error("missing canonical reference rule for default branch")]
    MissingBranchRule,
    #[error("could not get the default branch rule: {0}")]
    DefaultBranchRule(#[from] doc::DefaultBranchRuleError),
    #[error("failed to get canonical reference rules: {0}")]
    CanonicalRefs(#[from] doc::CanonicalRefsError),
    #[error(transparent)]
    FindObjects(#[from] canonical::effects::FindObjectsError),
}

impl From<Error> for RepositoryError {
    fn from(err: Error) -> Self {
        Self::Storage(Box::new(err))
    }
}

impl From<refs::Error> for RepositoryError {
    fn from(err: refs::Error) -> Self {
        Self::Refs(Box::new(err))
    }
}

impl RepositoryError {
    pub fn is_not_found(&self) -> bool {
        match self {
            Self::Storage(e) => e.is_not_found(),
            Self::Git(e) => e.is_not_found(),
            _ => false,
        }
    }
}

/// Storage error.
#[derive(Error, Debug)]
pub enum Error {
    #[error("invalid git reference")]
    InvalidRef,
    #[error("identity doc: {0}")]
    Doc(#[from] DocError),
    #[error("git reference error: {0}")]
    Ref(#[from] RefError),
    #[error(transparent)]
    Refs(#[from] refs::Error),
    #[error("git: {0}")]
    Git(#[from] crate::git::raw::Error),
    #[error("invalid repository identifier {0:?}")]
    InvalidId(std::ffi::OsString),
    #[error("i/o: {0}")]
    Io(#[from] io::Error),
}

impl Error {
    /// Whether this error is caused by something not being found.
    pub fn is_not_found(&self) -> bool {
        match self {
            Self::Io(e) if e.kind() == io::ErrorKind::NotFound => true,
            Self::Git(e) if e.is_not_found() => true,
            Self::Doc(e) if e.is_not_found() => true,
            _ => false,
        }
    }
}

/// Fetch error.
#[derive(Error, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum FetchError {
    #[error("git: {0}")]
    Git(#[from] crate::git::raw::Error),
    #[error("i/o: {0}")]
    Io(#[from] io::Error),
    #[error(transparent)]
    Refs(#[from] refs::Error),
    #[error(transparent)]
    Storage(#[from] Error),
    #[error("failed to validate remote layouts in storage")]
    Validation { validations: Validations },
    #[error("repository head: {0}")]
    SetHead(#[from] DocError),
    #[error("repository: {0}")]
    Repository(#[from] RepositoryError),
}

pub type RemoteId = PublicKey;

/// An update to a reference.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum RefUpdate {
    Updated {
        #[cfg_attr(
            feature = "schemars",
            schemars(with = "crate::schemars_ext::git::fmt::RefString")
        )]
        name: RefString,
        old: Oid,
        new: Oid,
    },
    Created {
        #[cfg_attr(
            feature = "schemars",
            schemars(with = "crate::schemars_ext::git::fmt::RefString")
        )]
        name: RefString,
        oid: Oid,
    },
    Deleted {
        #[cfg_attr(
            feature = "schemars",
            schemars(with = "crate::schemars_ext::git::fmt::RefString")
        )]
        name: RefString,
        oid: Oid,
    },
    Skipped {
        #[cfg_attr(feature = "schemars", schemars(with = "String"))]
        name: RefString,
        oid: Oid,
    },
}

impl RefUpdate {
    pub fn from(name: RefString, old: impl Into<Oid>, new: impl Into<Oid>) -> Self {
        let old = old.into();
        let new = new.into();

        if old.is_zero() {
            Self::Created { name, oid: new }
        } else if new.is_zero() {
            Self::Deleted { name, oid: old }
        } else if old != new {
            Self::Updated { name, old, new }
        } else {
            Self::Skipped { name, oid: old }
        }
    }

    /// Get the old OID, if any.
    pub fn old(&self) -> Option<Oid> {
        match self {
            RefUpdate::Updated { old, .. } => Some(*old),
            RefUpdate::Created { .. } => None,
            RefUpdate::Deleted { oid, .. } => Some(*oid),
            RefUpdate::Skipped { oid, .. } => Some(*oid),
        }
    }

    /// Get the new OID, if any.
    #[allow(clippy::new_ret_no_self)]
    pub fn new(&self) -> Option<Oid> {
        match self {
            RefUpdate::Updated { new, .. } => Some(*new),
            RefUpdate::Created { oid, .. } => Some(*oid),
            RefUpdate::Deleted { .. } => None,
            RefUpdate::Skipped { .. } => None,
        }
    }

    /// Get the ref name.
    pub fn name(&self) -> &RefStr {
        match self {
            RefUpdate::Updated { name, .. } => name.as_refstr(),
            RefUpdate::Created { name, .. } => name.as_refstr(),
            RefUpdate::Deleted { name, .. } => name.as_refstr(),
            RefUpdate::Skipped { name, .. } => name.as_refstr(),
        }
    }

    /// Is it an update.
    pub fn is_updated(&self) -> bool {
        matches!(self, RefUpdate::Updated { .. })
    }

    /// Is it a create.
    pub fn is_created(&self) -> bool {
        matches!(self, RefUpdate::Created { .. })
    }

    /// Is it a skip.
    pub fn is_skipped(&self) -> bool {
        matches!(self, RefUpdate::Skipped { .. })
    }
}

impl fmt::Display for RefUpdate {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Updated { name, old, new } => {
                write!(f, "~ {old:.7}..{new:.7} {name}")
            }
            Self::Created { name, oid } => {
                write!(f, "* 0000000..{oid:.7} {name}")
            }
            Self::Deleted { name, oid } => {
                write!(f, "- {oid:.7}..0000000 {name}")
            }
            Self::Skipped { name, oid } => {
                write!(f, "= {oid:.7}..{oid:.7} {name}")
            }
        }
    }
}

/// Project remotes. Tracks the git state of a project.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Remotes(RandomMap<RemoteId, Remote>);

impl FromIterator<(RemoteId, Remote)> for Remotes {
    fn from_iter<T: IntoIterator<Item = (RemoteId, Remote)>>(iter: T) -> Self {
        Self(iter.into_iter().collect())
    }
}

impl Deref for Remotes {
    type Target = RandomMap<RemoteId, Remote>;

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

impl Remotes {
    pub fn new(remotes: RandomMap<RemoteId, Remote>) -> Self {
        Self(remotes)
    }
}

impl IntoIterator for Remotes {
    type Item = (RemoteId, Remote);
    type IntoIter = hash_map::IntoIter<RemoteId, Remote>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl From<Remotes> for RandomMap<RemoteId, SignedRefs> {
    fn from(other: Remotes) -> Self {
        let mut remotes = RandomMap::with_hasher(fastrand::Rng::new().into());

        for (k, v) in other.into_iter() {
            remotes.insert(k, v.refs);
        }
        remotes
    }
}

/// A project remote.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Remote {
    /// Git references published under this remote, and their hashes.
    #[serde(flatten)]
    pub refs: SignedRefs,
}

impl Remote {
    /// Create a new remotes object.
    pub fn new(refs: impl Into<SignedRefs>) -> Self {
        Self { refs: refs.into() }
    }

    pub fn to_refspecs(&self) -> Vec<Refspec<PatternString, PatternString>> {
        let ns = self.id().to_namespace();
        // Nb. the references in Refs are expected to be Qualified
        self.refs
            .keys()
            .map(|name| {
                let name = PatternString::from(ns.join(name));
                Refspec {
                    src: name.clone(),
                    dst: name,
                    force: true,
                }
            })
            .collect()
    }
}

impl Deref for Remote {
    type Target = SignedRefs;

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

/// Read-only operations on a storage instance.
pub trait ReadStorage {
    type Repository: ReadRepository + self::refs::sigrefs::git::reference::Reader;

    /// Get user info for this storage.
    fn info(&self) -> &UserInfo;
    /// Get the storage base path.
    fn path(&self) -> &Path;
    /// Get a repository's path.
    fn path_of(&self, rid: &RepoId) -> PathBuf;
    /// Check whether storage contains a repository.
    fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError>;
    /// Return all repositories (public and private).
    fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error>;
    /// Open or create a read-only repository.
    fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError>;
    /// Get a repository's identity if it exists.
    fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError> {
        match self.repository(rid) {
            Ok(repo) => Ok(Some(repo.identity_doc()?.into())),
            Err(e) if e.is_not_found() => Ok(None),
            Err(e) => Err(e),
        }
    }
}

/// Allows access to individual storage repositories.
pub trait WriteStorage: ReadStorage {
    type RepositoryMut: WriteRepository;

    /// Open a read-write repository.
    fn repository_mut(&self, rid: RepoId) -> Result<Self::RepositoryMut, RepositoryError>;
    /// Create a read-write repository.
    fn create(&self, rid: RepoId) -> Result<Self::RepositoryMut, Error>;

    /// Clean the repository found at `rid`.
    ///
    /// If the local peer has initialised `rad/sigrefs` by forking or
    /// creating any COBs, then this will delete all remote namespaces
    /// that are neither the local's or a delegate's.
    ///
    /// If the local peer has no initialised `rad/sigrefs`, then the
    /// repository will be entirely removed from storage.
    fn clean(&self, rid: RepoId) -> Result<Vec<RemoteId>, RepositoryError>;
}

/// Anything can return the [`RepoId`] that it is associated with.
pub trait HasRepoId {
    fn rid(&self) -> RepoId;
}

impl<T: ReadRepository> HasRepoId for T {
    fn rid(&self) -> RepoId {
        ReadRepository::id(self)
    }
}

/// Allows read-only access to a repository.
pub trait ReadRepository: Sized + ValidateRepository {
    /// Return the repository id.
    fn id(&self) -> RepoId;

    /// Returns `true` if there are no references in the repository.
    fn is_empty(&self) -> Result<bool, crate::git::raw::Error>;

    /// The [`Path`] to the git repository.
    fn path(&self) -> &Path;

    /// Get a blob in this repository at the given commit and path.
    fn blob_at<P: AsRef<Path>>(
        &self,
        commit: Oid,
        path: P,
    ) -> Result<crate::git::raw::Blob<'_>, crate::git::raw::Error>;

    /// Get a blob in this repository, given its id.
    fn blob(&self, oid: Oid) -> Result<crate::git::raw::Blob<'_>, crate::git::raw::Error>;

    /// Get the head of this repository.
    ///
    /// Returns the reference pointed to by `HEAD` if it is set. Otherwise, computes the canonical
    /// head using [`ReadRepository::canonical_head`].
    ///
    /// Returns the [`Oid`] as well as the qualified reference name.
    fn head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError>;

    /// Gets the qualified reference name of the default branch of self,
    /// according to the project payload in the identity document.
    fn default_branch(&self) -> Result<Qualified<'_>, RepositoryError> {
        Ok(self.identity_doc()?.default_branch()?.to_owned())
    }

    /// Compute the canonical head of this repository.
    ///
    /// Ignores any existing `HEAD` reference.
    ///
    /// Returns the [`Oid`] as well as the qualified reference name.
    fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError>;

    /// Get the head of the `rad/id` reference in this repository.
    ///
    /// Returns the reference pointed to by `rad/id` if it is set. Otherwise, computes the canonical
    /// `rad/id` using [`ReadRepository::canonical_identity_head`].
    fn identity_head(&self) -> Result<Oid, RepositoryError>;

    /// Get the identity head of a specific remote.
    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, crate::git::raw::Error>;

    /// Get the root commit of the canonical identity branch.
    fn identity_root(&self) -> Result<Oid, RepositoryError>;

    /// Get the root commit of the identity branch of a specific remote.
    fn identity_root_of(&self, remote: &RemoteId) -> Result<Oid, RepositoryError>;

    /// Load the identity history.
    fn identity(&self) -> Result<Identity, RepositoryError>
    where
        Self: cob::Store,
    {
        Identity::load(self)
    }

    /// Compute the canonical `rad/id` of this repository.
    ///
    /// Ignores any existing `rad/id` reference.
    fn canonical_identity_head(&self) -> Result<Oid, RepositoryError>;

    /// Compute the canonical identity document.
    fn canonical_identity_doc(&self) -> Result<DocAt, RepositoryError> {
        let head = self.canonical_identity_head()?;
        let doc = self.identity_doc_at(head)?;

        Ok(doc)
    }

    /// Get the `reference` for the given `remote`.
    ///
    /// Returns `None` is the reference did not exist.
    fn reference(
        &self,
        remote: &RemoteId,
        reference: &Qualified,
    ) -> Result<crate::git::raw::Reference<'_>, crate::git::raw::Error>;

    /// Get the [`crate::git::raw::Commit`] found using its `oid`.
    ///
    /// Returns `Err` if the commit did not exist.
    fn commit(&self, oid: Oid) -> Result<crate::git::raw::Commit<'_>, crate::git::raw::Error>;

    /// Perform a revision walk of a commit history starting from the given head.
    fn revwalk(&self, head: Oid) -> Result<crate::git::raw::Revwalk<'_>, crate::git::raw::Error>;

    /// Check if the underlying ODB contains the given `oid`.
    fn contains(&self, oid: Oid) -> Result<bool, crate::git::raw::Error>;

    /// Check whether the given commit is an ancestor of another commit.
    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, crate::git::raw::Error>;

    /// Get the object id of a reference under the given remote.
    fn reference_oid(
        &self,
        remote: &RemoteId,
        reference: &Qualified,
    ) -> Result<Oid, crate::git::raw::Error>;

    /// Get all references of the given remote.
    fn references_of(&self, remote: &RemoteId) -> Result<Refs, Error>;

    /// Get all references following a pattern.
    /// Skips references with names that are not parseable into [`Qualified`].
    ///
    /// This function always peels reference to the commit. For tags, this means the [`Oid`] of the
    /// commit pointed to by the tag is returned, and not the [`Oid`] of the tag itself.
    fn references_glob(
        &self,
        pattern: &crate::git::fmt::refspec::PatternStr,
    ) -> Result<Vec<(Qualified<'_>, Oid)>, crate::git::raw::Error>;

    /// Get repository delegates.
    fn delegates(&self) -> Result<NonEmpty<Did>, RepositoryError> {
        let doc = self.identity_doc()?;

        Ok(doc.delegates().clone().into())
    }

    /// Get the repository's identity document.
    fn identity_doc(&self) -> Result<DocAt, RepositoryError> {
        let head = self.identity_head()?;
        let doc = self.identity_doc_at(head)?;

        Ok(doc)
    }

    /// Get the repository's identity document at a specific commit.
    fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError>;

    /// Get the merge base of two commits.
    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, crate::git::raw::Error>;
}

/// Access the remotes of a repository.
pub trait RemoteRepository {
    /// Get the given remote.
    fn remote(&self, remote: &RemoteId) -> Result<Remote, refs::Error>;

    /// Get all remotes.
    fn remotes(&self) -> Result<Remotes, refs::Error>;

    /// Get [`RefsAt`] of all remotes.
    fn remote_refs_at(&self) -> Result<Vec<RefsAt>, refs::Error>;
}

pub trait ValidateRepository
where
    Self: RemoteRepository,
{
    /// Validate all remotes with [`ValidateRepository::validate_remote`].
    fn validate(&self) -> Result<Validations, Error> {
        let mut failures = Validations::default();
        for (_, remote) in self.remotes()? {
            failures.append(&mut self.validate_remote(&remote)?);
        }
        Ok(failures)
    }

    /// Validates a remote's signed refs and identity.
    ///
    /// Returns any ref found under that remote that isn't signed.
    /// If a signed ref is missing from the repository, an error is returned.
    fn validate_remote(&self, remote: &Remote) -> Result<Validations, Error>;
}

/// Allows read-write access to a repository.
pub trait WriteRepository: ReadRepository + SignRepository {
    /// Sets the symbolic reference `HEAD` to target the default branch.
    /// This only depends on the value for the default branch in the identity
    /// document, and does not require the canonical reference behind the
    /// default branch to be computed, or even exist.
    fn set_head_to_default_branch(&self) -> Result<(), RepositoryError>;

    /// Computes the head of the default branch based on the delegate set,
    /// and sets it.
    fn set_default_branch_to_canonical_head(&self) -> Result<SetHead, RepositoryError>;

    /// Set the repository 'rad/id' to the canonical commit, agreed by quorum.
    fn set_identity_head(&self) -> Result<Oid, RepositoryError> {
        let head = self.canonical_identity_head()?;
        self.set_identity_head_to(head)?;

        Ok(head)
    }
    /// Set the identity root reference to the canonical identity root commit.
    fn set_remote_identity_root(&self, remote: &RemoteId) -> Result<Oid, RepositoryError> {
        let root = self.identity_root()?;
        self.set_remote_identity_root_to(remote, root)?;

        Ok(root)
    }
    /// Set the identity root reference to the given commit.
    fn set_remote_identity_root_to(
        &self,
        remote: &RemoteId,
        root: Oid,
    ) -> Result<(), RepositoryError>;
    /// Set the repository 'rad/id' to the given commit.
    fn set_identity_head_to(&self, commit: Oid) -> Result<(), RepositoryError>;
    /// Set the user info of the Git repository.
    fn set_user(&self, info: &UserInfo) -> Result<(), Error>;
    /// Get the underlying git repository.
    fn raw(&self) -> &crate::git::raw::Repository;
}

/// Allows signing refs.
pub trait SignRepository {
    /// Sign the repository's refs under the `refs/rad/sigrefs` branch.
    fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
    where
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
        Signer: crypto::signature::Signer<crypto::Signature>,
        Signer: crypto::signature::Verifier<crypto::Signature>;

    /// Sign the repository's refs under the `refs/rad/sigrefs` branch, even if unchanged.
    ///
    /// Most users will prefer [`Self::sign_refs`].
    fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
    where
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
        Signer: crypto::signature::Signer<crypto::Signature>,
        Signer: crypto::signature::Verifier<crypto::Signature>;
}

impl<T, S> ReadStorage for T
where
    T: Deref<Target = S>,
    S: ReadStorage + 'static,
{
    type Repository = S::Repository;

    fn info(&self) -> &UserInfo {
        self.deref().info()
    }

    fn path(&self) -> &Path {
        self.deref().path()
    }

    fn path_of(&self, rid: &RepoId) -> PathBuf {
        self.deref().path_of(rid)
    }

    fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError> {
        self.deref().contains(rid)
    }

    fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError> {
        self.deref().get(rid)
    }

    fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError> {
        self.deref().repository(rid)
    }

    fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
        self.deref().repositories()
    }
}

impl<T, S> WriteStorage for T
where
    T: Deref<Target = S>,
    S: WriteStorage + 'static,
{
    type RepositoryMut = S::RepositoryMut;

    fn repository_mut(&self, rid: RepoId) -> Result<Self::RepositoryMut, RepositoryError> {
        self.deref().repository_mut(rid)
    }

    fn create(&self, rid: RepoId) -> Result<Self::RepositoryMut, Error> {
        self.deref().create(rid)
    }

    fn clean(&self, rid: RepoId) -> Result<Vec<RemoteId>, RepositoryError> {
        self.deref().clean(rid)
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_storage() {}
}