Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src storage git.rs
#![deny(clippy::unwrap_used)]
pub mod cob;
pub mod transport;

pub mod temp;
pub use temp::TempRepository;

use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::{fs, io};

use crate::git::canonical::Quorum;
use crate::git::raw::ErrorExt as _;
use crate::identity::doc::DocError;
use crate::identity::{Doc, DocAt, RepoId};
use crate::identity::{Identity, Project};
use crate::node::device::Device;
use crate::storage::refs::{FeatureLevel, Refs, SignedRefs};
use crate::storage::{
    ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
    WriteRepository, WriteStorage,
};
use crate::storage::{SignedRefsInfo, refs};
use crate::{git, git::Oid, node};

use crate::git::RefError;
use crate::git::UserInfo;
use crate::git::fmt::{
    Qualified, RefString, refname, refspec, refspec::PatternStr, refspec::PatternString,
};
pub use crate::storage::{Error, RepositoryError};

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

pub static NAMESPACES_GLOB: LazyLock<PatternString> =
    LazyLock::new(|| git::fmt::pattern!("refs/namespaces/*"));
pub static SIGREFS_GLOB: LazyLock<refspec::PatternString> =
    LazyLock::new(|| git::fmt::pattern!("refs/namespaces/*/rad/sigrefs"));
pub static CANONICAL_IDENTITY: LazyLock<git::fmt::Qualified> = LazyLock::new(|| {
    git::fmt::Qualified::from_components(
        git::fmt::component!("rad"),
        git::fmt::component!("id"),
        None,
    )
});

/// A parsed Git reference.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ref {
    pub oid: crate::git::Oid,
    pub name: RefString,
    pub namespace: Option<RemoteId>,
}

impl TryFrom<git::raw::Reference<'_>> for Ref {
    type Error = RefError;

    fn try_from(r: git::raw::Reference) -> Result<Self, Self::Error> {
        let name = r.name().ok_or(RefError::InvalidName)?;
        let (namespace, name) = match git::parse_ref_namespaced::<RemoteId>(name) {
            Ok((namespace, refname)) => (Some(namespace), refname.to_ref_string()),
            Err(RefError::MissingNamespace(refname)) => (None, refname),
            Err(err) => return Err(err),
        };
        let oid = r.resolve()?.target().ok_or(RefError::NoTarget)?;

        Ok(Self {
            namespace,
            name,
            oid: oid.into(),
        })
    }
}

#[derive(Debug, Clone)]
pub struct Storage {
    path: PathBuf,
    info: UserInfo,
}

impl ReadStorage for Storage {
    type Repository = Repository;

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

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

    fn path_of(&self, rid: &RepoId) -> PathBuf {
        paths::repository(&self, rid)
    }

    fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError> {
        if paths::repository(&self, rid).exists() {
            let _ = self.repository(*rid)?.head()?;
            return Ok(true);
        }
        Ok(false)
    }

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

    fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
        let mut repos = Vec::new();

        for result in fs::read_dir(&self.path)? {
            let path = result?;

            // Skip non-directories.
            if !path.file_type()?.is_dir() {
                continue;
            }
            // Skip hidden files.
            if path.file_name().to_string_lossy().starts_with('.') {
                continue;
            }

            if let Some(ext) = path.path().extension().and_then(|s| s.to_str()) {
                if ext == TempRepository::EXT {
                    // Skip temporary repositories
                    log::debug!(target: "storage", "Skipping temporary repository at '{}'", path.path().display());
                    continue;
                } else if "lock" == ext {
                    // In previous versions, the extension ".lock" was used for temporary repositories.
                    // This is to handle those names in a backward-compatible way.
                    log::debug!(target: "storage", "Skipping locked repository at '{}'", path.path().display());
                    continue;
                } else {
                    log::warn!(target: "storage", "Found path '{}' with unexpected extension '{ext}'", path.path().display());
                }
            }

            let rid = RepoId::try_from(path.file_name())
                .map_err(|_| Error::InvalidId(path.file_name()))?;

            let repo = match self.repository(rid) {
                Ok(repo) => repo,
                Err(e) => {
                    log::warn!(target: "storage", "Repository {rid} is invalid: {e}");
                    continue;
                }
            };
            let doc = match repo.identity_doc() {
                Ok(doc) => doc.into(),
                Err(e) => {
                    log::warn!(target: "storage", "Repository {rid} is invalid: looking up doc: {e}");
                    continue;
                }
            };

            // For performance reasons, we don't do a full repository check here.
            let head = match repo.head() {
                Ok((_, head)) => head,
                Err(e) => {
                    log::warn!(target: "storage", "Repository {rid} is invalid: looking up head: {e}");
                    continue;
                }
            };
            // Nb. This will be `None` if they were not found.
            let refs = SignedRefsInfo::new(refs::SignedRefs::load(self.info.key, &repo))
                .map_err(|err| Error::Refs(refs::Error::Read(err)))?;

            let synced_at = match &refs {
                SignedRefsInfo::Some(refs) => Some(node::SyncedAt::new(refs.at, &repo)?),
                _ => None,
            };

            repos.push(RepositoryInfo {
                rid,
                head,
                doc,
                refs,
                synced_at,
            });
        }
        Ok(repos)
    }
}

