Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Migrate `sigrefs` to use `git::repository`
Draft fintohaps opened 16 days ago

This set of changes migrates the sigrefs code to use the traits defined in git::repository.

Each commit migrates a single trait, and ensures that all tests pass at each step.

The final commit then removes the now unused code.

16 files changed +391 -686 e1867632 33fd4967
modified crates/radicle/src/storage.rs
@@ -436,7 +436,7 @@ impl Deref for Remote {

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

    /// Get user info for this storage.
    fn info(&self) -> &UserInfo;
modified crates/radicle/src/storage/git.rs
@@ -32,7 +32,7 @@ use crate::git::fmt::{
};
pub use crate::storage::{Error, RepositoryError};

-
use super::refs::{RefsAt, sigrefs};
+
use super::refs::RefsAt;
use super::{RemoteId, RemoteRepository, ValidateRepository};

pub static NAMESPACES_GLOB: LazyLock<PatternString> =
@@ -1074,56 +1074,127 @@ impl Repository {
    }
}

-
impl sigrefs::git::object::Reader for Repository {
-
    fn read_commit(
+
impl crate::git::repository::ObjectReader for Repository {
+
    fn blob(
        &self,
-
        oid: &Oid,
-
    ) -> Result<Option<Vec<u8>>, sigrefs::git::object::error::ReadCommit> {
-
        self.backend.read_commit(oid)
+
        oid: Oid,
+
    ) -> Result<Option<git::repository::Blob>, git::repository::object::error::read::BlobError>
+
    {
+
        crate::git::repository::ObjectReader::blob(&self.backend, oid)
+
    }
+

+
    fn blob_at<P>(
+
        &self,
+
        commit: Oid,
+
        path: &P,
+
    ) -> Result<Option<git::repository::Blob>, git::repository::object::error::read::BlobAtError>
+
    where
+
        P: AsRef<Path>,
+
    {
+
        crate::git::repository::ObjectReader::blob_at(&self.backend, commit, path)
    }

-
    fn read_blob(
+
    fn commit(
        &self,
-
        commit: &Oid,
-
        path: &Path,
-
    ) -> Result<Option<sigrefs::git::object::Blob>, sigrefs::git::object::error::ReadBlob> {
-
        self.backend.read_blob(commit, path)
+
        oid: Oid,
+
    ) -> Result<Option<git::repository::Commit>, git::repository::object::error::read::CommitError>
+
    {
+
        crate::git::repository::ObjectReader::commit(&self.backend, oid)
+
    }
+

+
    fn exists(&self, oid: Oid) -> Result<bool, git::repository::object::error::read::ExistsError> {
+
        crate::git::repository::ObjectReader::exists(&self.backend, oid)
    }
}

-
impl sigrefs::git::object::Writer for Repository {
+
impl crate::git::repository::ObjectWriter for Repository {
+
    fn write_blob(
+
        &self,
+
        content: &[u8],
+
    ) -> Result<Oid, crate::git::repository::object::error::write::BlobError> {
+
        crate::git::repository::ObjectWriter::write_blob(&self.backend, content)
+
    }
+

    fn write_tree(
        &self,
-
        refs: sigrefs::git::object::RefsEntry,
-
        signature: sigrefs::git::object::SignatureEntry,
-
    ) -> Result<Oid, sigrefs::git::object::error::WriteTree> {
-
        self.backend.write_tree(refs, signature)
+
        entries: &[crate::git::repository::types::TreeEntry],
+
    ) -> Result<Oid, crate::git::repository::object::error::write::TreeError> {
+
        crate::git::repository::ObjectWriter::write_tree(&self.backend, entries)
+
    }
+

+
    fn write_commit(
+
        &self,
+
        bytes: &[u8],
+
    ) -> Result<Oid, crate::git::repository::object::error::write::CommitError> {
+
        crate::git::repository::ObjectWriter::write_commit(&self.backend, bytes)
+
    }
+
}
+

+
impl crate::git::repository::RefWriter for Repository {
+
    fn write_ref<R>(
+
        &self,
+
        name: &R,
+
        target: crate::git::repository::RefTarget,
+
        reflog: &str,
+
    ) -> Result<(), crate::git::repository::reference::error::write::WriteRefError>
+
    where
+
        R: AsRef<git::fmt::RefStr>,
+
    {
+
        crate::git::repository::RefWriter::write_ref(&self.backend, name, target, reflog)
    }

-
    fn write_commit(&self, bytes: &[u8]) -> Result<Oid, sigrefs::git::object::error::WriteCommit> {
-
        self.backend.write_commit(bytes)
+
    fn delete_ref<R>(
+
        &self,
+
        name: &R,
+
    ) -> Result<(), crate::git::repository::reference::error::write::DeleteRefError>
+
    where
+
        R: AsRef<git::fmt::RefStr>,
+
    {
+
        crate::git::repository::RefWriter::delete_ref(&self.backend, name)
    }
}

-
impl sigrefs::git::reference::Reader for Repository {
-
    fn find_reference(
+
impl crate::git::repository::SymbolicRefWriter for Repository {
+
    fn write_symbolic_ref<R>(
        &self,
-
        reference: &git::fmt::Namespaced,
-
    ) -> Result<Option<Oid>, sigrefs::git::reference::error::FindReference> {
-
        sigrefs::git::reference::Reader::find_reference(&self.backend, reference)
+
        name: &R,
+
        target: git::repository::SymbolicRefTarget,
+
        reflog: &str,
+
    ) -> Result<(), git::repository::reference::error::write::SymbolicWriteError>
+
    where
+
        R: AsRef<radicle_git_ref_format::RefStr>,
+
    {
+
        crate::git::repository::SymbolicRefWriter::write_symbolic_ref(
+
            &self.backend,
+
            name,
+
            target,
+
            reflog,
+
        )
    }
}

-
impl sigrefs::git::reference::Writer for Repository {
-
    fn write_reference(
+
impl crate::git::repository::RefReader for Repository {
+
    type References<'a> =
+
        <git::raw::Repository as crate::git::repository::RefReader>::References<'a>;
+

+
    fn ref_target<R>(
        &self,
-
        reference: &git::fmt::Namespaced,
-
        commit: Oid,
-
        parent: Option<Oid>,
-
        reflog: String,
-
    ) -> Result<(), sigrefs::git::reference::error::WriteReference> {
-
        self.backend
-
            .write_reference(reference, commit, parent, reflog)
+
        name: &R,
+
    ) -> Result<Option<Oid>, crate::git::repository::reference::error::read::RefTargetError>
+
    where
+
        R: AsRef<git::fmt::RefStr>,
+
    {
+
        crate::git::repository::RefReader::ref_target(&self.backend, name)
+
    }
+

+
    fn list_refs<'a, P>(
+
        &'a self,
+
        pattern: &P,
+
    ) -> Result<Self::References<'a>, crate::git::repository::reference::error::read::ListRefsError>
+
    where
+
        P: AsRef<git::fmt::refspec::PatternStr>,
+
    {
+
        crate::git::repository::RefReader::list_refs(&self.backend, pattern)
    }
}

modified crates/radicle/src/storage/refs.rs
@@ -19,6 +19,7 @@ use thiserror::Error;
use crate::git;
use crate::git::Oid;
use crate::git::raw::ErrorExt as _;
+
use crate::git::repository::{ObjectReader, ObjectWriter, RefReader, RefWriter};
use crate::storage;
use crate::storage::RemoteId;
use crate::storage::refs::sigrefs::read::Tip;
@@ -78,8 +79,8 @@ impl Refs {
        signer: &S,
    ) -> Result<SignedRefs, Error>
    where
-
        R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
-
        R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+
        R: ObjectReader + ObjectWriter,
+
        R: RefReader + RefWriter,
        R: HasRepoId,
        S: signature::Signer<crypto::Signature>,
        S: signature::Verifier<crypto::Signature>,
@@ -96,8 +97,8 @@ impl Refs {
        signer: &S,
    ) -> Result<SignedRefs, Error>
    where
-
        R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
-
        R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+
        R: ObjectReader + ObjectWriter,
+
        R: RefReader + RefWriter,
        R: HasRepoId,
        S: signature::Signer<crypto::Signature>,
        S: signature::Verifier<crypto::Signature>,
@@ -114,8 +115,8 @@ impl Refs {
        force: bool,
    ) -> Result<SignedRefs, Error>
    where
-
        R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
-
        R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
+
        R: ObjectReader + ObjectWriter,
+
        R: RefReader + RefWriter,
        R: HasRepoId,
        S: signature::Signer<crypto::Signature>,
        S: signature::Verifier<crypto::Signature>,
@@ -348,10 +349,10 @@ pub struct RefsAt {
impl RefsAt {
    pub fn new<R>(repo: &R, remote: RemoteId) -> Result<Self, sigrefs::read::error::Read>
    where
-
        R: sigrefs::git::reference::Reader,
+
        R: RefReader,
    {
        let at = repo
-
            .find_reference(
+
            .ref_target(
                &storage::refs::SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(&remote)),
            )
            .map_err(sigrefs::read::error::Read::FindReference)?
@@ -425,7 +426,7 @@ impl SignedRefs {
    pub fn load<R>(remote: RemoteId, repo: &R) -> Result<Option<Self>, sigrefs::read::error::Read>
    where
        R: HasRepoId,
-
        R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
+
        R: ObjectReader + RefReader,
    {
        Self::load_internal(remote, repo, sigrefs::read::Tip::Reference(remote))
    }
@@ -437,7 +438,7 @@ impl SignedRefs {
    ) -> Result<Option<Self>, sigrefs::read::error::Read>
    where
        R: HasRepoId,
-
        R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
+
        R: ObjectReader + RefReader,
    {
        Self::load_internal(remote, repo, sigrefs::read::Tip::Commit(oid))
    }
@@ -449,7 +450,7 @@ impl SignedRefs {
    ) -> Result<Option<Self>, sigrefs::read::error::Read>
    where
        R: HasRepoId,
-
        R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
+
        R: ObjectReader + RefReader,
    {
        let root = repo.rid();
        match sigrefs::SignedRefsReader::new(root, tip, repo, &remote).read() {
modified crates/radicle/src/storage/refs/sigrefs/git.rs
@@ -2,13 +2,52 @@
//! This module provides traits for interacting with a Git
//! repository to read and write data for Signed References.

-
pub mod object;
-
pub mod reference;
+
use std::path::Path;

use crypto::PublicKey;
use radicle_git_metadata::author::Author;
use radicle_git_metadata::author::Time;

+
use crate::git::repository::types::TreeEntry;
+

+
/// A [`TreeEntry`] for the signed references payload blob.
+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+
pub struct RefsEntry(TreeEntry);
+

+
impl RefsEntry {
+
    /// Create a new entry with the canonical refs bytes.
+
    pub fn new(content: Vec<u8>) -> Self {
+
        Self(TreeEntry::Blob {
+
            path: Path::new(crate::storage::refs::REFS_BLOB_PATH).into(),
+
            content,
+
        })
+
    }
+

+
    /// Unwrap into the underlying [`TreeEntry`].
+
    pub fn into_inner(self) -> TreeEntry {
+
        self.0
+
    }
+
}
+

+
/// A [`TreeEntry`] for the cryptographic signature blob.
+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+
pub struct SignatureEntry(TreeEntry);
+

+
impl SignatureEntry {
+
    /// Create a new entry with the signature bytes.
+
    pub fn new(content: Vec<u8>) -> Self {
+
        Self(TreeEntry::Blob {
+
            path: Path::new(crate::storage::refs::SIGNATURE_BLOB_PATH).into(),
+
            content,
+
        })
+
    }
+

+
    /// Unwrap into the underlying [`TreeEntry`].
+
    pub fn into_inner(self) -> TreeEntry {
+
        self.0
+
    }
+
}
+

/// Convenience type that corresponds to an [`Author`].
///
/// Most users will want to instantiate this via [`Committer::from_env_or_now`],
@@ -102,162 +141,3 @@ impl Committer {
        })
    }
}
-

-
mod git2_impls {
-
    //! [`git2::Repository`] implementations of the [`object`] and [`reference`] traits.
-
    //!
-
    //! [`object`]: super::object
-
    //! [`reference`]: super::reference
-

-
    use std::path::Path;
-

-
    use radicle_oid::Oid;
-

-
    use crate::git;
-

-
    use super::object;
-
    use super::object::{RefsEntry, SignatureEntry};
-
    use super::reference;
-

-
    impl object::Reader for git2::Repository {
-
        fn read_commit(&self, oid: &Oid) -> Result<Option<Vec<u8>>, object::error::ReadCommit> {
-
            use object::error::ReadCommit;
-

-
            let odb = self.odb().map_err(ReadCommit::other)?;
-
            let object = odb.read(git2::Oid::from(*oid));
-
            match object {
-
                Ok(object) => {
-
                    if object.kind() != git2::ObjectType::Commit {
-
                        return Err(ReadCommit::incorrect_object_error(*oid, object.kind()));
-
                    }
-
                    Ok(Some(object.data().to_vec()))
-
                }
-
                Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None),
-
                Err(e) => Err(ReadCommit::other(e)),
-
            }
-
        }
-

-
        fn read_blob(
-
            &self,
-
            oid: &Oid,
-
            path: &Path,
-
        ) -> Result<Option<object::Blob>, object::error::ReadBlob> {
-
            use object::error::ReadBlob;
-

-
            let commit = match self.find_commit(git2::Oid::from(*oid)) {
-
                Ok(c) => c,
-
                Err(e) if e.code() == git2::ErrorCode::NotFound => {
-
                    return Err(ReadBlob::commit_not_found_error(*oid));
-
                }
-
                Err(e) => return Err(ReadBlob::other(e)),
-
            };
-

-
            let tree = commit.tree().map_err(ReadBlob::other)?;
-

-
            let entry = match tree.get_path(path) {
-
                Ok(e) => e,
-
                Err(e) if e.code() == git2::ErrorCode::NotFound => return Ok(None),
-
                Err(e) => return Err(ReadBlob::other(e)),
-
            };
-

-
            let object = entry.to_object(self).map_err(ReadBlob::other)?;
-
            let blob = object.as_blob().ok_or(ReadBlob::incorrect_object_error(
-
                *oid,
-
                path.to_path_buf(),
-
                object.kind().unwrap_or(git2::ObjectType::Any),
-
            ))?;
-

-
            Ok(Some(object::Blob {
-
                oid: blob.id().into(),
-
                bytes: blob.content().to_vec(),
-
            }))
-
        }
-
    }
-

-
    impl object::Writer for git2::Repository {
-
        fn write_tree(
-
            &self,
-
            refs: RefsEntry,
-
            signature: SignatureEntry,
-
        ) -> Result<Oid, object::error::WriteTree> {
-
            use object::error::WriteTree;
-

-
            let odb = self.odb().map_err(WriteTree::write_error)?;
-

-
            let refs_oid = odb
-
                .write(git2::ObjectType::Blob, &refs.content)
-
                .map_err(WriteTree::refs_error)?;
-

-
            let sig_oid = odb
-
                .write(git2::ObjectType::Blob, &signature.content)
-
                .map_err(WriteTree::signature_error)?;
-

-
            let mut builder = self.treebuilder(None).map_err(WriteTree::write_error)?;
-

-
            builder
-
                .insert(&refs.path, refs_oid, git2::FileMode::Blob.into())
-
                .map_err(WriteTree::refs_error)?;
-

-
            builder
-
                .insert(&signature.path, sig_oid, git2::FileMode::Blob.into())
-
                .map_err(WriteTree::signature_error)?;
-

-
            let tree_oid = builder.write().map_err(WriteTree::write_error)?;
-

-
            Ok(Oid::from(tree_oid))
-
        }
-

-
        fn write_commit(&self, bytes: &[u8]) -> Result<Oid, object::error::WriteCommit> {
-
            use object::error::WriteCommit;
-

-
            let odb = self.odb().map_err(WriteCommit::other)?;
-

-
            let oid = odb
-
                .write(git2::ObjectType::Commit, bytes)
-
                .map_err(WriteCommit::other)?;
-

-
            Ok(Oid::from(oid))
-
        }
-
    }
-

-
    impl reference::Reader for git2::Repository {
-
        fn find_reference(
-
            &self,
-
            reference: &git::fmt::Namespaced,
-
        ) -> Result<Option<Oid>, reference::error::FindReference> {
-
            match self.refname_to_id(reference.as_str()) {
-
                Ok(oid) => Ok(Some(Oid::from(oid))),
-
                Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None),
-
                Err(e) => Err(reference::error::FindReference::other(e)),
-
            }
-
        }
-
    }
-

-
    impl reference::Writer for git2::Repository {
-
        fn write_reference(
-
            &self,
-
            reference: &git::fmt::Namespaced,
-
            commit: Oid,
-
            parent: Option<Oid>,
-
            reflog: String,
-
        ) -> Result<(), reference::error::WriteReference> {
-
            let new = git2::Oid::from(commit);
-

-
            match parent {
-
                Some(parent) => {
-
                    let old = git2::Oid::from(parent);
-
                    // The old OID provides a guard, which gives us a compare-and-swap —
-
                    // the write will fail if the ref has moved since we read it.
-
                    self.reference_matching(reference.as_str(), new, true, old, &reflog)
-
                        .map_err(reference::error::WriteReference::other)?;
-
                }
-
                None => {
-
                    self.reference(reference.as_str(), new, false, &reflog)
-
                        .map_err(reference::error::WriteReference::other)?;
-
                }
-
            }
-

-
            Ok(())
-
        }
-
    }
-
}
deleted crates/radicle/src/storage/refs/sigrefs/git/object.rs
@@ -1,102 +0,0 @@
-
//! Traits for interacting with Git objects, necessary for implementing Radicle
-
//! Signed References.
-
// TODO(finto): I think these are more generally useful than just being used for
-
// Signed References. They might be worth moving into a crate,
-
// `radicle-git-traits`, but for now they can live here.
-

-
pub mod error;
-

-
use std::path::{Path, PathBuf};
-

-
use radicle_oid::Oid;
-

-
/// A Git blob object, returned by [`Reader::read_blob`].
-
pub struct Blob {
-
    /// The [`Oid`] of the Git blob.
-
    pub oid: Oid,
-
    /// The contents of the Git blob.
-
    pub bytes: Vec<u8>,
-
}
-

-
/// Git object reader, generally a Git repository, or its corresponding Object
-
/// Database (ODB).
-
pub trait Reader {
-
    /// Read the raw bytes of a commit object identified by `oid`.
-
    ///
-
    /// Returns `None` if no such object exists.
-
    ///
-
    /// # Errors
-
    ///
-
    /// - [`error::ReadCommit::IncorrectObject`]: the object identified by the
-
    ///   [`Oid`] was found, but was not a commit.
-
    /// - [`error::ReadCommit::Other`]: failed to read the Git commit.
-
    fn read_commit(&self, oid: &Oid) -> Result<Option<Vec<u8>>, error::ReadCommit>;
-

-
    /// Read the raw bytes of the blob at `path` within the tree of `commit`.
-
    ///
-
    /// Returns `None` if the path does not exist in that tree.
-
    ///
-
    /// # Errors
-
    ///
-
    /// - [`error::ReadBlob::CommitNotFound`]: failed to find the commit
-
    ///   identified by the [`Oid`].
-
    /// - [`error::ReadBlob::IncorrectObject`]: the object identified by the
-
    ///   [`Oid`] was found, but was not a commit.
-
    /// - [`error::ReadBlob::Other`]: failed to read the Git blob.
-
    fn read_blob(&self, commit: &Oid, path: &Path) -> Result<Option<Blob>, error::ReadBlob>;
-
}
-

-
/// Input to the [`Writer::write_tree`] method.
-
///
-
/// The entry describes where in the Git tree to write the [`Refs`] content
-
/// blob.
-
///
-
/// [`Refs`]: crate::storage::refs::Refs
-
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
-
pub struct RefsEntry {
-
    /// Path in the Git tree to write to.
-
    pub path: PathBuf,
-
    /// The contents of the Git blob.
-
    pub content: Vec<u8>,
-
}
-

-
/// Input to the [`Writer::write_tree`] method.
-
///
-
/// The entry describes where in the Git tree to write the signature content
-
/// blob.
-
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
-
pub struct SignatureEntry {
-
    /// Path in the Git tree to write to.
-
    pub path: PathBuf,
-
    /// The contents of the Git blob.
-
    pub content: Vec<u8>,
-
}
-

-
/// Git object writer, generally a Git repository, or its corresponding Object
-
/// Database (ODB).
-
pub trait Writer {
-
    /// Write the [`RefsEntry`] and [`SignatureEntry`] to two separate Git blobs
-
    /// within a shared Git tree.
-
    ///
-
    /// Returns the [`Oid`] of the Git tree.
-
    ///
-
    /// # Errors
-
    ///
-
    /// - [`error::WriteTree::Refs`]: failed to write the references Git blob.
-
    /// - [`error::WriteTree::Signature`]: failed to write the signature Git blob.
-
    /// - [`error::WriteTree::Write`]: failed to write the Git tree.
-
    fn write_tree(
-
        &self,
-
        refs: RefsEntry,
-
        signature: SignatureEntry,
-
    ) -> Result<Oid, error::WriteTree>;
-

-
    /// Write the given Git commit, as bytes, to the Git object database.
-
    ///
-
    /// Returns the [`Oid`] of the Git commit.
-
    ///
-
    /// # Errors
-
    ///
-
    /// - [`error::WriteCommit`]: failed to write the Git commit.
-
    fn write_commit(&self, bytes: &[u8]) -> Result<Oid, error::WriteCommit>;
-
}
deleted crates/radicle/src/storage/refs/sigrefs/git/object/error.rs
@@ -1,146 +0,0 @@
-
use std::path::PathBuf;
-

-
use radicle_oid::Oid;
-
use thiserror::Error;
-

-
type StdError = dyn std::error::Error + Send + Sync + 'static;
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum ReadCommit {
-
    #[error(transparent)]
-
    IncorrectObject(NotCommit),
-
    #[error(transparent)]
-
    Other(Box<StdError>),
-
}
-

-
impl ReadCommit {
-
    pub fn incorrect_object_error<K>(oid: Oid, kind: K) -> Self
-
    where
-
        K: ToString,
-
    {
-
        Self::IncorrectObject(NotCommit {
-
            oid,
-
            kind: kind.to_string(),
-
        })
-
    }
-

-
    pub fn other<E>(err: E) -> Self
-
    where
-
        E: std::error::Error + Send + Sync + 'static,
-
    {
-
        Self::Other(Box::new(err))
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
#[error("the object {oid} is a {kind}, not a commit")]
-
pub struct NotCommit {
-
    oid: Oid,
-
    kind: String,
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
#[error(transparent)]
-
pub enum ReadBlob {
-
    #[error(transparent)]
-
    CommitNotFound(CommitNotFound),
-
    #[error(transparent)]
-
    IncorrectObject(NotBlob),
-
    #[error(transparent)]
-
    Other(Box<StdError>),
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
#[error("could not find commit {oid}")]
-
pub struct CommitNotFound {
-
    oid: Oid,
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
#[error("the object at {path:?} in commit {commit} is a {kind}, not a blob")]
-
pub struct NotBlob {
-
    commit: Oid,
-
    path: PathBuf,
-
    kind: String,
-
}
-

-
impl ReadBlob {
-
    pub fn commit_not_found_error(oid: Oid) -> Self {
-
        Self::CommitNotFound(CommitNotFound { oid })
-
    }
-

-
    pub fn incorrect_object_error<K>(commit: Oid, path: PathBuf, kind: K) -> Self
-
    where
-
        K: ToString,
-
    {
-
        Self::IncorrectObject(NotBlob {
-
            commit,
-
            path,
-
            kind: kind.to_string(),
-
        })
-
    }
-

-
    pub fn other<E>(err: E) -> Self
-
    where
-
        E: std::error::Error + Send + Sync + 'static,
-
    {
-
        Self::Other(Box::new(err))
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum WriteTree {
-
    #[error("failed to write reference blob for signed references")]
-
    Refs(Box<StdError>),
-
    #[error("failed to write signature blob for signed references")]
-
    Signature(Box<StdError>),
-
    #[error(transparent)]
-
    Write(Box<StdError>),
-
}
-

-
impl WriteTree {
-
    pub fn refs_error<E>(err: E) -> Self
-
    where
-
        E: std::error::Error + Send + Sync + 'static,
-
    {
-
        Self::Refs(Box::new(err))
-
    }
-

-
    pub fn signature_error<E>(err: E) -> Self
-
    where
-
        E: std::error::Error + Send + Sync + 'static,
-
    {
-
        Self::Signature(Box::new(err))
-
    }
-

-
    pub fn write_error<E>(err: E) -> Self
-
    where
-
        E: std::error::Error + Send + Sync + 'static,
-
    {
-
        Self::Write(Box::new(err))
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
#[error(transparent)]
-
pub struct WriteCommit {
-
    source: Box<StdError>,
-
}
-

-
impl WriteCommit {
-
    pub fn other<E>(err: E) -> Self
-
    where
-
        E: std::error::Error + Send + Sync + 'static,
-
    {
-
        Self {
-
            source: Box::new(err),
-
        }
-
    }
-
}
deleted crates/radicle/src/storage/refs/sigrefs/git/reference.rs
@@ -1,51 +0,0 @@
-
//! Traits for interacting with Git references, necessary for implementing
-
//! Radicle Signed References.
-
// TODO(finto): I think these are more generally useful than just being used for
-
// Signed References. They might be worth moving into a crate,
-
// `radicle-git-traits`, but for now they can live here.
-

-
pub mod error;
-

-
use radicle_oid::Oid;
-

-
use crate::git;
-

-
/// Git reference reader, generally a Git repository, or its corresponding Reference
-
/// Database (Ref DB).
-
pub trait Reader {
-
    /// Find the head [`Oid`] of the sigrefs reference for the given namespace.
-
    ///
-
    /// Returns `None` if the reference does not yet exist.
-
    /// # Errors
-
    ///
-
    /// - [`error::FindReference`]: failed to write the Git reference.
-
    fn find_reference(
-
        &self,
-
        reference: &git::fmt::Namespaced,
-
    ) -> Result<Option<Oid>, error::FindReference>;
-
}
-

-
/// Git reference writer, generally a Git repository, or its corresponding Reference
-
/// Database (Ref DB).
-
pub trait Writer {
-
    /// Write the given commit [`Oid`], and its parent, to the given
-
    /// `reference`.
-
    ///
-
    /// The `reflog` given can used as the Git reflog message of the reference.
-
    ///
-
    /// # Concurrency
-
    ///
-
    /// It is up to the implementer to ensure the safety of writing the
-
    /// reference safely in a concurrent environment.
-
    ///
-
    /// # Errors
-
    ///
-
    /// - [`error::WriteReference`]: failed to write the Git reference.
-
    fn write_reference(
-
        &self,
-
        reference: &git::fmt::Namespaced,
-
        commit: Oid,
-
        parent: Option<Oid>,
-
        reflog: String,
-
    ) -> Result<(), error::WriteReference>;
-
}
deleted crates/radicle/src/storage/refs/sigrefs/git/reference/error.rs
@@ -1,29 +0,0 @@
-
use thiserror::Error;
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
#[error(transparent)]
-
pub struct FindReference(Box<dyn std::error::Error + Send + Sync + 'static>);
-

-
impl FindReference {
-
    pub fn other<E>(err: E) -> Self
-
    where
-
        E: std::error::Error + Send + Sync + 'static,
-
    {
-
        Self(Box::new(err))
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
#[error(transparent)]
-
pub struct WriteReference(Box<dyn std::error::Error + Send + Sync + 'static>);
-

-
impl WriteReference {
-
    pub fn other<E>(err: E) -> Self
-
    where
-
        E: std::error::Error + Send + Sync + 'static,
-
    {
-
        Self(Box::new(err))
-
    }
-
}
modified crates/radicle/src/storage/refs/sigrefs/read.rs
@@ -16,8 +16,8 @@ use radicle_git_metadata::commit::CommitData;
use radicle_oid::Oid;

use crate::git;
+
use crate::git::repository::{ObjectReader, RefReader};
use crate::identity::doc;
-
use crate::storage::refs::sigrefs::git::{object, reference};
use crate::storage::refs::{
    FeatureLevel, IDENTITY_ROOT, REFS_BLOB_PATH, Refs, SIGNATURE_BLOB_PATH, SIGREFS_BRANCH,
    SignedRefs,
@@ -128,7 +128,7 @@ impl std::fmt::Display for FeatureLevels {

impl<'a, R, V> SignedRefsReader<'a, R, V>
where
-
    R: object::Reader + reference::Reader,
+
    R: ObjectReader + RefReader,
    V: signature::Verifier<crypto::Signature>,
{
    /// Construct a new [`SignedRefsReader`].
@@ -304,7 +304,7 @@ where
                    SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(&namespace));
                let head = self
                    .repository
-
                    .find_reference(&reference)
+
                    .ref_target(&reference)
                    .map_err(error::Read::FindReference)?
                    .ok_or_else(|| error::Read::MissingSigrefs { namespace })?;
                Ok(head)
@@ -440,7 +440,7 @@ pub(super) struct CommitReader<'a, R> {

impl<'a, R> CommitReader<'a, R>
where
-
    R: object::Reader,
+
    R: ObjectReader,
{
    pub(super) fn new(commit: Oid, repository: &'a R) -> Self {
        Self { commit, repository }
@@ -466,15 +466,10 @@ where
    }

    fn read_commit_data(&self) -> Result<CommitData<Oid, Oid>, error::Commit> {
-
        let bytes = self
-
            .repository
-
            .read_commit(&self.commit)
+
        self.repository
+
            .commit(self.commit)
            .map_err(error::Commit::Read)?
-
            .ok_or(error::Commit::Missing { oid: self.commit })?;
-
        CommitData::from_bytes(&bytes).map_err(|err| error::Commit::Parse {
-
            oid: self.commit,
-
            source: err,
-
        })
+
            .ok_or(error::Commit::Missing { oid: self.commit })
    }

    /// Extract the single parent [`Oid`] from a [`CommitData`], if any.
@@ -509,7 +504,7 @@ struct TreeReader<'a, R> {

impl<'a, R> TreeReader<'a, R>
where
-
    R: object::Reader,
+
    R: ObjectReader,
{
    fn new(commit: Oid, repository: &'a R) -> Self {
        Self { commit, repository }
@@ -517,40 +512,42 @@ where

    fn read(self) -> Result<Tree, error::Tree> {
        let (refs, signature) = self.try_handle_blobs()?;
-
        let refs = Refs::from_canonical(&refs.bytes).map_err(error::Tree::ParseRefs)?;
-
        let signature = crypto::Signature::try_from(signature.bytes.as_slice())
+
        let refs = Refs::from_canonical(&refs.content).map_err(error::Tree::ParseRefs)?;
+
        let signature = crypto::Signature::try_from(signature.content.as_slice())
            .map_err(error::Tree::ParseSignature)?;
        Ok(Tree { refs, signature })
    }

    /// Fetch the refs blob and signature blob from the repository, returning a
    /// descriptive error if either or both are missing.
-
    fn try_handle_blobs(&self) -> Result<(object::Blob, object::Blob), error::Tree> {
-
        let commit = &self.commit;
+
    fn try_handle_blobs(
+
        &self,
+
    ) -> Result<(git::repository::Blob, git::repository::Blob), error::Tree> {
+
        let commit = self.commit;
        let refs_path = Path::new(REFS_BLOB_PATH);
        let sig_path = Path::new(SIGNATURE_BLOB_PATH);

-
        let refs_bytes = self
+
        let refs_blob = self
            .repository
-
            .read_blob(commit, refs_path)
+
            .blob_at(commit, &refs_path)
            .map_err(error::Tree::Refs)?;
-
        let sig_bytes = self
+
        let sig_blob = self
            .repository
-
            .read_blob(commit, sig_path)
+
            .blob_at(commit, &sig_path)
            .map_err(error::Tree::Signature)?;

-
        let result = match (refs_bytes, sig_bytes) {
+
        let result = match (refs_blob, sig_blob) {
            (None, None) => Err(error::MissingBlobs::Both {
-
                commit: *commit,
+
                commit,
                refs: refs_path.to_path_buf(),
                signature: sig_path.to_path_buf(),
            }),
            (None, Some(_)) => Err(error::MissingBlobs::Signature {
-
                commit: *commit,
+
                commit,
                path: sig_path.to_path_buf(),
            }),
            (Some(_), None) => Err(error::MissingBlobs::Refs {
-
                commit: *commit,
+
                commit,
                path: refs_path.to_path_buf(),
            }),
            (Some(refs), Some(sig)) => Ok((refs, sig)),
@@ -573,7 +570,7 @@ struct IdentityRootReader<'a, 'b, R> {

impl<'a, 'b, R> IdentityRootReader<'a, 'b, R>
where
-
    R: object::Reader,
+
    R: ObjectReader,
{
    fn new(refs: &'a Refs, repository: &'b R) -> Self {
        Self { refs, repository }
@@ -590,11 +587,11 @@ where

    fn read_blob(&self, commit: &Oid) -> Result<RepoId, error::IdentityRoot> {
        let path = Path::new("embeds").join(*doc::PATH);
-
        let object::Blob { oid, .. } = self
+
        let blob = self
            .repository
-
            .read_blob(commit, &path)
+
            .blob_at(*commit, &path)
            .map_err(error::IdentityRoot::Blob)?
            .ok_or_else(|| error::IdentityRoot::MissingIdentity { commit: *commit })?;
-
        Ok(RepoId::from(oid))
+
        Ok(RepoId::from(blob.oid))
    }
}
modified crates/radicle/src/storage/refs/sigrefs/read/error.rs
@@ -6,7 +6,8 @@ use radicle_git_metadata::commit;
use radicle_oid::Oid;
use thiserror::Error;

-
use crate::storage::refs::sigrefs::git::{object, reference};
+
use crate::git::repository::object;
+
use crate::git::repository::reference;
use crate::storage::refs::sigrefs::read::FeatureLevels;
use crate::storage::refs::{FeatureLevel, canonical};

@@ -16,7 +17,7 @@ pub enum Read {
    #[error(transparent)]
    Commit(Commit),
    #[error(transparent)]
-
    FindReference(reference::error::FindReference),
+
    FindReference(reference::error::read::RefTargetError),
    #[error("failed to find `refs/namespaces/{namespace}/refs/rad/sigrefs`")]
    MissingSigrefs { namespace: NodeId },
    #[error(transparent)]
@@ -48,7 +49,7 @@ pub enum Commit {
    #[error(transparent)]
    TooManyParents(Parent),
    #[error(transparent)]
-
    Read(object::error::ReadCommit),
+
    Read(object::error::read::CommitError),
}

#[derive(Debug, Error)]
@@ -66,7 +67,7 @@ impl fmt::Display for Parent {
                .iter()
                .map(|oid| oid.to_string())
                .collect::<Vec<_>>()
-
                .join(", ")
+
                .join(", "),
        )
    }
}
@@ -75,9 +76,9 @@ impl fmt::Display for Parent {
#[non_exhaustive]
pub enum Tree {
    #[error(transparent)]
-
    Refs(object::error::ReadBlob),
+
    Refs(object::error::read::BlobAtError),
    #[error(transparent)]
-
    Signature(object::error::ReadBlob),
+
    Signature(object::error::read::BlobAtError),
    #[error(transparent)]
    ParseRefs(canonical::Error),
    #[error(transparent)]
@@ -104,7 +105,7 @@ pub enum MissingBlobs {
#[non_exhaustive]
pub enum IdentityRoot {
    #[error(transparent)]
-
    Blob(object::error::ReadBlob),
+
    Blob(object::error::read::BlobAtError),
    #[error("missing repository identity commit '{commit}'")]
    MissingIdentity { commit: Oid },
}
modified crates/radicle/src/storage/refs/sigrefs/read/iter.rs
@@ -1,6 +1,6 @@
use radicle_oid::Oid;

-
use crate::storage::refs::sigrefs::git::object;
+
use crate::git::repository::ObjectReader;

use super::{Commit, CommitReader, error};

@@ -18,7 +18,7 @@ impl<'a, R> Walk<'a, R> {
    }
}

-
impl<'a, R: object::Reader> Iterator for Walk<'a, R> {
+
impl<'a, R: ObjectReader> Iterator for Walk<'a, R> {
    type Item = Result<Commit, error::Commit>;

    fn next(&mut self) -> Option<Self::Item> {
modified crates/radicle/src/storage/refs/sigrefs/read/test/mock.rs
@@ -1,5 +1,7 @@
-
//! Mock implementations of [`object::Reader`] and [`reference::Reader`] for
+
//! Mock implementations of [`ObjectReader`] and [`reference::Reader`] for
//! unit-testing.
+
//!
+
//! [`ObjectReader`]: crate::git::repository::ObjectReader

use std::collections::HashMap;
use std::path::{Path, PathBuf};
@@ -12,13 +14,16 @@ use radicle_git_metadata::commit::trailers::OwnedTrailer;
use radicle_oid::Oid;

use crate::git;
+
use crate::git::repository::object;
+
use crate::git::repository::reference;
+
use crate::git::repository::types::{Blob, Commit};
+
use crate::git::repository::{ObjectReader, RefReader};
use crate::identity::doc;
-
use crate::storage::refs::sigrefs::git::{object, reference};
use crate::storage::refs::{REFS_BLOB_PATH, Refs, SIGNATURE_BLOB_PATH, SIGREFS_BRANCH};

pub(crate) const MOCKED_IDENTITY: u8 = 99u8;

-
/// A configurable in-memory repository implementing [`object::Reader`] and
+
/// A configurable in-memory repository implementing [`ObjectReader`] and
/// [`reference::Reader`].
/// All behaviour is set at construction time via the builder methods; the mock
/// is fully deterministic.
@@ -29,20 +34,20 @@ pub struct MockRepository {
}

enum CommitBehavior {
-
    /// [`object::Reader::read_commit`] returns `Ok(Some(bytes))`.
+
    /// [`ObjectReader::commit`] returns `Ok(Some(commit))`.
    Present(Box<CommitData<Oid, Oid>>),
-
    /// [`object::Reader::read_commit`] returns `Ok(None)`.
+
    /// [`ObjectReader::commit`] returns `Ok(None)`.
    Missing,
-
    /// [`object::Reader::read_commit`] returns `Err(…)`.
+
    /// [`ObjectReader::commit`] returns `Err(…)`.
    Error,
}

enum BlobBehavior {
-
    /// [`object::Reader::read_blob`] returns `Ok(Some(blob))`.
+
    /// [`ObjectReader::blob_at`] returns `Ok(Some(blob))`.
    Present(Vec<u8>),
-
    /// [`object::Reader::read_blob`] returns `Ok(None)`.
+
    /// [`ObjectReader::blob_at`] returns `Ok(None)`.
    Missing,
-
    /// [`object::Reader::read_blob`] returns `Err(…)`.
+
    /// [`ObjectReader::blob_at`] returns `Err(…)`.
    Error,
}

@@ -164,53 +169,77 @@ impl MockRepository {
    }
}

-
impl object::Reader for MockRepository {
-
    fn read_commit(&self, oid: &Oid) -> Result<Option<Vec<u8>>, object::error::ReadCommit> {
-
        match self.commits.get(oid) {
-
            Some(CommitBehavior::Present(data)) => Ok(Some(data.to_string().as_bytes().to_vec())),
-
            Some(CommitBehavior::Missing) | None => Ok(None),
-
            Some(CommitBehavior::Error) => Err(object::error::ReadCommit::other(
-
                std::io::Error::other("mock commit error"),
-
            )),
-
        }
+
impl ObjectReader for MockRepository {
+
    fn blob(&self, _oid: Oid) -> Result<Option<Blob>, object::error::read::BlobError> {
+
        unimplemented!("MockRepository::blob")
    }

-
    fn read_blob(
+
    fn blob_at<P: AsRef<Path>>(
        &self,
-
        commit: &Oid,
-
        path: &Path,
-
    ) -> Result<Option<object::Blob>, object::error::ReadBlob> {
-
        let key = (*commit, path.to_path_buf());
+
        commit: Oid,
+
        path: &P,
+
    ) -> Result<Option<Blob>, object::error::read::BlobAtError> {
+
        let key = (commit, path.as_ref().to_path_buf());
        match self.blobs.get(&key) {
-
            Some(BlobBehavior::Present(bytes)) => Ok(Some(object::Blob {
+
            Some(BlobBehavior::Present(bytes)) => Ok(Some(Blob {
                // The blob OID is returned as the commit OID.  This is
                // intentional: IdentityRootReader converts blob.oid into a
                // RepoId, so callers can predict which RepoId results from a
                // given identity-root commit OID.
-
                oid: *commit,
-
                bytes: bytes.clone(),
+
                oid: commit,
+
                content: bytes.clone(),
            })),
            Some(BlobBehavior::Missing) | None => Ok(None),
-
            Some(BlobBehavior::Error) => Err(object::error::ReadBlob::other(
+
            Some(BlobBehavior::Error) => Err(object::error::read::BlobAtError::backend(
                std::io::Error::other("mock blob error"),
            )),
        }
    }
+

+
    fn commit(&self, oid: Oid) -> Result<Option<Commit>, object::error::read::CommitError> {
+
        match self.commits.get(&oid) {
+
            Some(CommitBehavior::Present(data)) => {
+
                let bytes = data.to_string();
+
                let parsed = Commit::from_bytes(bytes.as_bytes())
+
                    .map_err(|e| object::error::read::CommitError::Parse { oid, source: e })?;
+
                Ok(Some(parsed))
+
            }
+
            Some(CommitBehavior::Missing) | None => Ok(None),
+
            Some(CommitBehavior::Error) => Err(object::error::read::CommitError::backend(
+
                std::io::Error::other("mock commit error"),
+
            )),
+
        }
+
    }
+

+
    fn exists(&self, _oid: Oid) -> Result<bool, object::error::read::ExistsError> {
+
        unimplemented!("MockRepository::exists")
+
    }
}

-
impl reference::Reader for MockRepository {
-
    fn find_reference(
+
impl RefReader for MockRepository {
+
    type References<'a> = std::iter::Empty<
+
        Result<(git::fmt::Qualified<'static>, Oid), reference::error::read::ListReferenceError>,
+
    >;
+

+
    fn ref_target<R: AsRef<git::fmt::RefStr>>(
        &self,
-
        reference: &git::fmt::Namespaced,
-
    ) -> Result<Option<Oid>, reference::error::FindReference> {
-
        match self.references.get(reference.as_str()) {
+
        name: &R,
+
    ) -> Result<Option<Oid>, reference::error::read::RefTargetError> {
+
        match self.references.get(name.as_ref().as_str()) {
            Some(RefBehavior::Present(oid)) => Ok(Some(*oid)),
            Some(RefBehavior::Missing) | None => Ok(None),
-
            Some(RefBehavior::Error) => Err(reference::error::FindReference::other(
+
            Some(RefBehavior::Error) => Err(reference::error::read::RefTargetError::backend(
                std::io::Error::other("mock reference error"),
            )),
        }
    }
+

+
    fn list_refs<'a, P: AsRef<git::fmt::refspec::PatternStr>>(
+
        &'a self,
+
        _pattern: &P,
+
    ) -> Result<Self::References<'a>, reference::error::read::ListRefsError> {
+
        unimplemented!("MockRepository::list_refs")
+
    }
}

/// Accepts every (message, signature) pair without inspecting either.
modified crates/radicle/src/storage/refs/sigrefs/write.rs
@@ -3,8 +3,6 @@ pub mod error;
#[cfg(test)]
mod test;

-
use std::path::Path;
-

use crypto::PublicKey;
use crypto::signature::{self, Signer};
use radicle_core::{NodeId, RepoId};
@@ -13,14 +11,14 @@ use radicle_git_metadata::commit::{CommitData, headers::Headers, trailers::Owned
use radicle_oid::Oid;

use crate::git;
+
use crate::git::repository::{ObjectReader, ObjectWriter, RefReader, RefTarget, RefWriter};
use crate::storage::refs::SignedRefs;
-
use crate::storage::refs::sigrefs::git::{Committer, object, reference};
+
use crate::storage::refs::sigrefs::git::Committer;
use crate::storage::refs::sigrefs::read::CommitReader;
use crate::storage::refs::sigrefs::{VerifiedCommit, read};
-
use crate::storage::refs::{
-
    FeatureLevel, IDENTITY_ROOT, REFS_BLOB_PATH, Refs, SIGNATURE_BLOB_PATH, SIGREFS_BRANCH,
-
    SIGREFS_PARENT,
-
};
+
use crate::storage::refs::{FeatureLevel, IDENTITY_ROOT, Refs, SIGREFS_BRANCH, SIGREFS_PARENT};
+

+
use super::git::{RefsEntry, SignatureEntry};

/// The result of attempting to write signed references using
/// [`SignedRefsWriter`].
@@ -72,7 +70,7 @@ pub struct SignedRefsWriter<'a, R, S> {

impl<'a, R, S> SignedRefsWriter<'a, R, S>
where
-
    R: object::Writer + object::Reader + reference::Writer + reference::Reader,
+
    R: ObjectWriter + ObjectWriter + ObjectReader + RefWriter + RefReader,
    S: Signer<crypto::Signature>,
    S: signature::Verifier<crypto::Signature>,
{
@@ -179,8 +177,15 @@ where
            Err(err) => return Err(error::Write::Head(err)),
        };
        let commit = commit_writer.write().map_err(error::Write::Commit)?;
+
        let target = match commit.parent {
+
            Some(expected) => RefTarget::Cas {
+
                target: commit.oid,
+
                expected,
+
            },
+
            None => RefTarget::Create { target: commit.oid },
+
        };
        repository
-
            .write_reference(&reference, commit.oid, commit.parent, reflog)
+
            .write_ref(&reference, target, &reflog)
            .map_err(error::Write::Reference)?;
        Ok(Update::changed(commit, FeatureLevel::Parent))
    }
@@ -227,7 +232,7 @@ struct CommitWriter<'a, R, S> {

impl<'a, R, S> CommitWriter<'a, R, S>
where
-
    R: object::Writer,
+
    R: ObjectWriter,
    S: Signer<crypto::Signature>,
{
    fn root(refs: Refs, author: Author, message: String, repository: &'a R, signer: &'a S) -> Self {
@@ -310,7 +315,7 @@ struct TreeWriter<'a, R, S> {

impl<'a, R, S> TreeWriter<'a, R, S>
where
-
    R: object::Writer,
+
    R: ObjectWriter,
    S: Signer<crypto::Signature>,
{
    fn new(refs: Refs, repository: &'a R, signer: &'a S) -> Self {
@@ -327,17 +332,11 @@ where
            .signer
            .try_sign(&canonical)
            .map_err(error::Tree::Sign)?;
-
        let refs = object::RefsEntry {
-
            path: Path::new(REFS_BLOB_PATH).to_path_buf(),
-
            content: canonical,
-
        };
-
        let sig = object::SignatureEntry {
-
            path: Path::new(SIGNATURE_BLOB_PATH).to_path_buf(),
-
            content: signature.to_vec(),
-
        };
+
        let refs = RefsEntry::new(canonical);
+
        let sig = SignatureEntry::new(signature.to_vec());
        let oid = self
            .repository
-
            .write_tree(refs, sig)
+
            .write_tree(&[refs.into_inner(), sig.into_inner()])
            .map_err(error::Tree::Write)?;
        Ok(Tree {
            oid,
@@ -372,7 +371,7 @@ struct HeadReader<'a, 'b, 'c, R, V> {

impl<'a, 'b, 'c, R, V> HeadReader<'a, 'b, 'c, R, V>
where
-
    R: object::Reader + reference::Reader,
+
    R: ObjectReader + RefReader,
    V: signature::Verifier<crypto::Signature>,
{
    /// Construct a [`HeadReader`] with the `reference` that is being read from
@@ -400,7 +399,7 @@ where
    fn read(self) -> Result<Option<Head>, error::Head> {
        let Some(oid) = self
            .repository
-
            .find_reference(self.reference)
+
            .ref_target(self.reference)
            .map_err(error::Head::Reference)?
        else {
            return Ok(None);
modified crates/radicle/src/storage/refs/sigrefs/write/error.rs
@@ -1,7 +1,8 @@
use radicle_oid::Oid;
use thiserror::Error;

-
use crate::storage::refs::sigrefs::git::{object, reference};
+
use crate::git::repository::object;
+
use crate::git::repository::reference;

// TODO: use commit NID (and RID?) for traceability
#[derive(Debug, Error)]
@@ -12,7 +13,7 @@ pub enum Write {
    #[error(transparent)]
    Commit(Commit),
    #[error(transparent)]
-
    Reference(reference::error::WriteReference),
+
    Reference(reference::error::write::WriteRefError),
}

// TODO: use commit OID for traceability
@@ -22,7 +23,7 @@ pub enum Commit {
    #[error(transparent)]
    Tree(Tree),
    #[error(transparent)]
-
    Write(object::error::WriteCommit),
+
    Write(object::error::write::CommitError),
}

// TODO: use commit OID for traceability
@@ -32,7 +33,7 @@ pub enum Tree {
    #[error("failed to sign references payload")]
    Sign(crypto::signature::Error),
    #[error(transparent)]
-
    Write(object::error::WriteTree),
+
    Write(object::error::write::TreeError),
}

// TODO: use commit OID for traceability
@@ -41,7 +42,7 @@ pub enum Tree {
#[error(transparent)]
pub enum Head {
    #[error(transparent)]
-
    Reference(reference::error::FindReference),
+
    Reference(reference::error::read::RefTargetError),
    #[error(transparent)]
    Commit(super::read::error::Commit),
    #[error("failed to verify commit {commit}: {source}")]
modified crates/radicle/src/storage/refs/sigrefs/write/test/mock.rs
@@ -10,27 +10,30 @@ use radicle_git_metadata::commit::trailers::OwnedTrailer;
use radicle_oid::Oid;

use crate::git;
+
use crate::git::repository::object;
+
use crate::git::repository::reference;
+
use crate::git::repository::types::{Blob, Commit, TreeEntry};
+
use crate::git::repository::{ObjectReader, ObjectWriter, RefReader, RefTarget, RefWriter};
use crate::identity::doc;
use crate::storage::HasRepoId;
-
use crate::storage::refs::sigrefs::git::{object, reference};
use crate::storage::refs::{REFS_BLOB_PATH, Refs, SIGNATURE_BLOB_PATH, SIGREFS_BRANCH};

const MOCKED_IDENTITY: u8 = 99u8;

enum WriteTreeBehavior {
-
    /// [`object::Writer::write_tree`] returns `Ok(oid)`.
+
    /// [`ObjectWriter::write_tree`] returns `Ok(oid)`.
    Ok(Oid),
-
    /// [`object::Writer::write_tree`] returns `Err(…)`.
+
    /// [`ObjectWriter::write_tree`] returns `Err(…)`.
    Error,
}

-
/// [`object::Writer::write_commit`] returns `Ok(oid)`.
+
/// [`ObjectWriter::write_commit`] returns `Ok(oid)`.
struct WriteCommitBehavior(Oid);

enum WriteReferenceBehavior {
-
    /// [`reference::Writer::write_reference`] returns `Ok(())`.
+
    /// [`RefWriter::write_ref`] returns `Ok(())`.
    Ok,
-
    /// [`reference::Writer::write_reference`] returns `Err(…)`.
+
    /// [`RefWriter::write_ref`] returns `Err(…)`.
    Error,
}

@@ -200,89 +203,119 @@ impl HasRepoId for MockRepository {
    }
}

-
impl object::Reader for MockRepository {
-
    fn read_commit(&self, oid: &Oid) -> Result<Option<Vec<u8>>, object::error::ReadCommit> {
-
        match self.commits.get(oid) {
-
            Some(CommitBehavior::Present(data)) => Ok(Some(data.to_string().as_bytes().to_vec())),
-
            None => Ok(None),
-
        }
+
impl ObjectReader for MockRepository {
+
    fn blob(&self, _oid: Oid) -> Result<Option<Blob>, object::error::read::BlobError> {
+
        unimplemented!("MockRepository::blob")
    }

-
    fn read_blob(
+
    fn blob_at<P: AsRef<Path>>(
        &self,
-
        commit: &Oid,
-
        path: &Path,
-
    ) -> Result<Option<object::Blob>, object::error::ReadBlob> {
-
        let key = (*commit, path.to_path_buf());
+
        commit: Oid,
+
        path: &P,
+
    ) -> Result<Option<Blob>, object::error::read::BlobAtError> {
+
        let key = (commit, path.as_ref().to_path_buf());
        match self.blobs.get(&key) {
-
            Some(BlobBehavior::Present(bytes)) => Ok(Some(object::Blob {
-
                oid: *commit,
-
                bytes: bytes.clone(),
+
            Some(BlobBehavior::Present(bytes)) => Ok(Some(Blob {
+
                oid: commit,
+
                content: bytes.clone(),
            })),
            Some(BlobBehavior::Missing) | None => Ok(None),
-
            Some(BlobBehavior::Error) => Err(object::error::ReadBlob::other(
+
            Some(BlobBehavior::Error) => Err(object::error::read::BlobAtError::backend(
                std::io::Error::other("mock blob error"),
            )),
        }
    }
+

+
    fn commit(&self, oid: Oid) -> Result<Option<Commit>, object::error::read::CommitError> {
+
        match self.commits.get(&oid) {
+
            Some(CommitBehavior::Present(data)) => {
+
                let bytes = data.to_string();
+
                let parsed = Commit::from_bytes(bytes.as_bytes())
+
                    .map_err(|e| object::error::read::CommitError::Parse { oid, source: e })?;
+
                Ok(Some(parsed))
+
            }
+
            None => Ok(None),
+
        }
+
    }
+

+
    fn exists(&self, _oid: Oid) -> Result<bool, object::error::read::ExistsError> {
+
        unimplemented!("MockRepository::exists")
+
    }
}

-
impl reference::Reader for MockRepository {
-
    fn find_reference(
+
impl RefReader for MockRepository {
+
    type References<'a> = std::iter::Empty<
+
        Result<(git::fmt::Qualified<'static>, Oid), reference::error::read::ListReferenceError>,
+
    >;
+

+
    fn ref_target<R: AsRef<git::fmt::RefStr>>(
        &self,
-
        reference: &git::fmt::Namespaced,
-
    ) -> Result<Option<Oid>, reference::error::FindReference> {
-
        match self.references.get(reference.as_str()) {
+
        name: &R,
+
    ) -> Result<Option<Oid>, reference::error::read::RefTargetError> {
+
        match self.references.get(name.as_ref().as_str()) {
            Some(RefBehavior::Present(oid)) => Ok(Some(*oid)),
            Some(RefBehavior::Missing) | None => Ok(None),
-
            Some(RefBehavior::Error) => Err(reference::error::FindReference::other(
+
            Some(RefBehavior::Error) => Err(reference::error::read::RefTargetError::backend(
                std::io::Error::other("mock reference error"),
            )),
        }
    }
+

+
    fn list_refs<'a, P: AsRef<git::fmt::refspec::PatternStr>>(
+
        &'a self,
+
        _pattern: &P,
+
    ) -> Result<Self::References<'a>, reference::error::read::ListRefsError> {
+
        unimplemented!("MockRepository::list_refs")
+
    }
}

-
impl object::Writer for MockRepository {
-
    fn write_tree(
-
        &self,
-
        _refs: object::RefsEntry,
-
        _signature: object::SignatureEntry,
-
    ) -> Result<Oid, object::error::WriteTree> {
+
impl ObjectWriter for MockRepository {
+
    fn write_blob(&self, _content: &[u8]) -> Result<Oid, object::error::write::BlobError> {
+
        unimplemented!("MockRepository::write_blob")
+
    }
+

+
    fn write_tree(&self, _entries: &[TreeEntry]) -> Result<Oid, object::error::write::TreeError> {
        match &self.write_tree {
            Some(WriteTreeBehavior::Ok(oid)) => Ok(*oid),
-
            Some(WriteTreeBehavior::Error) | None => Err(object::error::WriteTree::write_error(
+
            Some(WriteTreeBehavior::Error) | None => Err(object::error::write::TreeError::backend(
                std::io::Error::other("mock write_tree error"),
            )),
        }
    }

-
    fn write_commit(&self, _bytes: &[u8]) -> Result<Oid, object::error::WriteCommit> {
+
    fn write_commit(&self, _bytes: &[u8]) -> Result<Oid, object::error::write::CommitError> {
        match &self.write_commit {
            Some(WriteCommitBehavior(oid)) => Ok(*oid),
-
            None => Err(object::error::WriteCommit::other(std::io::Error::other(
-
                "mock write_commit error",
-
            ))),
+
            None => Err(object::error::write::CommitError::backend(
+
                std::io::Error::other("mock write_commit error"),
+
            )),
        }
    }
}

-
impl reference::Writer for MockRepository {
-
    fn write_reference(
+
impl RefWriter for MockRepository {
+
    fn write_ref<R: AsRef<git::fmt::RefStr>>(
        &self,
-
        _reference: &git::fmt::Namespaced,
-
        _commit: Oid,
-
        _parent: Option<Oid>,
-
        _reflog: String,
-
    ) -> Result<(), reference::error::WriteReference> {
+
        _name: &R,
+
        _target: RefTarget,
+
        _reflog: &str,
+
    ) -> Result<(), reference::error::write::WriteRefError> {
        match &self.write_reference {
            Some(WriteReferenceBehavior::Ok) => Ok(()),
            Some(WriteReferenceBehavior::Error) | None => {
-
                Err(reference::error::WriteReference::other(
-
                    std::io::Error::other("mock write_reference error"),
+
                Err(reference::error::write::WriteRefError::backend(
+
                    std::io::Error::other("mock write_ref error"),
                ))
            }
        }
    }
+

+
    fn delete_ref<R: AsRef<git::fmt::RefStr>>(
+
        &self,
+
        _name: &R,
+
    ) -> Result<(), reference::error::write::DeleteRefError> {
+
        unimplemented!("MockRepository::delete_ref")
+
    }
}

/// Always signs successfully, returning a fixed 64-byte signature.
modified crates/radicle/src/test/storage.rs
@@ -152,16 +152,29 @@ impl MockRepository {
    }
}

-
impl self::refs::sigrefs::git::reference::Reader for MockRepository {
-
    fn find_reference(
+
impl crate::git::repository::RefReader for MockRepository {
+
    type References<'a> = std::iter::Empty<
+
        Result<
+
            (git::fmt::Qualified<'static>, Oid),
+
            crate::git::repository::reference::error::read::ListReferenceError,
+
        >,
+
    >;
+

+
    fn ref_target<R: AsRef<git::fmt::RefStr>>(
        &self,
-
        reference: &git::fmt::Namespaced,
-
    ) -> Result<Option<Oid>, refs::sigrefs::git::reference::error::FindReference> {
-
        use refs::sigrefs::git::reference::error::FindReference;
-
        let ns = reference.namespace();
+
        name: &R,
+
    ) -> Result<Option<Oid>, crate::git::repository::reference::error::read::RefTargetError> {
+
        use crate::git::repository::reference::error::read::RefTargetError;
+

+
        let name = name.as_ref();
+
        let namespaced = match name.to_namespaced() {
+
            Some(ns) => ns,
+
            None => return Ok(None),
+
        };

-
        let remote: PublicKey = ns.as_str().parse().map_err(FindReference::other)?;
-
        let reference = reference.strip_namespace();
+
        let ns = namespaced.namespace();
+
        let remote: PublicKey = ns.as_str().parse().map_err(RefTargetError::backend)?;
+
        let reference = namespaced.strip_namespace();

        match self.remotes.get(&remote) {
            None => Ok(None),
@@ -174,6 +187,14 @@ impl self::refs::sigrefs::git::reference::Reader for MockRepository {
            }
        }
    }
+

+
    fn list_refs<'a, P: AsRef<git::fmt::refspec::PatternStr>>(
+
        &'a self,
+
        _pattern: &P,
+
    ) -> Result<Self::References<'a>, crate::git::repository::reference::error::read::ListRefsError>
+
    {
+
        unimplemented!("MockRepository::list_refs")
+
    }
}

impl RemoteRepository for MockRepository {