Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Implement identity updates
Alexis Sellier committed 3 years ago
commit 4ad85838054621e30c9cc6d397e46168f3804109
parent fa4ac94829e3edc9b6289c0da99ce1fa8f6a0676
10 files changed +340 -98
modified node/src/crypto.rs
@@ -48,7 +48,7 @@ where
}

/// Cryptographic signature.
-
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+
#[derive(PartialEq, Eq, Copy, Clone)]
pub struct Signature(pub ed25519::Signature);

impl fmt::Display for Signature {
@@ -58,6 +58,12 @@ impl fmt::Display for Signature {
    }
}

+
impl fmt::Debug for Signature {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Signature({})", self)
+
    }
+
}
+

#[derive(Error, Debug)]
pub enum SignatureError {
    #[error("invalid multibase string: {0}")]
@@ -106,7 +112,7 @@ impl TryFrom<&[u8]> for Signature {
}

/// The public/verification key.
-
#[derive(Serialize, Deserialize, Eq, Debug, Copy, Clone)]
+
#[derive(Serialize, Deserialize, Eq, Copy, Clone)]
#[serde(into = "String", try_from = "String")]
pub struct PublicKey(pub ed25519::VerificationKey);

@@ -141,6 +147,12 @@ impl From<PublicKey> for String {
    }
}

+
impl fmt::Debug for PublicKey {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "PublicKey({})", self)
+
    }
+
}
+

impl PartialEq for PublicKey {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
modified node/src/git.rs
@@ -4,7 +4,7 @@ use std::str::FromStr;
use git_ref_format as format;

use crate::collections::HashMap;
-
use crate::identity::PublicKey;
+
use crate::crypto::PublicKey;
use crate::storage::refs::Refs;
use crate::storage::RemoteId;

@@ -123,6 +123,22 @@ pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
    Ok(head)
}

+
/// Write a tree with the given blob at the given path.
+
pub fn write_tree<'r>(
+
    path: &Path,
+
    bytes: &[u8],
+
    repo: &'r git2::Repository,
+
) -> Result<git2::Tree<'r>, Error> {
+
    let blob_id = repo.blob(bytes)?;
+
    let mut builder = repo.treebuilder(None)?;
+
    builder.insert(path, blob_id, 0o100_644)?;
+

+
    let tree_id = builder.write()?;
+
    let tree = repo.find_tree(tree_id)?;
+

+
    Ok(tree)
+
}
+

/// Configure a repository's radicle remote.
///
/// Takes the repository in which to configure the remote, the name of the remote, the public
modified node/src/identity.rs
@@ -7,7 +7,8 @@ use std::{ffi::OsString, fmt, str::FromStr};
use serde::{Deserialize, Serialize};
use thiserror::Error;

-
use crate::crypto::{self, Verified};
+
use crate::crypto;
+
use crate::crypto::Verified;
use crate::git;
use crate::serde_ext;
use crate::storage::Remotes;
@@ -114,7 +115,7 @@ pub enum DidError {
    PublicKey(#[from] crypto::PublicKeyError),
}

-
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug, Clone)]
+
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
#[serde(into = "String", try_from = "String")]
pub struct Did(crypto::PublicKey);

@@ -160,6 +161,20 @@ impl fmt::Display for Did {
    }
}

+
impl fmt::Debug for Did {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Did({:?})", self.to_string())
+
    }
+
}
+