impl WriteStorage for Storage {
    type RepositoryMut = Repository;

    fn repository_mut(&self, rid: RepoId) -> Result<Self::RepositoryMut, RepositoryError> {
        Repository::open(paths::repository(self, &rid), rid)
    }

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

    fn clean(&self, rid: RepoId) -> Result<Vec<RemoteId>, RepositoryError> {
        let repo = self.repository(rid)?;
        // N.b. we remove the repository if the `local` peer has no
        // `rad/sigrefs`. There's no risk of them corrupting data.
        let has_sigrefs = SignedRefs::load(self.info.key, &repo)
            .map_err(|err| RepositoryError::from(refs::Error::Read(err)))?
            .is_some();
        if has_sigrefs {
            repo.clean(&self.info.key)
        } else {
            let remotes = repo.remote_ids()?.collect::<Result<_, _>>()?;
            repo.remove()?;
            Ok(remotes)
        }
    }
}

impl Storage {
    /// Open a new storage instance and load its inventory.
    pub fn open<P: AsRef<Path>>(path: P, info: UserInfo) -> Result<Self, Error> {
        let path = path.as_ref().to_path_buf();

        match fs::create_dir_all(&path) {
            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
            Err(err) => return Err(Error::Io(err)),
            Ok(()) => {}
        }
        Ok(Self { path, info })
    }

    /// Create a [`Repository`] in a temporary directory.
    ///
    /// This is used to prevent other processes accessing it during
    /// initialization. Usually, callers will want to move the repository
    /// to its destination after initialization in the temporary location.
    pub fn temporary_repository(&self, rid: RepoId) -> Result<TempRepository, RepositoryError> {
        if self.contains(&rid)? {
            return Err(Error::Io(io::Error::new(
                io::ErrorKind::AlreadyExists,
                format!("refusing to create temporary repository for {rid}"),
            ))
            .into());
        }
        TempRepository::new(self.path(), rid, &self.info)
    }

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

    pub fn repositories_by_id<'a, I>(
        &self,
        rids: I,
    ) -> impl Iterator<Item = Result<RepositoryInfo, RepositoryError>> + use<'_, 'a, I>
    where
        I: Iterator<Item = &'a RepoId>,
    {
        rids.map(|rid| {
            let repo = self.repository(*rid)?;
            let (_, head) = repo.head()?;

            let refs = SignedRefsInfo::new(refs::SignedRefs::load(self.info.key, &repo))
                .map_err(|err| Error::Refs(refs::Error::Read(err)))?;

            let synced_at = match &refs {
                SignedRefsInfo::Some(refs) => Some(node::SyncedAt::new(refs.at, &repo)?),
                _ => None,
            };

            Ok(RepositoryInfo {
                rid: *rid,
                head,
                doc: repo.identity_doc()?.into(),
                refs,
                synced_at,
            })
        })
    }

    pub fn inspect(&self) -> Result<(), RepositoryError> {
        for r in self.repositories()? {
            let rid = r.rid;
            let repo = self.repository(rid)?;

            for r in repo.raw().references()? {
                let r = r?;
                let name = r.name().ok_or(Error::InvalidRef)?;
                let oid = r.resolve()?.target().ok_or(Error::InvalidRef)?;

                println!("{} {oid} {name}", rid.urn());
            }
        }
        Ok(())
    }
}

/// Git implementation of [`WriteRepository`] using the `git2` crate.
pub struct Repository {
    /// The repository identifier (RID).
    pub id: RepoId,
    /// The backing Git repository.
    pub backend: git::raw::Repository,
}

impl AsRef<Repository> for Repository {
    fn as_ref(&self) -> &Repository {
        self
    }
}

impl git::canonical::effects::Ancestry for Repository {
    fn graph_ahead_behind(
        &self,
        commit: Oid,
        upstream: Oid,
    ) -> Result<git::canonical::GraphAheadBehind, git::canonical::effects::GraphDescendant> {
        git::canonical::effects::Ancestry::graph_ahead_behind(&self.backend, commit, upstream)
    }
}

impl git::canonical::effects::FindMergeBase for Repository {
    fn merge_base(
        &self,
        a: Oid,
        b: Oid,
    ) -> Result<git::canonical::MergeBase, git::canonical::effects::MergeBaseError> {
        git::canonical::effects::FindMergeBase::merge_base(&self.backend, a, b)
    }
}

impl git::canonical::effects::FindObjects for Repository {
    fn find_objects<'a, 'b, I>(
        &self,
        refname: &Qualified<'a>,
        dids: I,
    ) -> Result<git::canonical::FoundObjects, git::canonical::effects::FindObjectsError>
    where
        I: Iterator<Item = &'b crate::prelude::Did>,
    {
        git::canonical::effects::FindObjects::find_objects(&self.backend, refname, dids)
    }
}

/// A set of [`Validation`] errors that a caller **must use**.
#[must_use]
#[derive(Debug, Default)]
pub struct Validations(pub Vec<Validation>);

impl Validations {
    pub fn append(&mut self, vs: &mut Self) {
        self.0.append(&mut vs.0)
    }
}

impl IntoIterator for Validations {
    type Item = Validation;
    type IntoIter = std::vec::IntoIter<Self::Item>;

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

impl Deref for Validations {
    type Target = Vec<Validation>;

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

impl DerefMut for Validations {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

/// Validation errors that can occur when verifying the layout of the
/// storage. These errors include checking the validity of the
/// `rad/sigrefs` contents and the identity of the repository.
#[derive(Debug, Error)]
pub enum Validation {
    #[error("found unsigned ref `{0}`")]
    UnsignedRef(RefString),
    #[error("expected `refs/namespaces/{remote}/{refname}` at {expected} but found {actual}")]
    MismatchedRef {
        remote: RemoteId,
        refname: RefString,
        expected: Oid,
        actual: Oid,
    },
    #[error("missing `refs/namespaces/{remote}/{refname}`")]
    MissingRef {
        remote: RemoteId,
        refname: RefString,
    },
    #[error("missing `refs/namespaces/{0}/refs/rad/sigrefs`")]
    MissingRadSigRefs(RemoteId),
    #[error("failed to read `refs/namespaces/{remote}/refs/rad/sigrefs`: {source}")]
    Read {
        remote: RemoteId,
        #[source]
        source: crate::storage::refs::sigrefs::read::error::Read,
    },
    #[error(
        "rejecting `refs/namespaces/{remote}/refs/rad/sigrefs` on feature level '{actual}', below required minimum '{minimum}'"
    )]
    InsufficientFeatureLevel {
        remote: RemoteId,
        actual: FeatureLevel,
        minimum: FeatureLevel,
    },
}

impl Repository {
    /// Open an existing repository.
    pub fn open<P: AsRef<Path>>(path: P, id: RepoId) -> Result<Self, RepositoryError> {
        let backend = git::raw::Repository::open_ext(
            path.as_ref(),
            git::raw::RepositoryOpenFlags::empty()
                | git::raw::RepositoryOpenFlags::BARE
                | git::raw::RepositoryOpenFlags::NO_DOTGIT
                | git::raw::RepositoryOpenFlags::NO_SEARCH,
            &[] as &[&std::ffi::OsStr],
        )?;

        Ok(Self { id, backend })
    }

    /// Create a new repository.
    pub fn create<P: AsRef<Path>>(path: P, id: RepoId, info: &UserInfo) -> Result<Self, Error> {
        let backend = git::raw::Repository::init_opts(
            &path,
            git::raw::RepositoryInitOptions::new()
                .bare(true)
                .no_reinit(true)
                .external_template(false),
        )?;

        {
            // Even though `external_template(false)` is called above,
            // libgit2 places stub files in the repository:
            // https://github.com/libgit2/libgit2/blob/ca225744b992bf2bf24e9a2eb357ddef78179667/src/libgit2/repo_template.h#L50-L54
            // This is helpful for a "normal" repository, directly interacted
            // with by a human, but not necessary for our use case.
            // Attempt to remove these files, but ignore any errors.
            // An alternative solution would be to define our own template,
            // but distributing that template is way more complex than
            // deleting a handful of files.
            let _ = fs::remove_dir_all(path.as_ref().join("hooks"));
            let _ = fs::remove_dir_all(path.as_ref().join("info"));
            let _ = fs::remove_file(path.as_ref().join("description"));
        }

        let mut config = backend.config()?;

        config.set_str("user.name", &info.name())?;
        config.set_str("user.email", &info.email())?;

        Ok(Self { id, backend })
    }

