node: Implement identity updates
Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
fa4ac94829
commit
4ad8583805
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue