node: Implement identity updates

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-09-18 21:48:43 +02:00
parent fa4ac94829
commit 4ad8583805
No known key found for this signature in database
10 changed files with 340 additions and 98 deletions

View File

@ -48,7 +48,7 @@ where
} }
/// Cryptographic signature. /// Cryptographic signature.
#[derive(PartialEq, Eq, Debug, Copy, Clone)] #[derive(PartialEq, Eq, Copy, Clone)]
pub struct Signature(pub ed25519::Signature); pub struct Signature(pub ed25519::Signature);
impl fmt::Display for 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)] #[derive(Error, Debug)]
pub enum SignatureError { pub enum SignatureError {
#[error("invalid multibase string: {0}")] #[error("invalid multibase string: {0}")]
@ -106,7 +112,7 @@ impl TryFrom<&[u8]> for Signature {
} }
/// The public/verification key. /// The public/verification key.
#[derive(Serialize, Deserialize, Eq, Debug, Copy, Clone)] #[derive(Serialize, Deserialize, Eq, Copy, Clone)]
#[serde(into = "String", try_from = "String")] #[serde(into = "String", try_from = "String")]
pub struct PublicKey(pub ed25519::VerificationKey); 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 { impl PartialEq for PublicKey {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.0 == other.0 self.0 == other.0

View File

@ -4,7 +4,7 @@ use std::str::FromStr;
use git_ref_format as format; use git_ref_format as format;
use crate::collections::HashMap; use crate::collections::HashMap;
use crate::identity::PublicKey; use crate::crypto::PublicKey;
use crate::storage::refs::Refs; use crate::storage::refs::Refs;
use crate::storage::RemoteId; use crate::storage::RemoteId;
@ -123,6 +123,22 @@ pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
Ok(head) 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. /// Configure a repository's radicle remote.
/// ///
/// Takes the repository in which to configure the remote, the name of the remote, the public /// Takes the repository in which to configure the remote, the name of the remote, the public

View File

@ -7,7 +7,8 @@ use std::{ffi::OsString, fmt, str::FromStr};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::crypto::{self, Verified}; use crate::crypto;
use crate::crypto::Verified;
use crate::git; use crate::git;
use crate::serde_ext; use crate::serde_ext;
use crate::storage::Remotes; use crate::storage::Remotes;
@ -114,7 +115,7 @@ pub enum DidError {
PublicKey(#[from] crypto::PublicKeyError), 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")] #[serde(into = "String", try_from = "String")]
pub struct Did(crypto::PublicKey); 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. /// A stored and verified project.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Project { pub struct Project {
@ -173,6 +188,23 @@ pub struct Project {
pub path: PathBuf, 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -1,6 +1,8 @@
use std::collections::HashMap; use std::collections::{BTreeMap, HashMap};
use std::fmt::Write as _;
use std::io; use std::io;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use nonempty::NonEmpty; use nonempty::NonEmpty;
@ -14,7 +16,7 @@ use crate::crypto::{Signature, Unverified, Verified};
use crate::git; use crate::git;
use crate::identity::{Did, Id}; use crate::identity::{Did, Id};
use crate::storage::git::trailers; use crate::storage::git::trailers;
use crate::storage::{BranchName, ReadRepository, RemoteId}; use crate::storage::{BranchName, ReadRepository, RemoteId, WriteRepository, WriteStorage};
pub use crypto::PublicKey; pub use crypto::PublicKey;
@ -65,12 +67,23 @@ impl From<Delegate> for PublicKey {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Doc<V> { pub struct Payload {
pub name: String, pub name: String,
pub description: String, // TODO: Make optional. pub description: String, // TODO: Make optional.
pub default_branch: 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 delegates: NonEmpty<Delegate>,
pub threshold: usize, pub threshold: usize,
@ -78,17 +91,100 @@ pub struct Doc<V> {
} }
impl Doc<Verified> { 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 buf = Vec::new();
let mut serializer = let mut serializer =
serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new()); serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new());
self.serialize(&mut serializer)?; self.serialize(&mut serializer)?;
let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?; 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, delegate: Delegate,
) -> Self { ) -> Self {
Self { Self {
payload: Payload {
name, name,
description, description,
default_branch, default_branch,
version: 1, },
parent: None, extensions: BTreeMap::new(),
delegates: NonEmpty::new(delegate), delegates: NonEmpty::new(delegate),
threshold: 1, threshold: 1,
verified: PhantomData, verified: PhantomData,
@ -133,16 +230,16 @@ impl Doc<Unverified> {
name: String, name: String,
description: String, description: String,
default_branch: BranchName, default_branch: BranchName,
parent: Option<Oid>,
delegates: NonEmpty<Delegate>, delegates: NonEmpty<Delegate>,
threshold: usize, threshold: usize,
) -> Self { ) -> Self {
Self { Self {
payload: Payload {
name, name,
description, description,
default_branch, default_branch,
version: 1, },
parent, extensions: BTreeMap::new(),
delegates, delegates,
threshold, threshold,
verified: PhantomData, verified: PhantomData,
@ -194,14 +291,6 @@ impl Doc<Unverified> {
"default branch cannot exceed 255 bytes", "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() { if self.threshold > self.delegates.len() {
return Err(VerificationError::Threshold( return Err(VerificationError::Threshold(
self.threshold, self.threshold,
@ -216,12 +305,9 @@ impl Doc<Unverified> {
} }
Ok(Doc { Ok(Doc {
name: self.name, payload: self.payload,
description: self.description, extensions: self.extensions,
delegates: self.delegates, delegates: self.delegates,
default_branch: self.default_branch,
parent: self.parent,
version: self.version,
threshold: self.threshold, threshold: self.threshold,
verified: PhantomData, 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Identity<V> { pub struct Identity<V> {
/// The head of the identity branch. This points to a commit that /// The head of the identity branch. This points to a commit that
@ -284,6 +386,8 @@ pub struct Identity<V> {
pub root: Id, pub root: Id,
/// The object id of the current document blob. /// The object id of the current document blob.
pub current: Oid, pub current: Oid,
/// Revision number. The initial document has a revision of `0`.
pub revision: u32,
/// The current document. /// The current document.
pub doc: Doc<Verified>, pub doc: Doc<Verified>,
/// Signatures over this identity. /// Signatures over this identity.
@ -297,7 +401,7 @@ impl Identity<Untrusted> {
id: &Id, id: &Id,
remote: &RemoteId, remote: &RemoteId,
repo: &R, repo: &R,
) -> Result<Option<Self>, Error> { ) -> Result<Option<Identity<Trusted>>, IdentityError> {
if let Some(head) = Doc::<Untrusted>::head(remote, repo)? { if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
let mut history = repo.revwalk(head)?.collect::<Vec<_>>(); 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_blob = Doc::blob_at(root_oid, repo)?.unwrap();
let root = Id::from(root_blob.id()); let root = Id::from(root_blob.id());
let trusted = Doc::from_json(root_blob.content()).unwrap(); let trusted = Doc::from_json(root_blob.content()).unwrap();
let revision = history.len() as u32;
// The root hash must be equal to the id. // The root hash must be equal to the id.
if root != *id { if root != *id {
todo!(); return Err(IdentityError::MismatchedRoot(root));
} }
let mut trusted = trusted.verified()?; let mut trusted = trusted.verified()?;
@ -319,7 +424,7 @@ impl Identity<Untrusted> {
// Traverse the history chronologically. // Traverse the history chronologically.
for oid in history.into_iter().rev() { for oid in history.into_iter().rev() {
let oid = oid?; 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 = Doc::from_json(blob.content()).unwrap();
let untrusted = untrusted.verified()?; let untrusted = untrusted.verified()?;
let commit = repo.commit(oid.into())?.unwrap(); let commit = repo.commit(oid.into())?.unwrap();
@ -328,8 +433,8 @@ impl Identity<Untrusted> {
// Keys that signed the *current* document version. // Keys that signed the *current* document version.
signatures = trailers::parse_signatures(msg).unwrap(); signatures = trailers::parse_signatures(msg).unwrap();
for (pk, sig) in &signatures { for (pk, sig) in &signatures {
if pk.verify(sig, blob.content()).is_err() { if let Err(err) = pk.verify(sig, blob.content()) {
todo!(); return Err(IdentityError::InvalidSignature(*pk, err));
} }
} }
@ -338,19 +443,19 @@ impl Identity<Untrusted> {
.iter() .iter()
.filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key))) .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
.count(); .count();
// TODO: Check that difference isn't greater than threshold?
if quorum < trusted.threshold { if quorum < trusted.threshold {
todo!(); return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
} }
trusted = untrusted; trusted = untrusted;
current = blob.id().into(); current = blob.id().into();
} }
return Ok(Some(Self { return Ok(Some(Identity {
root, root,
head, head,
current, current,
revision,
doc: trusted, doc: trusted,
signatures: signatures.into_iter().collect(), signatures: signatures.into_iter().collect(),
verified: PhantomData, verified: PhantomData,
@ -362,9 +467,112 @@ impl Identity<Untrusted> {
#[cfg(test)] #[cfg(test)]
mod 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 super::*;
use quickcheck_macros::quickcheck; 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] #[quickcheck]
fn prop_encode_decode(doc: Doc<Verified>) { fn prop_encode_decode(doc: Doc<Verified>) {
let (_, bytes) = doc.encode().unwrap(); let (_, bytes) = doc.encode().unwrap();

View File

@ -39,7 +39,7 @@ pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
description: &str, description: &str,
default_branch: BranchName, default_branch: BranchName,
signer: G, signer: G,
storage: S, storage: &'r S,
) -> Result<(Id, SignedRefs<Verified>), InitError> { ) -> Result<(Id, SignedRefs<Verified>), InitError> {
let pk = signer.public_key(); let pk = signer.public_key();
let delegate = identity::Delegate { let delegate = identity::Delegate {
@ -47,46 +47,16 @@ pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
name: String::from("anonymous"), name: String::from("anonymous"),
id: identity::Did::from(*pk), id: identity::Did::from(*pk),
}; };
let (id, doc) = identity::Doc::initial( let doc = identity::Doc::initial(
name.to_owned(), name.to_owned(),
description.to_owned(), description.to_owned(),
default_branch.clone(), default_branch.clone(),
delegate, delegate,
) )
.verified()? .verified()?;
.encode()?;
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( git::set_upstream(
repo, repo,
REMOTE_NAME, REMOTE_NAME,
@ -242,7 +212,7 @@ mod tests {
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let signer = crypto::MockSigner::default(); let signer = crypto::MockSigner::default();
let public_key = *signer.public_key(); 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 (repo, _) = fixtures::repository(tempdir.path().join("working"));
let (proj, refs) = init( let (proj, refs) = init(
@ -251,7 +221,7 @@ mod tests {
"Acme's repo", "Acme's repo",
BranchName::from("master"), BranchName::from("master"),
&signer, &signer,
&mut storage, &storage,
) )
.unwrap(); .unwrap();
@ -279,7 +249,7 @@ mod tests {
let alice_id = alice.public_key(); let alice_id = alice.public_key();
let bob = crypto::MockSigner::new(&mut rng); let bob = crypto::MockSigner::new(&mut rng);
let bob_id = bob.public_key(); 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")); let (original, _) = fixtures::repository(tempdir.path().join("original"));
// Alice creates a project. // Alice creates a project.
@ -289,12 +259,12 @@ mod tests {
"Acme's repo", "Acme's repo",
BranchName::from("master"), BranchName::from("master"),
&alice, &alice,
&mut storage, &storage,
) )
.unwrap(); .unwrap();
// Bob forks it and creates a checkout. // 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(); checkout(&id, bob_id, tempdir.path().join("copy"), &storage).unwrap();
let bob_remote = storage.repository(&id).unwrap().remote(bob_id).unwrap(); let bob_remote = storage.repository(&id).unwrap().remote(bob_id).unwrap();
@ -310,7 +280,7 @@ mod tests {
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let signer = crypto::MockSigner::default(); let signer = crypto::MockSigner::default();
let remote_id = signer.public_key(); 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 (original, _) = fixtures::repository(tempdir.path().join("original"));
let (id, _) = init( let (id, _) = init(
@ -319,7 +289,7 @@ mod tests {
"Acme's repo", "Acme's repo",
BranchName::from("master"), BranchName::from("master"),
&signer, &signer,
&mut storage, &storage,
) )
.unwrap(); .unwrap();

View File

@ -252,6 +252,8 @@ pub trait ReadRepository<'r> {
fn references(&self, remote: &RemoteId) -> Result<Refs, Error>; fn references(&self, remote: &RemoteId) -> Result<Refs, Error>;
fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>; fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
fn remotes(&'r self) -> Result<Self::Remotes, git2::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> { pub trait WriteRepository<'r>: ReadRepository<'r> {

View File

@ -363,6 +363,10 @@ impl<'r> ReadRepository<'r> for Repository {
Ok(Box::new(iter)) Ok(Box::new(iter))
} }
fn project(&self) -> Result<Project, Error> {
todo!()
}
} }
impl<'r> WriteRepository<'r> for Repository { impl<'r> WriteRepository<'r> for Repository {

View File

@ -179,7 +179,8 @@ impl Arbitrary for MockStorage {
impl Arbitrary for Project { impl Arbitrary for Project {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let doc = Doc::<Verified>::arbitrary(g); 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 remotes = storage::Remotes::arbitrary(g);
let path = PathBuf::arbitrary(g); let path = PathBuf::arbitrary(g);
@ -230,7 +231,6 @@ impl Arbitrary for Doc<Verified> {
let default_branch = iter::repeat_with(|| rng.alphanumeric()) let default_branch = iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(1..16)) .take(rng.usize(1..16))
.collect(); .collect();
let parent = None;
let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate { let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate {
name: iter::repeat_with(|| rng.alphanumeric()) name: iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(1..16)) .take(rng.usize(1..16))
@ -242,14 +242,8 @@ impl Arbitrary for Doc<Verified> {
.try_into() .try_into()
.unwrap(); .unwrap();
let threshold = delegates.len() / 2 + 1; let threshold = delegates.len() / 2 + 1;
let doc: Doc<Unverified> = Doc::new( let doc: Doc<Unverified> =
name, Doc::new(name, description, default_branch, delegates, threshold);
description,
default_branch,
parent,
delegates,
threshold,
);
doc.verified().unwrap() doc.verified().unwrap()
} }

View File

@ -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. /// 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>( pub fn project<'r, P: AsRef<Path>, S: WriteStorage<'r>, G: Signer>(
path: P, path: P,
storage: S, storage: &'r S,
signer: G, signer: G,
) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> { ) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
let (repo, head) = repository(path); let (repo, head) = repository(path);

View File

@ -125,6 +125,10 @@ impl ReadRepository<'_> for MockRepository {
fn references(&self, _remote: &RemoteId) -> Result<crate::storage::refs::Refs, Error> { fn references(&self, _remote: &RemoteId) -> Result<crate::storage::refs::Refs, Error> {
todo!() todo!()
} }
fn project(&self) -> Result<Project, Error> {
todo!()
}
} }
impl WriteRepository<'_> for MockRepository { impl WriteRepository<'_> for MockRepository {