From c3b7a6fbd7ad4c70484222306ffaa2ef25781363 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Sat, 3 Sep 2022 19:07:37 +0200 Subject: [PATCH] node: Implement project retrieval Signed-off-by: Alexis Sellier --- node/src/git.rs | 8 ++--- node/src/identity.rs | 24 +++++++++++--- node/src/protocol.rs | 6 ++-- node/src/rad.rs | 21 ++++++++++-- node/src/storage.rs | 6 ++-- node/src/storage/git.rs | 67 +++++++++++++++++++++++++------------- node/src/test/arbitrary.rs | 50 +++++++++++++++++++++++++++- node/src/test/storage.rs | 14 ++++---- node/src/test/tests.rs | 4 +-- 9 files changed, 152 insertions(+), 48 deletions(-) diff --git a/node/src/git.rs b/node/src/git.rs index dcfa7ab5..4d43c441 100644 --- a/node/src/git.rs +++ b/node/src/git.rs @@ -1,16 +1,16 @@ use std::str::FromStr; use git_ref_format as format; -use radicle_git_ext as git_ext; use crate::collections::HashMap; use crate::identity::UserId; use crate::storage::refs::Refs; use crate::storage::RemoteId; -pub use git_ext::Oid; +pub use ext::Oid; pub use git_ref_format::{refname, RefStr, RefString}; pub use git_url::Url; +pub use radicle_git_ext as ext; /// Default port of the `git` transport protocol. pub const PROTOCOL_PORT: u16 = 9418; @@ -118,14 +118,14 @@ pub fn set_upstream( let branch_merge = format!("branch.{}.merge", branch); config.remove_multivar(&branch_remote, ".*").or_else(|e| { - if git_ext::is_not_found_err(&e) { + if ext::is_not_found_err(&e) { Ok(()) } else { Err(e) } })?; config.remove_multivar(&branch_merge, ".*").or_else(|e| { - if git_ext::is_not_found_err(&e) { + if ext::is_not_found_err(&e) { Ok(()) } else { Err(e) diff --git a/node/src/identity.rs b/node/src/identity.rs index 9f1ecd7c..3374e154 100644 --- a/node/src/identity.rs +++ b/node/src/identity.rs @@ -7,8 +7,9 @@ use radicle_git_ext::Oid; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::crypto; +use crate::crypto::{self, Verified}; use crate::hash; +use crate::storage::Remotes; pub static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new("Radicle.toml")); @@ -86,6 +87,17 @@ impl fmt::Display for Did { } } +/// A stored and verified project. +#[derive(Debug, Clone)] +pub struct Project { + /// The project identifier. + pub id: ProjId, + /// The latest project identity document. + pub doc: Doc, + /// The project remotes. + pub remotes: Remotes, +} + #[derive(Error, Debug)] pub enum DocError { #[error("toml: {0}")] @@ -94,20 +106,20 @@ pub enum DocError { Io(#[from] io::Error), } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Delegate { pub name: String, pub id: Did, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Doc { pub name: String, pub description: String, pub default_branch: String, pub version: u32, pub parent: Option, - pub delegate: NonEmpty, + pub delegates: NonEmpty, } impl Doc { @@ -120,6 +132,10 @@ impl Doc { Ok(id) } + + pub fn from_toml(bytes: &[u8]) -> Result { + toml::from_slice(bytes) + } } #[cfg(test)] diff --git a/node/src/protocol.rs b/node/src/protocol.rs index 52b1f343..bd9ac9a0 100644 --- a/node/src/protocol.rs +++ b/node/src/protocol.rs @@ -19,12 +19,12 @@ use crate::address_manager::AddressManager; use crate::clock::RefClock; use crate::collections::{HashMap, HashSet}; use crate::crypto; -use crate::identity::{ProjId, UserId}; +use crate::identity::{ProjId, Project, UserId}; use crate::protocol::config::ProjectTracking; use crate::protocol::message::Message; use crate::protocol::peer::{Peer, PeerError, PeerState}; use crate::storage::{self, ReadRepository, WriteRepository}; -use crate::storage::{Inventory, ReadStorage, Remotes, WriteStorage}; +use crate::storage::{Inventory, ReadStorage, WriteStorage}; pub use crate::protocol::config::{Config, Network}; @@ -546,7 +546,7 @@ impl Iterator for Protocol { #[derive(Debug)] pub struct Lookup { /// Whether the project was found locally or not. - pub local: Option>, + pub local: Option, /// A list of remote peers on which the project is known to exist. pub remote: Vec, } diff --git a/node/src/rad.rs b/node/src/rad.rs index 4c6681b8..ecf6c4f4 100644 --- a/node/src/rad.rs +++ b/node/src/rad.rs @@ -52,7 +52,7 @@ pub fn init( default_branch: default_branch.clone(), version: 1, parent: None, - delegate: NonEmpty::new(delegate), + delegates: NonEmpty::new(delegate), }; let filename = *identity::IDENTITY_PATH; @@ -123,7 +123,9 @@ pub fn init( mod tests { use super::*; use crate::git; + use crate::identity::{Delegate, Did}; use crate::storage::git::Storage; + use crate::storage::ReadStorage; use crate::test::crypto; #[test] @@ -137,7 +139,7 @@ mod tests { let head = git::commit(&repo, &head, "Second commit", "anonymous").unwrap(); let _branch = repo.branch("master", &head, false).unwrap(); - let (_id, _refs) = init( + let (id, refs) = init( &repo, "acme", "Acme's repo", @@ -145,5 +147,20 @@ mod tests { &mut storage, ) .unwrap(); + + let project = storage.get(&id).unwrap().unwrap(); + + assert_eq!(project.remotes[storage.user_id()].refs, refs); + assert_eq!(project.id, id); + assert_eq!(project.doc.name, "acme"); + assert_eq!(project.doc.description, "Acme's repo"); + assert_eq!(project.doc.default_branch, BranchName::from("master")); + assert_eq!( + project.doc.delegates.first(), + &Delegate { + name: String::from("anonymous"), + id: Did::from(*storage.user_id()), + } + ); } } diff --git a/node/src/storage.rs b/node/src/storage.rs index 5e2c171b..4b78ec4c 100644 --- a/node/src/storage.rs +++ b/node/src/storage.rs @@ -18,7 +18,7 @@ use crate::crypto::{self, Unverified, Verified}; use crate::git::Url; use crate::git::{RefError, RefStr}; use crate::identity; -use crate::identity::{ProjId, ProjIdError, UserId}; +use crate::identity::{ProjId, ProjIdError, Project, UserId}; use crate::storage::refs::Refs; use self::refs::SignedRefs; @@ -168,7 +168,7 @@ impl Remote { pub trait ReadStorage { fn user_id(&self) -> &UserId; fn url(&self) -> Url; - fn get(&self, proj: &ProjId) -> Result>, Error>; + fn get(&self, proj: &ProjId) -> Result, Error>; fn inventory(&self) -> Result; } @@ -215,7 +215,7 @@ where self.deref().inventory() } - fn get(&self, proj: &ProjId) -> Result>, Error> { + fn get(&self, proj: &ProjId) -> Result, Error> { self.deref().get(proj) } } diff --git a/node/src/storage/git.rs b/node/src/storage/git.rs index 17106dcd..8eb74baf 100644 --- a/node/src/storage/git.rs +++ b/node/src/storage/git.rs @@ -5,14 +5,14 @@ use std::{fmt, fs, io}; use git_ref_format::refspec; use once_cell::sync::Lazy; -use radicle_git_ext as git_ext; pub use radicle_git_ext::Oid; use crate::collections::HashMap; use crate::crypto::{Signer, Verified}; use crate::git; -use crate::identity::{ProjId, UserId}; +use crate::identity::{self, IDENTITY_PATH}; +use crate::identity::{ProjId, Project, UserId}; use crate::storage::refs; use crate::storage::refs::{Refs, SignedRefs}; use crate::storage::{ @@ -21,8 +21,7 @@ use crate::storage::{ use super::RemoteId; -pub static RADICLE_ID_REF: Lazy = - Lazy::new(|| refspec::pattern!("heads/radicle/id")); +pub static RADICLE_ID_REF: Lazy = Lazy::new(|| git::refname!("heads/radicle/id")); pub static REMOTES_GLOB: Lazy = Lazy::new(|| refspec::pattern!("refs/remotes/*")); pub static SIGNATURES_GLOB: Lazy = @@ -52,19 +51,26 @@ impl ReadStorage for Storage { } } - fn get(&self, id: &ProjId) -> Result>, Error> { + fn get(&self, id: &ProjId) -> Result, Error> { // TODO: Don't create a repo here if it doesn't exist? - // Perhaps for checking we could have a `contains` method? - match self.repository(id) { - Ok(r) => { - let remotes = r.remotes()?; - if remotes.is_empty() { - Ok(None) - } else { - Ok(Some(remotes)) - } - } - Err(e) => Err(e), + // Perhaps for checking we could have a `contains` method? + let local = self.user_id(); + let repo = self.repository(id)?; + + if let Some(doc) = repo.identity(local)? { + let remotes = repo.remotes()?; + + // TODO: We should check that there is at least one remote, which is + // the one of the local user, otherwise it means the project is in + // an corrupted state. + + Ok(Some(Project { + id: id.clone(), + doc, + remotes, + })) + } else { + Ok(None) } } @@ -163,7 +169,7 @@ pub struct Repository { impl Repository { pub fn open>(path: P) -> Result { let backend = match git2::Repository::open_bare(path.as_ref()) { - Err(e) if git_ext::is_not_found_err(&e) => { + Err(e) if git::ext::is_not_found_err(&e) => { let backend = git2::Repository::init_opts( path, git2::RepositoryInitOptions::new() @@ -196,6 +202,23 @@ impl Repository { } Ok(()) } + + pub fn identity(&self, remote: &RemoteId) -> Result, refs::Error> { + let oid = if let Some(oid) = self.reference_oid(remote, &RADICLE_ID_REF)? { + oid + } else { + return Ok(None); + }; + + let doc = match self.blob_at(oid, Path::new(&*IDENTITY_PATH)) { + Err(git::ext::Error::NotFound(_)) => return Ok(None), + Err(e) => return Err(e.into()), + Ok(doc) => doc, + }; + let doc = identity::Doc::from_toml(doc.content()).unwrap(); + + Ok(Some(doc)) + } } impl ReadRepository for Repository { @@ -203,8 +226,8 @@ impl ReadRepository for Repository { self.backend.path() } - fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result, git_ext::Error> { - git_ext::Blob::At { + fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result, git::ext::Error> { + git::ext::Blob::At { object: oid.into(), path, } @@ -218,7 +241,7 @@ impl ReadRepository for Repository { ) -> Result, git2::Error> { let name = format!("refs/remotes/{remote}/{name}"); self.backend.find_reference(&name).map(Some).or_else(|e| { - if git_ext::is_not_found_err(&e) { + if git::ext::is_not_found_err(&e) { Ok(None) } else { Err(e) @@ -228,10 +251,10 @@ impl ReadRepository for Repository { fn reference_oid( &self, - user: &RemoteId, + remote: &RemoteId, reference: &git::RefStr, ) -> Result, git2::Error> { - let reference = self.reference(user, reference)?; + let reference = self.reference(remote, reference)?; Ok(reference.and_then(|r| r.target().map(|o| o.into()))) } diff --git a/node/src/test/arbitrary.rs b/node/src/test/arbitrary.rs index beed679d..de73ee90 100644 --- a/node/src/test/arbitrary.rs +++ b/node/src/test/arbitrary.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashSet}; use std::hash::Hash; use std::ops::RangeBounds; +use nonempty::NonEmpty; use quickcheck::Arbitrary; use crate::collections::HashMap; @@ -9,7 +10,7 @@ use crate::crypto::{self, Signer}; use crate::crypto::{PublicKey, SecretKey}; use crate::git; use crate::hash; -use crate::identity::ProjId; +use crate::identity::{Delegate, Did, Doc, ProjId, Project}; use crate::storage; use crate::storage::refs::Refs; use crate::test::storage::MockStorage; @@ -49,6 +50,53 @@ impl Arbitrary for MockStorage { } } +impl Arbitrary for Project { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut buf = Vec::new(); + let doc = Doc::arbitrary(g); + let id = doc.write(&mut buf).unwrap(); + let remotes = storage::Remotes::arbitrary(g); + + Self { id, doc, remotes } + } +} + +impl Arbitrary for Did { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self::from(PublicKey::arbitrary(g)) + } +} + +impl Arbitrary for Delegate { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + name: String::arbitrary(g), + id: Did::arbitrary(g), + } + } +} + +impl Arbitrary for Doc { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let name = String::arbitrary(g); + let description = String::arbitrary(g); + let default_branch = String::arbitrary(g); + let version = u32::arbitrary(g); + let parent = None; + let delegate = Delegate::arbitrary(g); + let delegates = NonEmpty::new(delegate); + + Self { + name, + description, + default_branch, + version, + parent, + delegates, + } + } +} + impl Arbitrary for Refs { fn arbitrary(g: &mut quickcheck::Gen) -> Self { let mut refs: BTreeMap = BTreeMap::new(); diff --git a/node/src/test/storage.rs b/node/src/test/storage.rs index f3bb6f86..5def510a 100644 --- a/node/src/test/storage.rs +++ b/node/src/test/storage.rs @@ -2,7 +2,7 @@ use git_url::Url; use crate::crypto::Verified; use crate::git; -use crate::identity::{ProjId, UserId}; +use crate::identity::{ProjId, Project, UserId}; use crate::storage::refs; use crate::storage::{ Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, WriteRepository, WriteStorage, @@ -10,11 +10,11 @@ use crate::storage::{ #[derive(Clone, Debug)] pub struct MockStorage { - pub inventory: Vec<(ProjId, Remotes)>, + pub inventory: Vec, } impl MockStorage { - pub fn new(inventory: Vec<(ProjId, Remotes)>) -> Self { + pub fn new(inventory: Vec) -> Self { Self { inventory } } @@ -38,9 +38,9 @@ impl ReadStorage for MockStorage { } } - fn get(&self, proj: &ProjId) -> Result>, Error> { - if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) { - return Ok(Some(refs.clone())); + fn get(&self, proj: &ProjId) -> Result, Error> { + if let Some(proj) = self.inventory.iter().find(|p| p.id == *proj) { + return Ok(Some(proj.clone())); } Ok(None) } @@ -49,7 +49,7 @@ impl ReadStorage for MockStorage { let inventory = self .inventory .iter() - .map(|(id, _)| id.clone()) + .map(|proj| proj.id.clone()) .collect::>(); Ok(inventory) diff --git a/node/src/test/tests.rs b/node/src/test/tests.rs index 900d3ab3..14bd611d 100644 --- a/node/src/test/tests.rs +++ b/node/src/test/tests.rs @@ -347,9 +347,9 @@ fn prop_inventory_exchange_dense() { (bob_inv.inventory, bob.id()), (eve_inv.inventory, eve.id()), ] { - for (proj, _) in inv { + for proj in inv { routing - .entry(proj.clone()) + .entry(proj.id.clone()) .or_insert_with(|| HashSet::with_hasher(rng.clone().into())) .insert(*peer); }