    /// Remove an existing repository
    pub fn remove(&self) -> Result<(), Error> {
        let path = self.backend.path();
        if path.exists() {
            fs::remove_dir_all(path)?;
        }
        Ok(())
    }

    /// Remove all the remotes of a repository that are not the
    /// delegates of the repository or the local peer.
    ///
    /// N.b. failure to delete remotes or references will not result
    /// in an early exit. Instead, this method continues to delete the
    /// next available remote or reference.
    pub fn clean(&self, local: &RemoteId) -> Result<Vec<RemoteId>, RepositoryError> {
        let delegates = self
            .delegates()?
            .into_iter()
            .map(|did| *did)
            .collect::<BTreeSet<_>>();
        let mut deleted = Vec::new();
        for id in self.remote_ids()? {
            let id = match id {
                Ok(id) => id,
                Err(e) => {
                    log::error!(target: "storage", "Failed to clean up remote: {e}");
                    continue;
                }
            };

            // N.b. it is fatal to delete local or delegates
            if *local == id || delegates.contains(&id) {
                continue;
            }

            let glob = git::fmt::refname!("refs/namespaces")
                .join(git::fmt::Component::from(&id))
                .with_pattern(git::fmt::refspec::STAR);
            let refs = match self.references_glob(&glob) {
                Ok(refs) => refs,
                Err(e) => {
                    log::error!(target: "storage", "Failed to clean up remote '{id}': {e}");
                    continue;
                }
            };
            for (refname, _) in refs {
                if let Ok(mut r) = self.backend.find_reference(refname.as_str()) {
                    if let Err(e) = r.delete() {
                        log::error!(target: "storage", "Failed to clean up reference '{refname}': {e}");
                    }
                } else {
                    log::error!(target: "storage", "Failed to clean up reference '{refname}'");
                }
            }
            deleted.push(id);
        }

        Ok(deleted)
    }

    /// Create the repository's identity branch.
    pub fn init<G, S>(
        doc: &Doc,
        storage: &S,
        signer: &Device<G>,
    ) -> Result<(Self, crate::git::Oid), RepositoryError>
    where
        G: crypto::signature::Signer<crypto::Signature>,
        S: WriteStorage,
    {
        let (doc_oid, doc_bytes) = doc.encode()?;
        let id = RepoId::from(doc_oid);
        let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
        let oid = repo.backend.blob(&doc_bytes)?; // Store document blob in repository.

        debug_assert_eq!(doc_oid, oid);

        let commit = doc.init(&repo, signer)?;

        Ok((repo, commit))
    }

    pub fn inspect(&self) -> Result<(), Error> {
        for r in self.backend.references()? {
            let r = r?;
            let name = r.name().ok_or(Error::InvalidRef)?;
            let oid = r.resolve()?.target().ok_or(Error::InvalidRef)?;

            println!("{oid} {name}");
        }
        Ok(())
    }

    /// Iterate over all references.
    pub fn references(
        &self,
    ) -> Result<impl Iterator<Item = Result<Ref, refs::Error>> + '_, git::raw::Error> {
        let refs = self
            .backend
            .references()?
            .map(|reference| {
                let r = reference?;

                match Ref::try_from(r) {
                    Err(err) => Err(err.into()),
                    Ok(r) => Ok(Some(r)),
                }
            })
            .filter_map(Result::transpose);

        Ok(refs)
    }

    /// Get the canonical project information.
    pub fn project(&self) -> Result<Project, RepositoryError> {
        let head = self.identity_head()?;
        let doc = self.identity_doc_at(head)?;
        let proj = doc.project()?;

        Ok(proj)
    }

    pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc, DocError> {
        let oid = self.identity_head_of(remote)?;
        Doc::load_at(oid, self).map(|d| d.into())
    }

    pub fn remote_ids(
        &self,
    ) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git::raw::Error> {
        let iter = self.backend.references_glob(SIGREFS_GLOB.as_str())?.map(
            |reference| -> Result<RemoteId, refs::Error> {
                let r = reference?;
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
                let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;

                Ok(id)
            },
        );
        Ok(iter)
    }

    pub fn remotes(
        &self,
    ) -> Result<impl Iterator<Item = Result<(RemoteId, Remote), refs::Error>> + '_, git::raw::Error>
    {
        let remotes =
            self.backend
                .references_glob(SIGREFS_GLOB.as_str())?
                .map(|reference| -> Result<_, _> {
                    let r = reference?;
                    let name = r.name().ok_or(refs::Error::InvalidRef)?;
                    let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
                    let remote = self.remote(&id)?;

                    Ok((id, remote))
                });
        Ok(remotes)
    }
}

impl RemoteRepository for Repository {
    fn remotes(&self) -> Result<Remotes, refs::Error> {
        let mut remotes = Vec::new();
        for remote in Repository::remotes(self)? {
            remotes.push(remote?);
        }
        Ok(Remotes::from_iter(remotes))
    }

    fn remote(&self, remote: &RemoteId) -> Result<Remote, refs::Error> {
        let refs = SignedRefs::load(*remote, self)?;
        let refs = refs.ok_or_else(|| {
            refs::Error::Read(refs::sigrefs::read::error::Read::MissingSigrefs {
                namespace: *remote,
            })
        })?;
        Ok(Remote::new(refs))
    }

    fn remote_refs_at(&self) -> Result<Vec<RefsAt>, refs::Error> {
        let mut all = Vec::new();

        for remote in self.remote_ids()? {
            let remote = remote?;
            let refs_at = RefsAt::new(self, remote)?;

            all.push(refs_at);
        }
        Ok(all)
    }
}

impl ValidateRepository for Repository {
    fn validate_remote(&self, remote: &Remote) -> Result<Validations, Error> {
        // Contains a copy of the signed refs of this remote.
        let mut signed = BTreeMap::from((*remote.refs).clone());
        let mut failures = Validations::default();
        let mut has_sigrefs = false;

        // Check all repository references, making sure they are present in the signed refs map.
        for (refname, oid) in self.references_of(&remote.id())? {
            // Skip validation of the signed refs branch, as it is not part of `Remote`.
            if refname == refs::SIGREFS_BRANCH.to_ref_string() {
                has_sigrefs = true;
                continue;
            }
            if let Some(signed_oid) = signed.remove(&refname) {
                if oid != signed_oid {
                    failures.push(Validation::MismatchedRef {
                        remote: remote.id(),
                        refname,
                        expected: signed_oid,
                        actual: oid,
                    });
                }
            } else {
                failures.push(Validation::UnsignedRef(refname));
            }
        }

        if !has_sigrefs {
            failures.push(Validation::MissingRadSigRefs(remote.id()));
        }

        // The refs that are left in the map, are ones that were signed, but are not
        // in the repository. If any are left, bail.
        if let Some((name, _)) = signed.into_iter().next() {
            failures.push(Validation::MissingRef {
                refname: name,
                remote: remote.id(),
            });
        }

        // Nb. As it stands, it doesn't make sense to verify a single remote's identity branch,
        // since it is a COB.

        Ok(failures)
    }
}

impl ReadRepository for Repository {
    fn id(&self) -> RepoId {
        self.id
    }

    fn is_empty(&self) -> Result<bool, git::raw::Error> {
        Ok(self.remotes()?.next().is_none())
    }

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

    fn blob_at<P: AsRef<Path>>(
        &self,
        commit_id: Oid,
        path: P,
    ) -> Result<git::raw::Blob<'_>, git::raw::Error> {
        let commit = self.backend.find_commit(git::raw::Oid::from(commit_id))?;
        let tree = commit.tree()?;
        let entry = tree.get_path(path.as_ref())?;
        let obj = entry.to_object(&self.backend)?;
        let blob = obj.into_blob().map_err(|_|
            crate::git::raw::Error::new(
                crate::git::raw::ErrorCode::NotFound,
                crate::git::raw::ErrorClass::None,
                format!("Path '{}' in tree of commit {commit_id} was expected to be a blob, but is not.", path.as_ref().display()),
            )
        )?;

        Ok(blob)
    }

    fn blob(&self, oid: Oid) -> Result<git::raw::Blob<'_>, git::raw::Error> {
        self.backend.find_blob(oid.into())
    }

    fn reference(
        &self,
        remote: &RemoteId,
        name: &git::fmt::Qualified,
    ) -> Result<git::raw::Reference<'_>, git::raw::Error> {
        let name = name.with_namespace(remote.into());
        self.backend.find_reference(&name)
    }

    fn reference_oid(
        &self,
        remote: &RemoteId,
        reference: &git::fmt::Qualified,
    ) -> Result<Oid, crate::git::raw::Error> {
        let name = reference.with_namespace(remote.into());
        let oid = self.backend.refname_to_id(&name)?;

        Ok(oid.into())
    }

    fn commit(&self, oid: Oid) -> Result<git::raw::Commit<'_>, git::raw::Error> {
        self.backend.find_commit(oid.into())
    }

    fn revwalk(&self, head: Oid) -> Result<git::raw::Revwalk<'_>, git::raw::Error> {
        let mut revwalk = self.backend.revwalk()?;
        revwalk.push(head.into())?;

        Ok(revwalk)
    }

    fn contains(&self, oid: Oid) -> Result<bool, crate::git::raw::Error> {
        self.backend.odb().map(|odb| odb.exists(oid.into()))
    }

    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, crate::git::raw::Error> {
        self.backend
            .graph_descendant_of(head.into(), ancestor.into())
    }

    /// The published references of the given `remote`.
    ///
    /// Note that this includes all references, including `refs/rad/sigrefs`.
    /// This reference must be removed before signing the payload.
    ///
    /// # Skipped References
    ///
    /// References created by [`staging::patch`], i.e. references that begin
    /// with `refs/tmp/heads`, are skipped.
    ///
    /// [`staging::patch`]: crate::git::refs::storage::staging::patch
    fn references_of(&self, remote: &RemoteId) -> Result<Refs, Error> {
        let entries = self
            .backend
            .references_glob(format!("refs/namespaces/{remote}/*").as_str())?;

        let mut refs = Refs::new();

        for e in entries {
            let e = e?;
            let name = e.name().ok_or(Error::InvalidRef)?;
            let (_, refname) = git::parse_ref::<RemoteId>(name)?;
            let oid = e.resolve()?.target().ok_or(Error::InvalidRef)?;
            let (_, category, subcategory, _) = refname.non_empty_components();

            match (category.as_str(), subcategory.as_str()) {
                ("tmp", "heads") => continue,
                _ => {
                    refs.insert(refname.into(), oid.into());
                }
            }
        }
        Ok(refs)
    }

    fn references_glob(
        &self,
        pattern: &PatternStr,
    ) -> Result<Vec<(Qualified<'_>, Oid)>, crate::git::raw::Error> {
        let mut refs = Vec::new();

        for r in self.backend.references_glob(pattern)? {
            let r = r?;

            let Some(oid) = r.resolve()?.target() else {
                continue;
            };

            if let Some(name) = r
                .name()
                .and_then(|n| git::fmt::RefStr::try_from_str(n).ok())
                .and_then(git::fmt::Qualified::from_refstr)
            {
                refs.push((name.to_owned(), oid.into()));
            }
        }
        Ok(refs)
    }

    fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError> {
        Doc::load_at(head, self)
    }

    fn head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
        // If `HEAD` is already set locally, just return that.
        if let Ok(head) = self.backend.head() {
            if let Ok((name, oid)) = git::refs::qualified_from(&head) {
                return Ok((name.to_owned(), oid));
            }
        }
        self.canonical_head()
    }

    fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
        let doc = self.identity_doc()?;
        let refname = doc.default_branch()?.to_owned();

        let crefs = doc.canonical_refs()?;

        Ok(crefs
            .rules()
            .canonical(refname, self)
            .ok_or(RepositoryError::MissingBranchRule)?
            .find_objects()?
            .quorum()?)
        .map(
            |Quorum {
                 refname, object, ..
             }| (refname, object.id()),
        )
    }

    fn identity_head(&self) -> Result<Oid, RepositoryError> {
        let result = self
            .backend
            .refname_to_id(CANONICAL_IDENTITY.as_str())
            .map(Oid::from);

        match result {
            Ok(oid) => Ok(oid),
            Err(err) if err.is_not_found() => self.canonical_identity_head(),
            Err(err) => Err(err.into()),
        }
    }

    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, crate::git::raw::Error> {
        self.reference_oid(remote, &git::refs::storage::IDENTITY_BRANCH)
    }

    fn identity_root(&self) -> Result<Oid, RepositoryError> {
        let oid = self.backend.refname_to_id(CANONICAL_IDENTITY.as_str())?;
        let root = self
            .revwalk(oid.into())?
            .last()
            .ok_or(RepositoryError::Doc(DocError::Missing))??;

        Ok(root.into())
    }

    fn identity_root_of(&self, remote: &RemoteId) -> Result<Oid, RepositoryError> {
        // Remotes that run newer clients will have this reference set. For older clients,
        // compute the root OID based on the identity head.
        if let Ok(root) = self.reference_oid(remote, &git::refs::storage::IDENTITY_ROOT) {
            return Ok(root);
        }
        let oid = self.identity_head_of(remote)?;
        let root = self
            .revwalk(oid)?
            .last()
            .ok_or(RepositoryError::Doc(DocError::Missing))??;

        Ok(root.into())
    }

    fn canonical_identity_head(&self) -> Result<Oid, RepositoryError> {
        for remote in self.remote_ids()? {
            let remote = remote?;
            // Nb. A remote may not have an identity document if the user has not contributed
            // any changes to the identity COB.
            let Ok(root) = self.identity_root_of(&remote) else {
                continue;
            };
            let blob = Doc::blob_at(root, self)?;

            // We've got an identity that goes back to the correct root.
            if *self.id == blob.id() {
                let identity = Identity::get(&root.into(), self)?;

                return Ok(identity.head());
            }
        }
        Err(DocError::Missing.into())
    }

    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, crate::git::raw::Error> {
        self.backend
            .merge_base(left.into(), right.into())
            .map(Oid::from)
    }
}

