node: Finalize `init`
Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
parent
3e173a18ab
commit
68099688e6
|
|
@ -6,7 +6,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
pub use ed25519::Signature;
|
pub use ed25519::Signature;
|
||||||
|
|
||||||
pub trait Signer {
|
pub trait Signer: 'static {
|
||||||
/// Return this signer's public/verification key.
|
/// Return this signer's public/verification key.
|
||||||
fn public_key(&self) -> &PublicKey;
|
fn public_key(&self) -> &PublicKey;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use git_ref_format as format;
|
||||||
|
use git_url::Url;
|
||||||
|
use radicle_git_ext as git_ext;
|
||||||
|
|
||||||
use crate::collections::HashMap;
|
use crate::collections::HashMap;
|
||||||
use crate::identity::UserId;
|
use crate::identity::UserId;
|
||||||
use crate::storage::{Remote, Remotes, Unverified};
|
use crate::storage::{Remote, Remotes, Unverified};
|
||||||
use git_ref_format as format;
|
|
||||||
use git_url::Url;
|
|
||||||
|
|
||||||
/// Default port of the `git` transport protocol.
|
/// Default port of the `git` transport protocol.
|
||||||
pub const PROTOCOL_PORT: u16 = 9418;
|
pub const PROTOCOL_PORT: u16 = 9418;
|
||||||
|
|
@ -91,3 +93,43 @@ pub fn commit<'a>(
|
||||||
|
|
||||||
Ok(commit)
|
Ok(commit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the upstream of the given branch to the given remote.
|
||||||
|
///
|
||||||
|
/// This writes to the `config` directly. The entry will look like the
|
||||||
|
/// following:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// [branch "main"]
|
||||||
|
/// remote = rad
|
||||||
|
/// merge = refs/heads/main
|
||||||
|
/// ```
|
||||||
|
pub fn set_upstream(
|
||||||
|
repo: &git2::Repository,
|
||||||
|
remote: &str,
|
||||||
|
branch: &str,
|
||||||
|
merge: &str,
|
||||||
|
) -> Result<(), git2::Error> {
|
||||||
|
let mut config = repo.config()?;
|
||||||
|
let branch_remote = format!("branch.{}.remote", branch);
|
||||||
|
let branch_merge = format!("branch.{}.merge", branch);
|
||||||
|
|
||||||
|
config.remove_multivar(&branch_remote, ".*").or_else(|e| {
|
||||||
|
if git_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) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
config.set_multivar(&branch_remote, ".*", remote)?;
|
||||||
|
config.set_multivar(&branch_merge, ".*", merge)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
use std::{fs, io};
|
use std::{fs, io};
|
||||||
|
|
||||||
|
use git_url::Url;
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::git;
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::{ProjId, UserId};
|
||||||
|
use crate::storage::git::RADICLE_ID_REF;
|
||||||
|
use crate::storage::ReadRepository as _;
|
||||||
use crate::{identity, storage};
|
use crate::{identity, storage};
|
||||||
|
|
||||||
|
pub const REMOTE_NAME: &str = "rad";
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[error("doc: {0}")]
|
#[error("doc: {0}")]
|
||||||
|
|
@ -14,6 +20,8 @@ pub enum InitError {
|
||||||
Git(#[from] git2::Error),
|
Git(#[from] git2::Error),
|
||||||
#[error("i/o: {0}")]
|
#[error("i/o: {0}")]
|
||||||
Io(#[from] io::Error),
|
Io(#[from] io::Error),
|
||||||
|
#[error("storage: {0}")]
|
||||||
|
Storage(#[from] storage::Error),
|
||||||
#[error("cannot initialize project inside a bare repository")]
|
#[error("cannot initialize project inside a bare repository")]
|
||||||
BareRepo,
|
BareRepo,
|
||||||
#[error("cannot initialize project from detached head state")]
|
#[error("cannot initialize project from detached head state")]
|
||||||
|
|
@ -23,11 +31,12 @@ pub enum InitError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a new radicle project from a git repository.
|
/// Initialize a new radicle project from a git repository.
|
||||||
pub fn init(
|
pub fn init<S: storage::WriteStorage>(
|
||||||
repo: &git2::Repository,
|
repo: &git2::Repository,
|
||||||
name: &str,
|
name: &str,
|
||||||
description: &str,
|
description: &str,
|
||||||
delegate: UserId,
|
delegate: UserId,
|
||||||
|
storage: S,
|
||||||
) -> Result<ProjId, InitError> {
|
) -> Result<ProjId, InitError> {
|
||||||
let delegate = identity::Delegate {
|
let delegate = identity::Delegate {
|
||||||
// TODO: Use actual user name.
|
// TODO: Use actual user name.
|
||||||
|
|
@ -65,10 +74,11 @@ pub fn init(
|
||||||
let mut index = repo.index()?;
|
let mut index = repo.index()?;
|
||||||
index.add_path(filename)?;
|
index.add_path(filename)?;
|
||||||
|
|
||||||
|
let rad_id_ref = RADICLE_ID_REF.as_str();
|
||||||
let tree_id = index.write_tree()?;
|
let tree_id = index.write_tree()?;
|
||||||
let tree = repo.find_tree(tree_id)?;
|
let tree = repo.find_tree(tree_id)?;
|
||||||
let _oid = repo.commit(
|
let _oid = repo.commit(
|
||||||
Some(storage::git::RAD_ID_REF.as_str()),
|
Some(rad_id_ref),
|
||||||
&sig,
|
&sig,
|
||||||
&sig,
|
&sig,
|
||||||
"Initialize Radicle",
|
"Initialize Radicle",
|
||||||
|
|
@ -81,6 +91,38 @@ pub fn init(
|
||||||
// called the same name. Ideally we are able to create the file in the id branch.
|
// called the same name. Ideally we are able to create the file in the id branch.
|
||||||
fs::remove_file(path)?;
|
fs::remove_file(path)?;
|
||||||
|
|
||||||
|
let project = storage.repository(&id)?;
|
||||||
|
let url = Url {
|
||||||
|
scheme: git_url::Scheme::File,
|
||||||
|
path: project.path().to_string_lossy().to_string().into(),
|
||||||
|
|
||||||
|
..Url::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = storage.user_id();
|
||||||
|
let fetch = format!("+refs/remotes/{user_id}/heads/*:refs/remotes/rad/*");
|
||||||
|
let push = format!("refs/heads/*:refs/remotes/{user_id}/heads/*");
|
||||||
|
let mut remote = repo.remote_with_fetch(REMOTE_NAME, url.to_string().as_str(), &fetch)?;
|
||||||
|
repo.remote_add_push(REMOTE_NAME, &push)?;
|
||||||
|
|
||||||
|
git::set_upstream(
|
||||||
|
repo,
|
||||||
|
REMOTE_NAME,
|
||||||
|
"master",
|
||||||
|
&format!("refs/remotes/{user_id}/heads/master"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// TODO: Note that you'll likely want to use `RemoteCallbacks` and set
|
||||||
|
// `push_update_reference` to test whether all the references were pushed
|
||||||
|
// successfully.
|
||||||
|
remote.push::<&str>(
|
||||||
|
&[
|
||||||
|
&format!("refs/heads/master:refs/remotes/{user_id}/heads/master"),
|
||||||
|
&format!("{rad_id_ref}:refs/remotes/{user_id}/heads/rad/id"),
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,12 +131,15 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::Signer;
|
use crate::crypto::Signer;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
|
use crate::storage::git::Storage;
|
||||||
use crate::test::crypto;
|
use crate::test::crypto;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_init() {
|
fn test_init() {
|
||||||
let tempdir = tempfile::tempdir().unwrap();
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
let repo = git2::Repository::init(tempdir.path()).unwrap();
|
let signer = crypto::MockSigner::default();
|
||||||
|
let mut storage = Storage::open(tempdir.path().join("storage"), signer).unwrap();
|
||||||
|
let repo = git2::Repository::init(tempdir.path().join("working")).unwrap();
|
||||||
let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap();
|
let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap();
|
||||||
let head = git::initial_commit(&repo, &sig).unwrap();
|
let head = git::initial_commit(&repo, &sig).unwrap();
|
||||||
let head = git::commit(&repo, &head, "Second commit", "anonymous").unwrap();
|
let head = git::commit(&repo, &head, "Second commit", "anonymous").unwrap();
|
||||||
|
|
@ -104,6 +149,6 @@ mod tests {
|
||||||
let signer = crypto::MockSigner::new(&mut fastrand::Rng::new());
|
let signer = crypto::MockSigner::new(&mut fastrand::Rng::new());
|
||||||
let delegate = *signer.public_key();
|
let delegate = *signer.public_key();
|
||||||
|
|
||||||
init(&repo, "acme", "Acme's repo", delegate).unwrap();
|
init(&repo, "acme", "Acme's repo", delegate, &mut storage).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,22 +116,24 @@ impl Remote<Unverified> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ReadStorage {
|
pub trait ReadStorage {
|
||||||
|
fn user_id(&self) -> &UserId;
|
||||||
fn url(&self) -> Url;
|
fn url(&self) -> Url;
|
||||||
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error>;
|
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error>;
|
||||||
fn inventory(&self) -> Result<Inventory, Error>;
|
fn inventory(&self) -> Result<Inventory, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WriteStorage {
|
pub trait WriteStorage: ReadStorage {
|
||||||
type Repository: WriteRepository;
|
type Repository: WriteRepository;
|
||||||
|
|
||||||
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error>;
|
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ReadRepository {
|
pub trait ReadRepository {
|
||||||
|
fn path(&self) -> &Path;
|
||||||
fn remotes(&self) -> Result<Remotes<Unverified>, Error>;
|
fn remotes(&self) -> Result<Remotes<Unverified>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WriteRepository {
|
pub trait WriteRepository: ReadRepository {
|
||||||
fn fetch(&mut self, url: &Url) -> Result<(), git2::Error>;
|
fn fetch(&mut self, url: &Url) -> Result<(), git2::Error>;
|
||||||
fn namespace(&mut self, user: &UserId) -> Result<&mut git2::Repository, git2::Error>;
|
fn namespace(&mut self, user: &UserId) -> Result<&mut git2::Repository, git2::Error>;
|
||||||
}
|
}
|
||||||
|
|
@ -139,8 +141,12 @@ pub trait WriteRepository {
|
||||||
impl<T, S> ReadStorage for T
|
impl<T, S> ReadStorage for T
|
||||||
where
|
where
|
||||||
T: Deref<Target = S>,
|
T: Deref<Target = S>,
|
||||||
S: ReadStorage,
|
S: ReadStorage + 'static,
|
||||||
{
|
{
|
||||||
|
fn user_id(&self) -> &UserId {
|
||||||
|
self.deref().user_id()
|
||||||
|
}
|
||||||
|
|
||||||
fn url(&self) -> Url {
|
fn url(&self) -> Url {
|
||||||
self.deref().url()
|
self.deref().url()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use radicle_git_ext as git_ext;
|
||||||
pub use radicle_git_ext::Oid;
|
pub use radicle_git_ext::Oid;
|
||||||
|
|
||||||
use crate::collections::HashMap;
|
use crate::collections::HashMap;
|
||||||
|
use crate::crypto::Signer;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::{ProjId, UserId};
|
||||||
|
|
||||||
|
|
@ -17,19 +18,14 @@ use super::{
|
||||||
WriteStorage,
|
WriteStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub static RAD_ID_REF: Lazy<refspec::PatternString> =
|
pub static RADICLE_ID_REF: Lazy<refspec::PatternString> =
|
||||||
Lazy::new(|| refspec::pattern!("refs/heads/rad/id"));
|
Lazy::new(|| refspec::pattern!("refs/heads/radicle/id"));
|
||||||
pub static NAMESPACES_GLOB: Lazy<refspec::PatternString> =
|
pub static NAMESPACES_GLOB: Lazy<refspec::PatternString> =
|
||||||
Lazy::new(|| refspec::pattern!("refs/namespaces/*"));
|
Lazy::new(|| refspec::pattern!("refs/namespaces/*"));
|
||||||
|
|
||||||
pub struct Storage {
|
pub struct Storage {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
}
|
signer: Box<dyn Signer>,
|
||||||
|
|
||||||
impl From<PathBuf> for Storage {
|
|
||||||
fn from(path: PathBuf) -> Self {
|
|
||||||
Self { path }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Storage {
|
impl fmt::Debug for Storage {
|
||||||
|
|
@ -39,6 +35,10 @@ impl fmt::Debug for Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadStorage for Storage {
|
impl ReadStorage for Storage {
|
||||||
|
fn user_id(&self) -> &UserId {
|
||||||
|
self.signer.public_key()
|
||||||
|
}
|
||||||
|
|
||||||
fn url(&self) -> Url {
|
fn url(&self) -> Url {
|
||||||
Url {
|
Url {
|
||||||
scheme: git_url::Scheme::File,
|
scheme: git_url::Scheme::File,
|
||||||
|
|
@ -65,7 +65,7 @@ impl WriteStorage for Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Storage {
|
impl Storage {
|
||||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
|
pub fn open<P: AsRef<Path>>(path: P, signer: impl Signer) -> Result<Self, io::Error> {
|
||||||
let path = path.as_ref().to_path_buf();
|
let path = path.as_ref().to_path_buf();
|
||||||
|
|
||||||
match fs::create_dir_all(&path) {
|
match fs::create_dir_all(&path) {
|
||||||
|
|
@ -74,7 +74,10 @@ impl Storage {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { path })
|
Ok(Self {
|
||||||
|
path,
|
||||||
|
signer: Box::new(signer),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &Path {
|
pub fn path(&self) -> &Path {
|
||||||
|
|
@ -132,6 +135,10 @@ impl Repository {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadRepository for Repository {
|
impl ReadRepository for Repository {
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
self.backend.path()
|
||||||
|
}
|
||||||
|
|
||||||
fn remotes(&self) -> Result<Remotes<Unverified>, Error> {
|
fn remotes(&self) -> Result<Remotes<Unverified>, Error> {
|
||||||
let refs = self.backend.references_glob(NAMESPACES_GLOB.as_str())?;
|
let refs = self.backend.references_glob(NAMESPACES_GLOB.as_str())?;
|
||||||
let mut remotes = HashMap::default();
|
let mut remotes = HashMap::default();
|
||||||
|
|
@ -196,6 +203,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::storage::{ReadStorage, WriteRepository};
|
use crate::storage::{ReadStorage, WriteRepository};
|
||||||
|
use crate::test::crypto::MockSigner;
|
||||||
use crate::test::fixtures;
|
use crate::test::fixtures;
|
||||||
use git_url::Url;
|
use git_url::Url;
|
||||||
|
|
||||||
|
|
@ -222,7 +230,7 @@ mod tests {
|
||||||
fn test_fetch() {
|
fn test_fetch() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let alice = fixtures::storage(tmp.path().join("alice"));
|
let alice = fixtures::storage(tmp.path().join("alice"));
|
||||||
let bob = Storage::open(tmp.path().join("bob")).unwrap();
|
let bob = Storage::open(tmp.path().join("bob"), MockSigner::default()).unwrap();
|
||||||
let inventory = alice.inventory().unwrap();
|
let inventory = alice.inventory().unwrap();
|
||||||
let proj = inventory.first().unwrap();
|
let proj = inventory.first().unwrap();
|
||||||
let remotes = alice.repository(proj).unwrap().remotes().unwrap();
|
let remotes = alice.repository(proj).unwrap().remotes().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ use crate::identity::{ProjId, UserId};
|
||||||
use crate::storage::git::Storage;
|
use crate::storage::git::Storage;
|
||||||
use crate::storage::{WriteRepository, WriteStorage};
|
use crate::storage::{WriteRepository, WriteStorage};
|
||||||
use crate::test::arbitrary;
|
use crate::test::arbitrary;
|
||||||
|
use crate::test::crypto::MockSigner;
|
||||||
|
|
||||||
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
|
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let storage = Storage::open(path).unwrap();
|
let storage = Storage::open(path, MockSigner::default()).unwrap();
|
||||||
let proj_ids = arbitrary::set::<ProjId>(3..5);
|
let proj_ids = arbitrary::set::<ProjId>(3..5);
|
||||||
let user_ids = arbitrary::set::<UserId>(1..3);
|
let user_ids = arbitrary::set::<UserId>(1..3);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use git_url::Url;
|
use git_url::Url;
|
||||||
|
|
||||||
use crate::identity::ProjId;
|
use crate::identity::{ProjId, UserId};
|
||||||
use crate::storage::{
|
use crate::storage::{
|
||||||
Error, Inventory, ReadStorage, Remotes, Unverified, WriteRepository, WriteStorage,
|
Error, Inventory, ReadRepository, ReadStorage, Remotes, Unverified, WriteRepository,
|
||||||
|
WriteStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -23,6 +24,10 @@ impl MockStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadStorage for MockStorage {
|
impl ReadStorage for MockStorage {
|
||||||
|
fn user_id(&self) -> &UserId {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
fn url(&self) -> Url {
|
fn url(&self) -> Url {
|
||||||
Url {
|
Url {
|
||||||
scheme: git_url::Scheme::Radicle,
|
scheme: git_url::Scheme::Radicle,
|
||||||
|
|
@ -59,6 +64,16 @@ impl WriteStorage for MockStorage {
|
||||||
|
|
||||||
pub struct MockRepository {}
|
pub struct MockRepository {}
|
||||||
|
|
||||||
|
impl ReadRepository for MockRepository {
|
||||||
|
fn path(&self) -> &std::path::Path {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remotes(&self) -> Result<Remotes<Unverified>, Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WriteRepository for MockRepository {
|
impl WriteRepository for MockRepository {
|
||||||
fn fetch(&mut self, _url: &Url) -> Result<(), git2::Error> {
|
fn fetch(&mut self, _url: &Url) -> Result<(), git2::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use crate::protocol::peer::*;
|
||||||
use crate::protocol::*;
|
use crate::protocol::*;
|
||||||
use crate::storage::git::Storage;
|
use crate::storage::git::Storage;
|
||||||
use crate::storage::ReadStorage;
|
use crate::storage::ReadStorage;
|
||||||
|
use crate::test::crypto::MockSigner;
|
||||||
use crate::test::fixtures;
|
use crate::test::fixtures;
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use crate::test::logger;
|
use crate::test::logger;
|
||||||
|
|
@ -133,7 +134,7 @@ fn test_inventory_sync() {
|
||||||
let mut alice = Peer::new(
|
let mut alice = Peer::new(
|
||||||
"alice",
|
"alice",
|
||||||
[7, 7, 7, 7],
|
[7, 7, 7, 7],
|
||||||
Storage::open(tmp.path().join("alice")).unwrap(),
|
Storage::open(tmp.path().join("alice"), MockSigner::default()).unwrap(),
|
||||||
);
|
);
|
||||||
let bob_storage = fixtures::storage(tmp.path().join("bob"));
|
let bob_storage = fixtures::storage(tmp.path().join("bob"));
|
||||||
let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage);
|
let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue