node: Implement project retrieval

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier 2022-09-03 19:07:37 +02:00
parent 910cd74294
commit c3b7a6fbd7
No known key found for this signature in database
9 changed files with 152 additions and 48 deletions

View File

@ -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)

View File

@ -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<Verified>,
}
#[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<Oid>,
pub delegate: NonEmpty<Delegate>,
pub delegates: NonEmpty<Delegate>,
}
impl Doc {
@ -120,6 +132,10 @@ impl Doc {
Ok(id)
}
pub fn from_toml(bytes: &[u8]) -> Result<Self, toml::de::Error> {
toml::from_slice(bytes)
}
}
#[cfg(test)]

View File

@ -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<S, T, G> Iterator for Protocol<S, T, G> {
#[derive(Debug)]
pub struct Lookup {
/// Whether the project was found locally or not.
pub local: Option<Remotes<crypto::Verified>>,
pub local: Option<Project>,
/// A list of remote peers on which the project is known to exist.
pub remote: Vec<NodeId>,
}

View File

@ -52,7 +52,7 @@ pub fn init<S: storage::WriteStorage>(
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<S: storage::WriteStorage>(
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()),
}
);
}
}

View File

@ -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<Verified> {
pub trait ReadStorage {
fn user_id(&self) -> &UserId;
fn url(&self) -> Url;
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error>;
fn get(&self, proj: &ProjId) -> Result<Option<Project>, Error>;
fn inventory(&self) -> Result<Inventory, Error>;
}
@ -215,7 +215,7 @@ where
self.deref().inventory()
}
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
fn get(&self, proj: &ProjId) -> Result<Option<Project>, Error> {
self.deref().get(proj)
}
}

View File

@ -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<refspec::PatternString> =
Lazy::new(|| refspec::pattern!("heads/radicle/id"));
pub static RADICLE_ID_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("heads/radicle/id"));
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
Lazy::new(|| refspec::pattern!("refs/remotes/*"));
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
@ -52,19 +51,26 @@ impl ReadStorage for Storage {
}
}
fn get(&self, id: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
fn get(&self, id: &ProjId) -> Result<Option<Project>, 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<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
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<Option<identity::Doc>, 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<git2::Blob<'a>, git_ext::Error> {
git_ext::Blob::At {
fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git::ext::Error> {
git::ext::Blob::At {
object: oid.into(),
path,
}
@ -218,7 +241,7 @@ impl ReadRepository for Repository {
) -> Result<Option<git2::Reference>, 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<Option<Oid>, 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())))
}

View File

@ -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<git::RefString, storage::Oid> = BTreeMap::new();

View File

@ -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<Verified>)>,
pub inventory: Vec<Project>,
}
impl MockStorage {
pub fn new(inventory: Vec<(ProjId, Remotes<Verified>)>) -> Self {
pub fn new(inventory: Vec<Project>) -> Self {
Self { inventory }
}
@ -38,9 +38,9 @@ impl ReadStorage for MockStorage {
}
}
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) {
return Ok(Some(refs.clone()));
fn get(&self, proj: &ProjId) -> Result<Option<Project>, 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::<Vec<_>>();
Ok(inventory)

View File

@ -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);
}