impl WriteRepository for Repository {
    fn set_head_to_default_branch(&self) -> Result<(), RepositoryError> {
        let head_ref = refname!("HEAD");
        let branch_ref = self.default_branch()?;

        match self.raw().find_reference(head_ref.as_str()) {
            Ok(mut head_ref) => {
                if head_ref
                    .symbolic_target()
                    .is_some_and(|t| t != branch_ref.as_str())
                {
                    head_ref.symbolic_set_target(branch_ref.as_str(), "set-head (radicle)")?;
                }
                Ok(())
            }
            Err(err) if err.is_not_found() => {
                self.raw().reference_symbolic(
                    head_ref.as_str(),
                    branch_ref.as_str(),
                    true,
                    "set-head (radicle)",
                )?;
                Ok(())
            }
            Err(err) => Err(err.into()),
        }
    }

    fn set_default_branch_to_canonical_head(&self) -> Result<SetHead, RepositoryError> {
        let (branch_ref, new) = self.canonical_head()?;

        let old = self
            .raw()
            .refname_to_id(&branch_ref)
            .ok()
            .map(|oid| oid.into());

        if old == Some(new) {
            return Ok(SetHead { old, new });
        }
        log::debug!(target: "storage", "Setting ref: {} -> {}", &branch_ref, new);
        self.raw()
            .reference(&branch_ref, new.into(), true, "set-local-branch (radicle)")?;

        Ok(SetHead { old, new })
    }

    fn set_identity_head_to(&self, commit: Oid) -> Result<(), RepositoryError> {
        log::debug!(target: "storage", "Setting ref: {} -> {}", *CANONICAL_IDENTITY, commit);
        self.raw().reference(
            CANONICAL_IDENTITY.as_str(),
            commit.into(),
            true,
            "set-local-branch (radicle)",
        )?;
        Ok(())
    }

    fn set_remote_identity_root_to(
        &self,
        remote: &RemoteId,
        root: Oid,
    ) -> Result<(), RepositoryError> {
        let refname = git::refs::storage::id_root(remote);

        self.raw()
            .reference(refname.as_str(), root.into(), true, "set-id-root (radicle)")?;

        Ok(())
    }

    fn set_user(&self, info: &UserInfo) -> Result<(), Error> {
        let mut config = self.backend.config()?;
        config.set_str("user.name", &info.name())?;
        config.set_str("user.email", &info.email())?;
        Ok(())
    }

    fn raw(&self) -> &git::raw::Repository {
        &self.backend
    }
}

impl SignRepository for Repository {
    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>,
    {
        self.sign_refs_with(signer, false)
    }

    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>,
    {
        self.sign_refs_with(signer, true)
    }
}

impl Repository {
    fn sign_refs_with<Signer>(
        &self,
        signer: &Signer,
        force: bool,
    ) -> Result<SignedRefs, RepositoryError>
    where
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
        Signer: crypto::signature::Signer<crypto::Signature>,
        Signer: crypto::signature::Verifier<crypto::Signature>,
    {
        let remote = signer.verifying_key();
        // Ensure the root reference is set, which is checked during sigref verification.
        if self
            .reference_oid(&remote, &git::refs::storage::IDENTITY_ROOT)
            .is_err()
        {
            self.set_remote_identity_root(&remote)?;
        }

        let committer = refs::sigrefs::git::Committer::from_env_or_now(&remote);

        let refs = self.references_of(&remote)?;
        let signed = if force {
            refs.force_save(remote, committer, self, signer)?
        } else {
            refs.save(remote, committer, self, signer)?
        };

        Ok(signed)
    }
}

impl sigrefs::git::object::Reader for Repository {
    fn read_commit(
        &self,
        oid: &Oid,
    ) -> Result<Option<Vec<u8>>, sigrefs::git::object::error::ReadCommit> {
        self.backend.read_commit(oid)
    }