+
impl Deref for Did {
+
    type Target = PublicKey;
+

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

/// A stored and verified project.
#[derive(Debug, Clone)]
pub struct Project {
@@ -173,6 +188,23 @@ pub struct Project {
    pub path: PathBuf,
}

+
impl Project {
+
    pub fn delegate(&mut self, name: String, key: crypto::PublicKey) -> bool {
+
        self.doc.delegate(Delegate {
+
            name,
+
            id: Did::from(key),
+
        })
+
    }
+
}
+

+
impl Deref for Project {
+
    type Target = Doc<Verified>;
+

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

#[cfg(test)]
mod test {
    use super::*;
modified node/src/identity/doc.rs
@@ -1,6 +1,8 @@
-
use std::collections::HashMap;
+
use std::collections::{BTreeMap, HashMap};
+
use std::fmt::Write as _;
use std::io;
use std::marker::PhantomData;
+
use std::ops::Deref;
use std::path::Path;

use nonempty::NonEmpty;
@@ -14,7 +16,7 @@ use crate::crypto::{Signature, Unverified, Verified};
use crate::git;
use crate::identity::{Did, Id};
use crate::storage::git::trailers;
-
use crate::storage::{BranchName, ReadRepository, RemoteId};
+
use crate::storage::{BranchName, ReadRepository, RemoteId, WriteRepository, WriteStorage};

pub use crypto::PublicKey;

@@ -65,12 +67,23 @@ impl From<Delegate> for PublicKey {

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
-
pub struct Doc<V> {
+
pub struct Payload {
    pub name: String,
    pub description: String,    // TODO: Make optional.
    pub default_branch: String, // TODO: Make optional.
-
    pub version: u32,           // TODO: Remove this.
-
    pub parent: Option<Oid>,
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+
#[serde(transparent)]
+
// TODO: Restrict values.
+
pub struct Namespace(String);
+

+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Doc<V> {
+
    #[serde(rename = "xyz.radicle.project")]
+
    pub payload: Payload,
+
    #[serde(flatten)]
+
    pub extensions: BTreeMap<Namespace, serde_json::Value>,
    pub delegates: NonEmpty<Delegate>,
    pub threshold: usize,

@@ -78,17 +91,100 @@ pub struct Doc<V> {
}

impl Doc<Verified> {
-
    pub fn encode(&self) -> Result<(Id, Vec<u8>), Error> {
+
    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), Error> {
        let mut buf = Vec::new();
        let mut serializer =
            serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new());

        self.serialize(&mut serializer)?;
-

        let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
-
        let id = Id::from(oid);

-
        Ok((id, buf))
+
        Ok((oid.into(), buf))
+
    }
+

+
    /// Attempt to add a new delegate to the document. Returns `true` if it wasn't there before.
+
    pub fn delegate(&mut self, delegate: Delegate) -> bool {
+
        if self.delegates.iter().all(|d| d.id != delegate.id) {
+
            self.delegates.push(delegate);
+
            return true;
+
        }
+
        false
+
    }
+

+
    pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), Error> {
+
        let (oid, bytes) = self.encode()?;
+
        let sig = signer.sign(&bytes);
+

+
        Ok((oid, sig))
+
    }
+

+
    pub fn create<'r, S: WriteStorage<'r>>(
+
        &self,
+
        remote: &RemoteId,
+
        msg: &str,
+
        storage: &'r S,
+
    ) -> Result<(Id, git::Oid, S::Repository), Error> {
+
        // You can checkout this branch in your working copy with:
+
        //
+
        //      git fetch rad
+
        //      git checkout -b radicle/id remotes/rad/radicle/id
+
        //
+
        let (doc_oid, doc) = self.encode()?;
+
        let id = Id::from(doc_oid);
+
        let repo = storage.repository(&id).unwrap();
+
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
+
        let oid = Doc::commit(remote, &tree, msg, &[], repo.raw())?;
+

+
        drop(tree);
+

+
        Ok((id, oid, repo))
+
    }
+

+
    pub fn update<'r, R: WriteRepository<'r>>(
+
        &self,
+
        remote: &RemoteId,
+
        msg: &str,
+
        signatures: &[(&PublicKey, Signature)],
+
        repo: &R,
+
    ) -> Result<git::Oid, Error> {
+
        let mut msg = format!("{msg}\n\n");
+
        for (key, sig) in signatures {
+
            writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
+
                .expect("in-memory writes don't fail");
+
        }
+

+
        let (_, doc) = self.encode()?;
+
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
+
        let id_ref = format!("refs/remotes/{remote}/{}", &*REFERENCE_NAME);
+
        let head = repo.raw().find_reference(&id_ref)?.peel_to_commit()?;
+
        let oid = Doc::commit(remote, &tree, &msg, &[&head], repo.raw())?;
+

+
        Ok(oid)
+
    }
+

+
    fn commit(
+
        remote: &RemoteId,
+
        tree: &git2::Tree,
+
        msg: &str,
+
        parents: &[&git2::Commit],
+
        repo: &git2::Repository,
+
    ) -> Result<git::Oid, Error> {
+
        let sig = repo
+
            .signature()
+
            .or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
+

+
        let id_ref = format!("refs/remotes/{remote}/{}", &*REFERENCE_NAME);
+
        let oid = repo.commit(Some(&id_ref), &sig, &sig, msg, tree, parents)?;
+

+
        Ok(oid.into())
+
    }
+
}
+

+
impl<V> Deref for Doc<V> {
+
    type Target = Payload;
+

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

@@ -118,11 +214,12 @@ impl Doc<Unverified> {
        delegate: Delegate,
    ) -> Self {
        Self {
-
            name,
-
            description,
-
            default_branch,
-
            version: 1,
-
            parent: None,
+
            payload: Payload {
+
                name,
+
                description,
+
                default_branch,
+
            },
+
            extensions: BTreeMap::new(),
            delegates: NonEmpty::new(delegate),
            threshold: 1,
            verified: PhantomData,
@@ -133,16 +230,16 @@ impl Doc<Unverified> {
        name: String,
        description: String,
        default_branch: BranchName,
-
        parent: Option<Oid>,
        delegates: NonEmpty<Delegate>,
        threshold: usize,
    ) -> Self {
        Self {
-
            name,
-
            description,
-
            default_branch,
-
            version: 1,
-
            parent,
+
            payload: Payload {
+
                name,
+
                description,
+
                default_branch,
+
            },
+
            extensions: BTreeMap::new(),
            delegates,
            threshold,
            verified: PhantomData,
@@ -194,14 +291,6 @@ impl Doc<Unverified> {
                "default branch cannot exceed 255 bytes",
            ));
        }
-
        if let Some(parent) = self.parent {
-
            if parent.is_zero() {
-
                return Err(VerificationError::Parent("parent cannot be zero"));
-
            }
-
        }
-
        if self.version != 1 {
-
            return Err(VerificationError::Version(self.version));
-
        }
        if self.threshold > self.delegates.len() {
            return Err(VerificationError::Threshold(
                self.threshold,
@@ -216,12 +305,9 @@ impl Doc<Unverified> {
        }

        Ok(Doc {
-
            name: self.name,
-
            description: self.description,
+
            payload: self.payload,
+
            extensions: self.extensions,
            delegates: self.delegates,
-
            default_branch: self.default_branch,
-
            parent: self.parent,
-
            version: self.version,
            threshold: self.threshold,
            verified: PhantomData,
        })
@@ -274,6 +360,22 @@ impl<V> Doc<V> {
    }
}

+
#[derive(Error, Debug)]
+
pub enum IdentityError {
+
    #[error("git: {0}")]
+
    GitRaw(#[from] git2::Error),
+
    #[error("git: {0}")]
+
    Git(#[from] git::Error),
+
    #[error("verification: {0}")]
+
    Verification(#[from] VerificationError),
+
    #[error("root hash `{0}` does not match repository")]
+
    MismatchedRoot(Id),
+
    #[error("commit signature for {0} is invalid: {1}")]
+
    InvalidSignature(PublicKey, crypto::Error),
+
    #[error("quorum not reached: {0} signatures for a threshold of {1}")]
+
    QuorumNotReached(usize, usize),
+
}
+

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Identity<V> {
    /// The head of the identity branch. This points to a commit that
@@ -284,6 +386,8 @@ pub struct Identity<V> {
    pub root: Id,
    /// The object id of the current document blob.
    pub current: Oid,
+
    /// Revision number. The initial document has a revision of `0`.
+
    pub revision: u32,
    /// The current document.
    pub doc: Doc<Verified>,
    /// Signatures over this identity.
@@ -297,7 +401,7 @@ impl Identity<Untrusted> {
        id: &Id,
        remote: &RemoteId,
        repo: &R,
-
    ) -> Result<Option<Self>, Error> {
+
    ) -> Result<Option<Identity<Trusted>>, IdentityError> {
        if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
            let mut history = repo.revwalk(head)?.collect::<Vec<_>>();

@@ -306,10 +410,11 @@ impl Identity<Untrusted> {
            let root_blob = Doc::blob_at(root_oid, repo)?.unwrap();
            let root = Id::from(root_blob.id());
            let trusted = Doc::from_json(root_blob.content()).unwrap();
+
            let revision = history.len() as u32;

            // The root hash must be equal to the id.
            if root != *id {
-
                todo!();
+
                return Err(IdentityError::MismatchedRoot(root));
            }

            let mut trusted = trusted.verified()?;
@@ -319,7 +424,7 @@ impl Identity<Untrusted> {
            // Traverse the history chronologically.
            for oid in history.into_iter().rev() {
                let oid = oid?;
-
                let blob = Doc::blob_at(head, repo)?.unwrap();
+
                let blob = Doc::blob_at(oid.into(), repo)?.unwrap();
                let untrusted = Doc::from_json(blob.content()).unwrap();
                let untrusted = untrusted.verified()?;
                let commit = repo.commit(oid.into())?.unwrap();
@@ -328,8 +433,8 @@ impl Identity<Untrusted> {
                // Keys that signed the *current* document version.
                signatures = trailers::parse_signatures(msg).unwrap();
                for (pk, sig) in &signatures {
-
                    if pk.verify(sig, blob.content()).is_err() {
-
                        todo!();
+
                    if let Err(err) = pk.verify(sig, blob.content()) {
+
                        return Err(IdentityError::InvalidSignature(*pk, err));
                    }
                }

@@ -338,19 +443,19 @@ impl Identity<Untrusted> {
                    .iter()
                    .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
                    .count();
-
                // TODO: Check that difference isn't greater than threshold?
                if quorum < trusted.threshold {
-
                    todo!();
+
                    return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
                }

                trusted = untrusted;
                current = blob.id().into();
            }

-
            return Ok(Some(Self {
+
            return Ok(Some(Identity {
                root,
                head,
                current,
+
                revision,
                doc: trusted,
                signatures: signatures.into_iter().collect(),
                verified: PhantomData,
@@ -362,9 +467,112 @@ impl Identity<Untrusted> {

#[cfg(test)]
mod test {
+
    use crate::prelude::Signer;
+
    use crate::rad;
+
    use crate::storage::git::Storage;
+
    use crate::storage::{ReadStorage, WriteStorage};
+
    use crate::test::{crypto, fixtures};
+

    use super::*;
    use quickcheck_macros::quickcheck;

+
    #[test]
+
    fn test_valid_identity() {
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let mut rng = fastrand::Rng::new();
+

+
        let alice = crypto::MockSigner::new(&mut rng);
+
        let bob = crypto::MockSigner::new(&mut rng);
+
        let eve = crypto::MockSigner::new(&mut rng);
+

+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (id, _, _, _) =
+
            fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
+

+
        // Bob and Eve fork the project from Alice.
+
        rad::fork(&id, alice.public_key(), &bob, &storage).unwrap();
+
        rad::fork(&id, alice.public_key(), &eve, &storage).unwrap();
+

+
        // TODO: In some cases we want to get the repo and the project, but don't
+
        // want to have to create a repository object twice. Perhaps there should
+
        // be a way of getting a project from a repo.
+
        let mut proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
+
        let repo = storage.repository(&id).unwrap();
+

+
        // Make a change to the description and sign it.
+
        proj.doc.payload.description += "!";
+
        proj.sign(&alice)
+
            .and_then(|(_, sig)| {
+
                proj.update(
+
                    alice.public_key(),
+
                    "Update description",
+
                    &[(alice.public_key(), sig)],
+
                    &repo,
+
                )
+
            })
+
            .unwrap();
+

+
        // Add Bob as a delegate, and sign it.
+
        proj.delegate("bob".to_owned(), *bob.public_key());
+
        proj.doc.threshold = 2;
+
        proj.sign(&alice)
+
            .and_then(|(_, sig)| {
+
                proj.update(
+
                    alice.public_key(),
+
                    "Add bob",
+
                    &[(alice.public_key(), sig)],
+
                    &repo,
+
                )
+
            })
+
            .unwrap();
+

+
        // Add Eve as a delegate, and sign it.
+
        proj.delegate("eve".to_owned(), *eve.public_key());
+
        proj.sign(&alice)
+
            .and_then(|(_, alice_sig)| {
+
                proj.sign(&bob).and_then(|(_, bob_sig)| {
+
                    proj.update(
+
                        alice.public_key(),
+
                        "Add eve",
+
                        &[(alice.public_key(), alice_sig), (bob.public_key(), bob_sig)],
+
                        &repo,
+
                    )
+
                })
+
            })
+
            .unwrap();
+

+
        // Update description again with signatures by Eve and Bob.
+
        proj.doc.payload.description += "?";
+
        let (current, head) = proj
+
            .sign(&bob)
+
            .and_then(|(_, bob_sig)| {
+
                proj.sign(&eve).and_then(|(blob_id, eve_sig)| {
+
                    proj.update(
+
                        alice.public_key(),
+
                        "Update description",
+
                        &[(bob.public_key(), bob_sig), (eve.public_key(), eve_sig)],
+
                        &repo,
+
                    )
+
                    .map(|head| (blob_id, head))
+
                })
+
            })
+
            .unwrap();
+

+
        let identity = Identity::load(&id, alice.public_key(), &repo)
+
            .unwrap()
+
            .unwrap();
+

+
        assert_eq!(identity.signatures.len(), 2);
+
        assert_eq!(identity.revision, 4);
+
        assert_eq!(identity.root, id);
+
        assert_eq!(identity.current, current);
+
        assert_eq!(identity.head, head);
+
        assert_eq!(identity.doc, proj.doc);
+

+
        let proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
+
        assert_eq!(proj.description, "Acme's repository!?");
+
    }
+

    #[quickcheck]
    fn prop_encode_decode(doc: Doc<Verified>) {
        let (_, bytes) = doc.encode().unwrap();
modified node/src/rad.rs
@@ -39,7 +39,7 @@ pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
    description: &str,
    default_branch: BranchName,
    signer: G,
-
    storage: S,
+
    storage: &'r S,
) -> Result<(Id, SignedRefs<Verified>), InitError> {
    let pk = signer.public_key();
    let delegate = identity::Delegate {
@@ -47,46 +47,16 @@ pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
        name: String::from("anonymous"),
        id: identity::Did::from(*pk),
    };
-
    let (id, doc) = identity::Doc::initial(
+
    let doc = identity::Doc::initial(
        name.to_owned(),
        description.to_owned(),
        default_branch.clone(),
        delegate,
    )
-
    .verified()?
-
    .encode()?;
+
    .verified()?;

-
    let project = storage.repository(&id)?;
+
    let (id, _, project) = doc.create(pk, "Initialize Radicle", storage)?;

-
    {
-
        // Within this scope, redefine `repo` to refer to the project storage,
-
        // since we're going to create the identity file there, and not in there
-
        // working copy.
-
        //
-
        // You can checkout this branch in your working copy with:
-
        //
-
        //      git fetch rad
-
        //      git checkout -b radicle/id remotes/rad/radicle/id
-
        //
-
        let filename = *identity::doc::PATH;
-
        let repo = project.raw();
-
        let tree = {
-
            let doc_blob = repo.blob(&doc)?;
-
            let mut builder = repo.treebuilder(None)?;
-
            builder.insert(filename, doc_blob, 0o100_644)?;
-

-
            assert_eq!(git::Oid::from(doc_blob), *id);
-

-
            let tree_id = builder.write()?;
-
            repo.find_tree(tree_id)
-
        }?;
-
        let sig = repo
-
            .signature()
-
            .or_else(|_| git2::Signature::now("radicle", pk.to_string().as_str()))?;
-

-
        let id_ref = format!("refs/remotes/{pk}/{}", &*identity::doc::REFERENCE_NAME);
-
        let _oid = repo.commit(Some(&id_ref), &sig, &sig, "Initialize Radicle", &tree, &[])?;
-
    }
    git::set_upstream(
        repo,
        REMOTE_NAME,
@@ -242,7 +212,7 @@ mod tests {
        let tempdir = tempfile::tempdir().unwrap();
        let signer = crypto::MockSigner::default();
        let public_key = *signer.public_key();
-
        let mut storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));

        let (proj, refs) = init(
@@ -251,7 +221,7 @@ mod tests {
            "Acme's repo",
            BranchName::from("master"),
            &signer,
-
            &mut storage,
+
            &storage,
        )
        .unwrap();

@@ -279,7 +249,7 @@ mod tests {
        let alice_id = alice.public_key();
        let bob = crypto::MockSigner::new(&mut rng);
        let bob_id = bob.public_key();
-
        let mut storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
        let (original, _) = fixtures::repository(tempdir.path().join("original"));

        // Alice creates a project.
@@ -289,12 +259,12 @@ mod tests {
            "Acme's repo",
            BranchName::from("master"),
            &alice,
-
            &mut storage,
+
            &storage,
        )
        .unwrap();

        // Bob forks it and creates a checkout.
-
        fork(&id, alice_id, &bob, &mut storage).unwrap();
+
        fork(&id, alice_id, &bob, &storage).unwrap();
        checkout(&id, bob_id, tempdir.path().join("copy"), &storage).unwrap();

        let bob_remote = storage.repository(&id).unwrap().remote(bob_id).unwrap();
@@ -310,7 +280,7 @@ mod tests {
        let tempdir = tempfile::tempdir().unwrap();
        let signer = crypto::MockSigner::default();
        let remote_id = signer.public_key();
-
        let mut storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
        let (original, _) = fixtures::repository(tempdir.path().join("original"));

        let (id, _) = init(
@@ -319,7 +289,7 @@ mod tests {
            "Acme's repo",
            BranchName::from("master"),
            &signer,
-
            &mut storage,
+
            &storage,
        )
        .unwrap();

modified node/src/storage.rs
@@ -252,6 +252,8 @@ pub trait ReadRepository<'r> {
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error>;
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error>;
+
    /// Return the project associated with this repository.
+
    fn project(&self) -> Result<Project, Error>;
}

pub trait WriteRepository<'r>: ReadRepository<'r> {
modified node/src/storage/git.rs
@@ -363,6 +363,10 @@ impl<'r> ReadRepository<'r> for Repository {

        Ok(Box::new(iter))
    }
+

+
    fn project(&self) -> Result<Project, Error> {
+
        todo!()
+
    }
}

impl<'r> WriteRepository<'r> for Repository {
modified node/src/test/arbitrary.rs
@@ -179,7 +179,8 @@ impl Arbitrary for MockStorage {
impl Arbitrary for Project {
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
        let doc = Doc::<Verified>::arbitrary(g);
-
        let (id, _) = doc.encode().unwrap();
+
        let (oid, _) = doc.encode().unwrap();
+
        let id = Id::from(oid);
        let remotes = storage::Remotes::arbitrary(g);
        let path = PathBuf::arbitrary(g);

@@ -230,7 +231,6 @@ impl Arbitrary for Doc<Verified> {
        let default_branch = iter::repeat_with(|| rng.alphanumeric())
            .take(rng.usize(1..16))
            .collect();
-
        let parent = None;
        let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate {
            name: iter::repeat_with(|| rng.alphanumeric())
                .take(rng.usize(1..16))
@@ -242,14 +242,8 @@ impl Arbitrary for Doc<Verified> {
        .try_into()
        .unwrap();
        let threshold = delegates.len() / 2 + 1;
-
        let doc: Doc<Unverified> = Doc::new(
-
            name,
-
            description,
-
            default_branch,
-
            parent,
-
            delegates,
-
            threshold,
-
        );
+
        let doc: Doc<Unverified> =
+
            Doc::new(name, description, default_branch, delegates, threshold);

        doc.verified().unwrap()
    }
modified node/src/test/fixtures.rs
@@ -66,7 +66,7 @@ pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
/// Create a new repository at the given path, and initialize it into a project.
pub fn project<'r, P: AsRef<Path>, S: WriteStorage<'r>, G: Signer>(
    path: P,
-
    storage: S,
+
    storage: &'r S,
    signer: G,
) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
    let (repo, head) = repository(path);
modified node/src/test/storage.rs
@@ -125,6 +125,10 @@ impl ReadRepository<'_> for MockRepository {
    fn references(&self, _remote: &RemoteId) -> Result<crate::storage::refs::Refs, Error> {
        todo!()
    }
+

+
    fn project(&self) -> Result<Project, Error> {
+
        todo!()
+
    }
}

impl WriteRepository<'_> for MockRepository {