Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cob: Don't require a local fork to exist
Alexis Sellier committed 3 years ago
commit 5f59493c702ca57442659f8eae27384ec514698a
parent 2a687502c55803b6a20a77538fc5ef6b39d047f5
22 files changed +127 -177
modified radicle-cli/src/commands/checkout.rs
@@ -94,7 +94,7 @@ pub fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
    let remote = options.remote.unwrap_or(*profile.id());
    let doc = storage
        .repository(id)?
-
        .identity_of(&remote)
+
        .identity_doc_of(&remote)
        .context("project could not be found in local storage")?;
    let payload = doc.project()?;
    let path = PathBuf::from(payload.name());
modified radicle-cli/src/commands/clone.rs
@@ -7,14 +7,14 @@ use anyhow::anyhow;
use thiserror::Error;

use radicle::git::raw;
-
use radicle::identity::doc;
use radicle::identity::doc::{DocError, Id};
+
use radicle::identity::{doc, IdentityError};
use radicle::node;
use radicle::node::{FetchResult, Handle as _, Node};
use radicle::prelude::*;
use radicle::rad;
use radicle::storage;
-
use radicle::storage::git::{ProjectError, Storage};
+
use radicle::storage::git::Storage;

use crate::commands::rad_checkout as checkout;
use crate::project;
@@ -151,7 +151,7 @@ pub enum CloneError {
    #[error("payload: {0}")]
    Payload(#[from] doc::PayloadError),
    #[error("project error: {0}")]
-
    Project(#[from] ProjectError),
+
    Identity(#[from] IdentityError),
    #[error("no seeds found for {0}")]
    NotFound(Id),
}
@@ -223,7 +223,7 @@ pub fn clone<G: Signer>(
        }
    }

-
    let doc = repository.identity_of(&me)?;
+
    let doc = repository.identity_doc_of(&me)?;
    let proj = doc.project()?;
    let path = Path::new(proj.name());

modified radicle-cli/src/commands/merge.rs
@@ -137,7 +137,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    let signer = term::signer(&profile)?;
    let repository = profile.storage.repository(id)?;
    let _project = repository
-
        .identity_of(profile.id())
+
        .identity_doc_of(profile.id())
        .context(format!("couldn't load project {id} from local state"))?;
    let repository = profile.storage.repository(id)?;
    let mut patches = Patches::open(*profile.id(), &repository)?;
modified radicle-cli/src/commands/review.rs
@@ -137,7 +137,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    let signer = term::signer(&profile)?;
    let repository = profile.storage.repository(id)?;
    let _project = repository
-
        .identity_of(profile.id())
+
        .identity_doc_of(profile.id())
        .context(format!("couldn't load project {id} from local state"))?;
    let mut patches = Patches::open(*profile.id(), &repository)?;

deleted radicle-cob/src/identity.rs
@@ -1,19 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use git_ext::Oid;
-

-
/// An [`Identity`] represents a content addressed identity
-
/// (i.e. expected to be stored in a git backend).
-
///
-
/// It should have a unique, stable, content addressable identifier.
-
pub trait Identity {
-
    type Identifier;
-

-
    /// Provide the content address for the given identity. This is
-
    /// expected to be the latest address for the identity at the time
-
    /// of use.
-
    fn content_id(&self) -> Oid;
-
}
modified radicle-cob/src/lib.rs
@@ -46,10 +46,6 @@
//! represents the type of resource the collaborative objects are
//! relating to, for example a software project.
//!
-
//! This `Resource` must implement [`identity::Identity`] to allow the
-
//! internal logic to reference the resource's content-address in
-
//! `git` as well as the stable identifier used for the resource.
-
//!
//! ## History Traversal
//!
//! The [`History`] of a [`CollaborativeObject`] -- accessed via
@@ -89,8 +85,6 @@ mod trailers;
pub mod change;
pub use change::Change;

-
pub mod identity;
-

pub mod history;
pub use history::{Contents, Entry, History};

modified radicle-cob/src/object/collaboration.rs
@@ -8,7 +8,7 @@ use std::collections::BTreeSet;
use git_ext::Oid;

use crate::change::store::Manifest;
-
use crate::{change, identity::Identity, History, ObjectId, TypeName};
+
use crate::{change, History, ObjectId, TypeName};

pub mod error;

modified radicle-cob/src/object/collaboration/create.rs
@@ -51,21 +51,20 @@ impl Create {
///
/// The `args` are the metadata for this [`CollaborativeObject`]. See
/// [`Create`] for further information.
-
pub fn create<S, G, Resource>(
+
pub fn create<S, G>(
    storage: &S,
    signer: &G,
-
    resource: &Resource,
+
    resource: Oid,
    identifier: &S::Identifier,
    args: Create,
) -> Result<CollaborativeObject, error::Create>
where
    S: Store,
    G: crypto::Signer,
-
    Resource: Identity,
{
    let Create { ref typename, .. } = &args;
    let init_change = storage
-
        .store(resource.content_id(), signer, args.template())
+
        .store(resource, signer, args.template())
        .map_err(error::Create::from)?;
    let object_id = init_change.id().into();

@@ -76,7 +75,7 @@ where
    let history = History::new_from_root(
        *init_change.id(),
        init_change.signature.key,
-
        resource.content_id(),
+
        resource,
        init_change.contents,
        init_change.timestamp,
    );
modified radicle-cob/src/object/collaboration/update.rs
@@ -3,12 +3,10 @@
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

+
use git_ext::Oid;
use nonempty::NonEmpty;

-
use crate::{
-
    change, change_graph::ChangeGraph, identity::Identity, CollaborativeObject, ObjectId, Store,
-
    TypeName,
-
};
+
use crate::{change, change_graph::ChangeGraph, CollaborativeObject, ObjectId, Store, TypeName};

use super::error;

@@ -44,17 +42,16 @@ pub struct Update {
///
/// The `args` are the metadata for this [`CollaborativeObject`]
/// udpate. See [`Update`] for further information.
-
pub fn update<S, G, Resource>(
+
pub fn update<S, G>(
    storage: &S,
    signer: &G,
-
    resource: &Resource,
+
    resource: Oid,
    identifier: &S::Identifier,
    args: Update,
) -> Result<CollaborativeObject, error::Update>
where
    S: Store,
    G: crypto::Signer,
-
    Resource: Identity,
{
    let Update {
        ref typename,
@@ -73,7 +70,7 @@ where
        .ok_or(error::Update::NoSuchObject)?;

    let change = storage.store(
-
        resource.content_id(),
+
        resource,
        signer,
        change::Template {
            tips: object.tips().iter().cloned().collect(),
modified radicle-cob/src/test/identity/person.rs
@@ -1,10 +1,9 @@
use git_ext::Oid;
use serde::{Deserialize, Serialize};

-
use crate::identity::Identity;
use crate::test::storage::{self, Storage};

-
use super::{Name, Urn};
+
use super::Name;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Person {
@@ -57,11 +56,3 @@ impl Person {
        &self.payload.name
    }
}
-

-
impl Identity for Person {
-
    type Identifier = Urn;
-

-
    fn content_id(&self) -> Oid {
-
        self.content_id
-
    }
-
}
modified radicle-cob/src/test/identity/project.rs
@@ -3,7 +3,6 @@ use std::collections::BTreeSet;
use git_ext::Oid;
use serde::{Deserialize, Serialize};

-
use crate::identity::Identity;
use crate::test;
use crate::test::storage::{self, Storage};

@@ -74,11 +73,3 @@ impl Project {
        &self.payload.name
    }
}
-

-
impl Identity for RemoteProject {
-
    type Identifier = Urn;
-

-
    fn content_id(&self) -> Oid {
-
        self.project.content_id
-
    }
-
}
modified radicle-cob/src/tests.rs
@@ -26,7 +26,7 @@ fn roundtrip() {
    let cob = create(
        &storage,
        &signer,
-
        &proj,
+
        proj.project.content_id,
        &proj.identifier(),
        Create {
            history_type: "test".to_string(),
@@ -58,7 +58,7 @@ fn list_cobs() {
    let issue_1 = create(
        &storage,
        &signer,
-
        &proj,
+
        proj.project.content_id,
        &proj.identifier(),
        Create {
            history_type: "test".to_string(),
@@ -72,7 +72,7 @@ fn list_cobs() {
    let issue_2 = create(
        &storage,
        &signer,
-
        &proj,
+
        proj.project.content_id,
        &proj.identifier(),
        Create {
            history_type: "test".to_string(),
@@ -106,7 +106,7 @@ fn update_cob() {
    let cob = create(
        &storage,
        &signer,
-
        &proj,
+
        proj.project.content_id,
        &proj.identifier(),
        Create {
            history_type: "test".to_string(),
@@ -124,7 +124,7 @@ fn update_cob() {
    let updated = update(
        &storage,
        &signer,
-
        &proj,
+
        proj.project.content_id,
        &proj.identifier(),
        Update {
            changes: nonempty!(b"issue 1".to_vec()),
@@ -164,7 +164,7 @@ fn traverse_cobs() {
    let cob = create(
        &storage,
        &terry_signer,
-
        &terry_proj,
+
        terry_proj.project.content_id,
        &terry_proj.identifier(),
        Create {
            contents: nonempty!(b"issue 1".to_vec()),
@@ -186,7 +186,7 @@ fn traverse_cobs() {
    let updated = update(
        &storage,
        &neil_signer,
-
        &neil_proj,
+
        neil_proj.project.content_id,
        &neil_proj.identifier(),
        Update {
            changes: nonempty!(b"issue 2".to_vec()),
modified radicle-httpd/src/api/error.rs
@@ -38,9 +38,9 @@ pub enum Error {
    #[error(transparent)]
    CobStore(#[from] radicle::cob::store::Error),

-
    /// Git project error.
+
    /// Identity error.
    #[error(transparent)]
-
    GitProject(#[from] radicle::storage::git::ProjectError),
+
    Identity(#[from] radicle::identity::IdentityError),

    /// Project doc error.
    #[error(transparent)]
modified radicle-node/src/service.rs
@@ -25,6 +25,7 @@ use crate::address::AddressBook;
use crate::clock::Timestamp;
use crate::crypto;
use crate::crypto::{Signer, Verified};
+
use crate::identity::IdentityError;
use crate::identity::{Doc, Id};
use crate::node;
use crate::node::{Address, Features, FetchResult, Seed, Seeds};
@@ -1277,8 +1278,8 @@ pub trait ServiceState {
    fn sessions(&self) -> &Sessions;
    /// Get the current inventory.
    fn inventory(&self) -> Result<Inventory, storage::Error>;
-
    /// Get a project from storage, using the local node's key.
-
    fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, storage::ProjectError>;
+
    /// Get a repository from storage, using the local node's key.
+
    fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError>;
    /// Get the clock.
    fn clock(&self) -> &LocalTime;
    /// Get the clock mutably.
@@ -1303,7 +1304,7 @@ where
        self.storage.inventory()
    }

-
    fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, storage::ProjectError> {
+
    fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError> {
        self.storage.get(&self.node_id(), proj)
    }

@@ -1385,7 +1386,7 @@ pub enum LookupError {
    #[error(transparent)]
    Routing(#[from] routing::Error),
    #[error(transparent)]
-
    Project(#[from] storage::ProjectError),
+
    Identity(#[from] IdentityError),
}

/// Information on a peer, that we may or may not be connected to.
modified radicle-node/src/worker.rs
@@ -8,7 +8,7 @@ use netservices::tunnel::Tunnel;
use netservices::{NetSession, SplitIo};

use radicle::crypto::Signer;
-
use radicle::identity::Id;
+
use radicle::identity::{Id, IdentityError};
use radicle::storage::{Namespaces, ReadRepository, RefUpdate, WriteRepository, WriteStorage};
use radicle::{git, Storage};
use reactor::poller::popol;
@@ -46,7 +46,7 @@ pub enum FetchError {
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error(transparent)]
-
    Project(#[from] storage::ProjectError),
+
    Identity(#[from] IdentityError),
}

impl FetchError {
modified radicle/src/cob.rs
@@ -11,8 +11,8 @@ pub mod test;

pub use cob::{create, get, list, remove, update};
pub use cob::{
-
    history::entry::EntryBlob, identity::Identity, object::collaboration::error,
-
    CollaborativeObject, Contents, Create, Entry, History, ObjectId, TypeName, Update,
+
    history::entry::EntryBlob, object::collaboration::error, CollaborativeObject, Contents, Create,
+
    Entry, History, ObjectId, TypeName, Update,
};
pub use common::*;
pub use op::{ActorId, Op, OpId};
modified radicle/src/cob/store.rs
@@ -10,17 +10,15 @@ use rand::rngs::StdRng;
use rand::{RngCore as _, SeedableRng};
use serde::{Deserialize, Serialize};

-
use crate::cob;
use crate::cob::common::Author;
use crate::cob::op::{Nonce, Op, OpId, Ops};
use crate::cob::CollaborativeObject;
use crate::cob::{ActorId, Create, History, ObjectId, TypeName, Update};
use crate::crypto::PublicKey;
use crate::git;
-
use crate::identity;
-
use crate::identity::Identity;
use crate::prelude::*;
use crate::storage::git as storage;
+
use crate::{cob, identity};

/// History type for standard radicle COBs.
pub const HISTORY_TYPE: &str = "radicle";
@@ -91,7 +89,7 @@ pub enum Error {
/// Storage for collaborative objects of a specific type `T` in a single repository.
pub struct Store<'a, T> {
    whoami: PublicKey,
-
    identity: Identity<git::Oid>,
+
    parent: git::Oid,
    raw: &'a storage::Repository,
    witness: PhantomData<T>,
    rng: StdRng,
@@ -107,10 +105,10 @@ impl<'a, T> Store<'a, T> {
    /// Open a new generic store.
    pub fn open(whoami: PublicKey, store: &'a storage::Repository) -> Result<Self, Error> {
        let rng = rng::std();
-
        let identity = Identity::load(&whoami, store)?;
+
        let identity = store.identity()?;

        Ok(Self {
-
            identity,
+
            parent: identity.current,
            whoami,
            raw: store,
            witness: PhantomData,
@@ -151,7 +149,7 @@ where
        cob::update(
            self.raw,
            signer,
-
            &self.identity,
+
            self.parent,
            signer.public_key(),
            Update {
                object_id,
@@ -175,7 +173,7 @@ where
        let cob = cob::create(
            self.raw,
            signer,
-
            &self.identity,
+
            self.parent,
            signer.public_key(),
            Create {
                history_type: HISTORY_TYPE.to_owned(),
modified radicle/src/identity.rs
@@ -10,11 +10,12 @@ use thiserror::Error;
use crate::crypto;
use crate::crypto::{Signature, Verified};
use crate::git;
-
use crate::storage::{ReadRepository, RemoteId};
+
use crate::storage;
+
use crate::storage::{refs, ReadRepository, RemoteId};

pub use crypto::PublicKey;
pub use did::Did;
-
pub use doc::{Doc, Id, IdError};
+
pub use doc::{Doc, Id, IdError, PayloadError};
pub use project::Project;

/// Untrusted, well-formed input.
@@ -30,18 +31,39 @@ pub enum IdentityError {
    Git(#[from] git2::Error),
    #[error("git: {0}")]
    GitExt(#[from] git::Error),
+
    #[error("identity branches diverge from each other")]
+
    BranchesDiverge,
    #[error("root hash `{0}` does not match project")]
    MismatchedRoot(Oid),
+
    #[error("the identity branch is missing")]
+
    MissingBranch,
    #[error("the document root is missing")]
    MissingRoot,
    #[error("root commit is missing one or more delegate signatures")]
    MissingRootSignatures,
+
    #[error(transparent)]
+
    Payload(#[from] PayloadError),
    #[error("commit signature for {0} is invalid: {1}")]
    InvalidSignature(PublicKey, crypto::Error),
    #[error("threshold not reached: {0} signatures for a threshold of {1}")]
    ThresholdNotReached(usize, usize),
    #[error("identity document error: {0}")]
    Doc(#[from] doc::DocError),
+
    #[error(transparent)]
+
    Refs(#[from] refs::Error),
+
    #[error(transparent)]
+
    Storage(#[from] storage::Error),
+
}
+

+
impl IdentityError {
+
    /// Whether this error is caused by something not being found.
+
    pub fn is_not_found(&self) -> bool {
+
        match self {
+
            Self::Doc(doc) => doc.is_not_found(),
+
            Self::Refs(refs) => refs.is_not_found(),
+
            _ => false,
+
        }
+
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
@@ -62,14 +84,6 @@ pub struct Identity<I> {
    pub signatures: HashMap<PublicKey, Signature>,
}

-
impl radicle_cob::identity::Identity for Identity<Oid> {
-
    type Identifier = Oid;
-

-
    fn content_id(&self) -> Oid {
-
        self.current
-
    }
-
}
-

impl Identity<Oid> {
    pub fn verified(self, id: doc::Id) -> Result<Identity<doc::Id>, IdentityError> {
        // The root hash must be equal to the id.
@@ -94,6 +108,11 @@ impl Identity<Untrusted> {
        repo: &R,
    ) -> Result<Identity<Oid>, IdentityError> {
        let head = Doc::<Untrusted>::head(remote, repo)?;
+

+
        Self::load_at(head, repo)
+
    }
+

+
    pub fn load_at<R: ReadRepository>(head: Oid, repo: &R) -> Result<Identity<Oid>, IdentityError> {
        let mut history = repo.revwalk(head)?.collect::<Vec<_>>();

        // Retrieve root document.
modified radicle/src/rad.rs
@@ -8,11 +8,11 @@ use thiserror::Error;

use crate::crypto::{Signer, Verified};
use crate::git;
-
use crate::identity::doc;
use crate::identity::doc::{DocError, Id};
use crate::identity::project::Project;
+
use crate::identity::{doc, IdentityError};
use crate::storage::git::transport;
-
use crate::storage::git::{ProjectError, Repository, Storage};
+
use crate::storage::git::{Repository, Storage};
use crate::storage::refs::SignedRefs;
use crate::storage::WriteRepository;
use crate::storage::{BranchName, ReadRepository as _, RemoteId};
@@ -26,7 +26,7 @@ pub enum InitError {
    #[error("doc: {0}")]
    Doc(#[from] DocError),
    #[error("project: {0}")]
-
    Project(#[from] storage::git::ProjectError),
+
    Identity(#[from] IdentityError),
    #[error("project payload: {0}")]
    ProjectPayload(String),
    #[error("git: {0}")]
@@ -102,7 +102,7 @@ pub enum ForkError {
    #[error("project `{0}` was not found in storage")]
    NotFound(Id),
    #[error("project identity error: {0}")]
-
    InvalidIdentity(#[from] storage::git::ProjectError),
+
    InvalidIdentity(#[from] IdentityError),
    #[error("project identity document error: {0}")]
    Doc(#[from] DocError),
    #[error("git: invalid reference")]
@@ -199,7 +199,7 @@ pub enum CheckoutError {
    #[error("project `{0}` was not found in storage")]
    NotFound(Id),
    #[error("project error: {0}")]
-
    Project(#[from] ProjectError),
+
    Identity(#[from] IdentityError),
}

/// Checkout a project from storage as a working copy.
modified radicle/src/storage.rs
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;

use crypto::{PublicKey, Signer, Unverified, Verified};
-
pub use git::{ProjectError, VerifyError};
+
pub use git::VerifyError;
pub use radicle_git_ext::Oid;

use crate::collections::HashMap;
@@ -18,7 +18,7 @@ use crate::git::ext as git_ext;
use crate::git::{Qualified, RefError, RefString};
use crate::identity;
use crate::identity::doc::DocError;
-
use crate::identity::{Id, IdError};
+
use crate::identity::{Id, IdError, IdentityError};
use crate::storage::refs::Refs;

use self::refs::SignedRefs;
@@ -98,7 +98,7 @@ pub enum FetchError {
    Storage(#[from] Error),
    // TODO: This should wrap a more specific error.
    #[error("repository head: {0}")]
-
    SetHead(#[from] ProjectError),
+
    SetHead(#[from] IdentityError),
}

pub type RemoteId = PublicKey;
@@ -269,9 +269,9 @@ pub trait ReadStorage {
        &self,
        remote: &RemoteId,
        rid: Id,
-
    ) -> Result<Option<identity::Doc<Verified>>, ProjectError>;
+
    ) -> Result<Option<identity::Doc<Verified>>, IdentityError>;
    /// Check whether storage contains a repository.
-
    fn contains(&self, rid: &Id) -> Result<bool, ProjectError>;
+
    fn contains(&self, rid: &Id) -> Result<bool, IdentityError>;
    /// Get the inventory of repositories hosted under this storage.
    fn inventory(&self) -> Result<Inventory, Error>;
    /// Open or create a read-only repository.
@@ -314,14 +314,14 @@ pub trait ReadRepository {
    /// head using [`ReadRepository::canonical_head`].
    ///
    /// Returns the [`Oid`] as well as the qualified reference name.
-
    fn head(&self) -> Result<(Qualified, Oid), ProjectError>;
+
    fn head(&self) -> Result<(Qualified, Oid), IdentityError>;

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

    /// Get the `reference` for the given `remote`.
    ///
@@ -357,14 +357,14 @@ pub trait ReadRepository {
    fn remotes(&self) -> Result<Remotes<Verified>, refs::Error>;

    /// Get the repository's identity document.
-
    fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), ProjectError>;
+
    fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError>;
}

/// Allows read-write access to a repository.
pub trait WriteRepository: ReadRepository {
    /// Set the repository head to the canonical branch.
    /// This computes the head based on the delegate set.
-
    fn set_head(&self) -> Result<Oid, ProjectError>;
+
    fn set_head(&self) -> Result<Oid, IdentityError>;
    /// Sign the repository's refs under the `refs/rad/sigrefs` branch.
    fn sign_refs<G: Signer>(&self, signer: &G) -> Result<SignedRefs<Verified>, Error>;
    /// Get the underlying git repository.
@@ -386,7 +386,7 @@ where
        self.deref().path_of(rid)
    }

-
    fn contains(&self, rid: &Id) -> Result<bool, ProjectError> {
+
    fn contains(&self, rid: &Id) -> Result<bool, IdentityError> {
        self.deref().contains(rid)
    }

@@ -398,7 +398,7 @@ where
        &self,
        remote: &RemoteId,
        proj: Id,
-
    ) -> Result<Option<identity::Doc<Verified>>, ProjectError> {
+
    ) -> Result<Option<identity::Doc<Verified>>, IdentityError> {
        self.deref().get(remote, proj)
    }

modified radicle/src/storage/git.rs
@@ -11,7 +11,7 @@ use once_cell::sync::Lazy;

use crate::git;
use crate::identity;
-
use crate::identity::{doc, Doc, Id};
+
use crate::identity::{Doc, Id};
use crate::identity::{Identity, IdentityError, Project};
use crate::storage::refs;
use crate::storage::refs::{Refs, SignedRefs};
@@ -58,37 +58,6 @@ impl<'a> TryFrom<git2::Reference<'a>> for Ref {
    }
}

-
// TODO: Is this is the wrong place for this type?
-
#[derive(Error, Debug)]
-
pub enum ProjectError {
-
    #[error("identity branches diverge from each other")]
-
    BranchesDiverge,
-
    #[error("identity branches missing")]
-
    MissingHeads,
-
    #[error("storage error: {0}")]
-
    Storage(#[from] Error),
-
    #[error("identity document error: {0}")]
-
    Doc(#[from] doc::DocError),
-
    #[error("payload error: {0}")]
-
    Payload(#[from] doc::PayloadError),
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("git: {0}")]
-
    GitExt(#[from] git::Error),
-
    #[error("refs: {0}")]
-
    Refs(#[from] refs::Error),
-
}
-

-
impl ProjectError {
-
    /// Whether this error is caused by the project not being found.
-
    pub fn is_not_found(&self) -> bool {
-
        match self {
-
            Self::Doc(doc) => doc.is_not_found(),
-
            _ => false,
-
        }
-
    }
-
}
-

#[derive(Debug, Clone)]
pub struct Storage {
    path: PathBuf,
@@ -105,7 +74,7 @@ impl ReadStorage for Storage {
        paths::repository(&self, rid)
    }

-
    fn contains(&self, rid: &Id) -> Result<bool, ProjectError> {
+
    fn contains(&self, rid: &Id) -> Result<bool, IdentityError> {
        if paths::repository(&self, rid).exists() {
            let _ = self.repository(*rid)?.head()?;
            return Ok(true);
@@ -113,13 +82,13 @@ impl ReadStorage for Storage {
        Ok(false)
    }

-
    fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, ProjectError> {
+
    fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError> {
        let repo = match self.repository(proj) {
            Ok(doc) => doc,
            Err(e) if e.is_not_found() => return Ok(None),
            Err(e) => return Err(e.into()),
        };
-
        match repo.identity_of(remote) {
+
        match repo.identity_doc_of(remote) {
            Ok(doc) => Ok(Some(doc)),
            Err(e) if e.is_not_found() => Ok(None),
            Err(e) => Err(e),
@@ -300,18 +269,24 @@ impl Repository {
        Ok(refs)
    }

-
    pub fn identity(&self, remote: &RemoteId) -> Result<Identity<Oid>, IdentityError> {
+
    pub fn identity_of(&self, remote: &RemoteId) -> Result<Identity<Oid>, IdentityError> {
        Identity::load(remote, self)
    }

-
    pub fn project_of(&self, remote: &RemoteId) -> Result<Project, ProjectError> {
-
        let doc = self.identity_of(remote)?;
+
    pub fn identity(&self) -> Result<Identity<Oid>, IdentityError> {
+
        let head = self.identity_head()?;
+

+
        Identity::load_at(head, self)
+
    }
+

+
    pub fn project_of(&self, remote: &RemoteId) -> Result<Project, IdentityError> {
+
        let doc = self.identity_doc_of(remote)?;
        let proj = doc.project()?;

        Ok(proj)
    }

-
    pub fn identity_of(&self, remote: &RemoteId) -> Result<Doc<Verified>, ProjectError> {
+
    pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc<Verified>, IdentityError> {
        let (doc, _) = identity::Doc::load(remote, self)?;
        let verified = doc.verified()?;

@@ -319,8 +294,18 @@ impl Repository {
    }

    /// Return the canonical identity [`git::Oid`] and document.
-
    pub fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), ProjectError> {
+
    pub fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError> {
+
        let head = self.identity_head()?;
+

+
        Doc::<Unverified>::load_at(head, self)
+
            .map(|(doc, _)| (head, doc))
+
            .map_err(IdentityError::from)
+
    }
+

+
    /// Return the canonical identity branch head.
+
    pub fn identity_head(&self) -> Result<Oid, IdentityError> {
        let mut heads = Vec::new();
+

        for remote in self.remote_ids()? {
            let remote = remote?;
            let oid = Doc::<Unverified>::head(&remote, self)?;
@@ -328,7 +313,7 @@ impl Repository {
            heads.push(oid.into());
        }
        // Keep track of the longest identity branch.
-
        let mut longest = heads.pop().ok_or(ProjectError::MissingHeads)?;
+
        let mut longest = heads.pop().ok_or(IdentityError::MissingBranch)?;

        for head in &heads {
            let base = self.raw().merge_base(*head, longest)?;
@@ -360,13 +345,10 @@ impl Repository {
                //            o (base)
                //            |
                //
-
                return Err(ProjectError::BranchesDiverge);
+
                return Err(IdentityError::BranchesDiverge);
            }
        }
-

-
        Doc::<Unverified>::load_at(longest.into(), self)
-
            .map(|(doc, _)| (longest.into(), doc))
-
            .map_err(ProjectError::from)
+
        Ok(longest.into())
    }

    pub fn remote_ids(
@@ -484,7 +466,7 @@ impl ReadRepository for Repository {
                return Err(VerifyError::MissingRef(remote, name));
            }
            // Verify identity history of remote.
-
            self.identity(&remote)?.verified(self.id)?;
+
            self.identity_of(&remote)?.verified(self.id)?;
        }

        Ok(())
@@ -554,11 +536,11 @@ impl ReadRepository for Repository {
        Ok(Remotes::from_iter(remotes))
    }

-
    fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), ProjectError> {
+
    fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError> {
        Repository::identity_doc(self)
    }

-
    fn head(&self) -> Result<(Qualified, Oid), ProjectError> {
+
    fn head(&self) -> Result<(Qualified, Oid), IdentityError> {
        // 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) {
@@ -568,7 +550,7 @@ impl ReadRepository for Repository {
        self.canonical_head()
    }

-
    fn canonical_head(&self) -> Result<(Qualified, Oid), ProjectError> {
+
    fn canonical_head(&self) -> Result<(Qualified, Oid), IdentityError> {
        // TODO: In the `fork` function for example, we call Repository::project_identity again,
        // This should only be necessary once.
        let (_, doc) = self.identity_doc()?;
@@ -595,7 +577,7 @@ impl ReadRepository for Repository {
}

impl WriteRepository for Repository {
-
    fn set_head(&self) -> Result<Oid, ProjectError> {
+
    fn set_head(&self) -> Result<Oid, IdentityError> {
        let head_ref = refname!("HEAD");
        let (branch_ref, head) = self.canonical_head()?;

modified radicle/src/test/storage.rs
@@ -6,6 +6,7 @@ use radicle_git_ext as git_ext;

use crate::crypto::{Signer, Verified};
use crate::identity::doc::{Doc, Id};
+
use crate::identity::IdentityError;

pub use crate::storage::*;

@@ -42,15 +43,11 @@ impl ReadStorage for MockStorage {
        self.path().join(rid.canonical())
    }

-
    fn contains(&self, rid: &Id) -> Result<bool, ProjectError> {
+
    fn contains(&self, rid: &Id) -> Result<bool, IdentityError> {
        Ok(self.inventory.contains_key(rid))
    }

-
    fn get(
-
        &self,
-
        _remote: &RemoteId,
-
        proj: Id,
-
    ) -> Result<Option<Doc<Verified>>, git::ProjectError> {
+
    fn get(&self, _remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError> {
        Ok(self.inventory.get(&proj).cloned())
    }

@@ -86,11 +83,11 @@ impl ReadRepository for MockRepository {
        Ok(true)
    }

-
    fn head(&self) -> Result<(fmt::Qualified, Oid), ProjectError> {
+
    fn head(&self) -> Result<(fmt::Qualified, Oid), IdentityError> {
        todo!()
    }

-
    fn canonical_head(&self) -> Result<(fmt::Qualified, Oid), ProjectError> {
+
    fn canonical_head(&self) -> Result<(fmt::Qualified, Oid), IdentityError> {
        todo!()
    }

@@ -148,7 +145,7 @@ impl ReadRepository for MockRepository {

    fn identity_doc(
        &self,
-
    ) -> Result<(Oid, crate::identity::Doc<crate::crypto::Unverified>), git::ProjectError> {
+
    ) -> Result<(Oid, crate::identity::Doc<crate::crypto::Unverified>), IdentityError> {
        todo!()
    }
}
@@ -158,7 +155,7 @@ impl WriteRepository for MockRepository {
        todo!()
    }

-
    fn set_head(&self) -> Result<Oid, ProjectError> {
+
    fn set_head(&self) -> Result<Oid, IdentityError> {
        todo!()
    }