    fn read_blob(
        &self,
        commit: &Oid,
        path: &Path,
    ) -> Result<Option<sigrefs::git::object::Blob>, sigrefs::git::object::error::ReadBlob> {
        self.backend.read_blob(commit, path)
    }
}

impl sigrefs::git::object::Writer for Repository {
    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)
    }

    fn write_commit(&self, bytes: &[u8]) -> Result<Oid, sigrefs::git::object::error::WriteCommit> {
        self.backend.write_commit(bytes)
    }
}

impl sigrefs::git::reference::Reader for Repository {
    fn find_reference(
        &self,
        reference: &git::fmt::Namespaced,
    ) -> Result<Option<Oid>, sigrefs::git::reference::error::FindReference> {
        sigrefs::git::reference::Reader::find_reference(&self.backend, reference)
    }
}

impl sigrefs::git::reference::Writer for Repository {
    fn write_reference(
        &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)
    }
}

pub mod trailers {
    use std::str::FromStr;

    use thiserror::Error;

    use super::*;
    use crypto::{PublicKey, PublicKeyError};
    use crypto::{Signature, SignatureError};

    pub const SIGNATURE_TRAILER: &str = "Rad-Signature";

    #[derive(Error, Debug)]
    pub enum Error {
        #[error("invalid format for signature trailer")]
        SignatureTrailerFormat,
        #[error("invalid public key in signature trailer")]
        PublicKey(#[from] PublicKeyError),
        #[error("invalid signature in trailer")]
        Signature(#[from] SignatureError),
    }

    pub fn parse_signatures(msg: &str) -> Result<HashMap<PublicKey, Signature>, Error> {
        let trailers =
            git::raw::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
        let mut signatures = HashMap::with_capacity(trailers.len());

        for (key, val) in trailers.iter() {
            if key == SIGNATURE_TRAILER {
                if let Some((pk, sig)) = val.split_once(' ') {
                    let pk = PublicKey::from_str(pk)?;
                    let sig = Signature::from_str(sig)?;

                    signatures.insert(pk, sig);
                } else {
                    return Err(Error::SignatureTrailerFormat);
                }
            }
        }
        Ok(signatures)
    }
}

pub mod paths {
    use std::path::PathBuf;

    use super::ReadStorage;
    use super::RepoId;

    pub fn repository<S: ReadStorage>(storage: &S, proj: &RepoId) -> PathBuf {
        storage.path().join(proj.canonical())
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {

    use super::*;
    use crate::git;

    use crate::storage::{ReadRepository, ReadStorage};
    use crate::test::fixtures;

    #[test]
    fn test_references_of() {
        let tmp = tempfile::tempdir().unwrap();
        let signer = Device::mock();
        let storage = Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap();

        transport::local::register(storage.clone());

        let (rid, _, _, _) =
            fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
        let repo = storage.repository(rid).unwrap();
        let id = repo.identity().unwrap().head();
        let cob = format!("refs/cobs/xyz.radicle.id/{id}");

        let mut refs = repo
            .references_of(signer.public_key())
            .unwrap()
            .keys()
            .map(|r| r.to_string())
            .collect::<Vec<_>>();
        refs.sort();

        assert_eq!(
            refs,
            vec![
                &cob,
                "refs/heads/master",
                "refs/rad/id",
                "refs/rad/root",
                "refs/rad/sigrefs"
            ]
        );
    }

    #[test]
    fn test_sign_refs() {
        let tmp = tempfile::tempdir().unwrap();
        let mut rng = fastrand::Rng::new();
        let signer = Device::mock_rng(&mut rng);
        let storage = Storage::open(tmp.path(), fixtures::user()).unwrap();
        let alice = *signer.public_key();
        let (rid, _, working, _) =
            fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
        let stored = storage.repository(rid).unwrap();
        let sig =
            git::raw::Signature::now(&alice.to_string(), "anonymous@radicle.example.com").unwrap();
        let head = working.head().unwrap().peel_to_commit().unwrap();

        git::commit(
            &working,
            &head,
            &git::fmt::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
            "Second commit",
            &sig,
            &head.tree().unwrap(),
        )
        .unwrap();

        let signed = stored.sign_refs(&signer).unwrap();
        let remote = stored.remote(&alice).unwrap();
        let mut unsigned = stored.references_of(&alice).unwrap();

        // The signed refs doesn't contain the signature ref itself.
        unsigned.remove_sigrefs().unwrap();

        assert_eq!(remote.refs.refs(), signed.refs());
        assert_eq!(*remote.refs.refs(), unsigned);
    }
}