diff --git a/radicle-node/src/client/handle.rs b/radicle-node/src/client/handle.rs index 75448835..6531f6f2 100644 --- a/radicle-node/src/client/handle.rs +++ b/radicle-node/src/client/handle.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::io::{self, Write}; use std::os::unix::net::UnixStream; use std::sync::atomic::{AtomicBool, Ordering}; @@ -9,9 +10,10 @@ use thiserror::Error; use crate::crypto::Signer; use crate::identity::Id; +use crate::node::FetchLookup; use crate::profile::Home; use crate::service; -use crate::service::{CommandError, FetchLookup, QueryState}; +use crate::service::{CommandError, QueryState}; use crate::service::{NodeId, Sessions}; use crate::wire; use crate::worker::WorkerResp; @@ -62,6 +64,12 @@ pub struct Handle { shutdown: Arc, } +impl fmt::Debug for Handle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Handle").field("home", &self.home).finish() + } +} + impl Clone for Handle { fn clone(&self) -> Self { Self { @@ -98,7 +106,6 @@ impl Handle { impl radicle::node::Handle for Handle { type Sessions = Sessions; - type FetchLookup = FetchLookup; type Error = Error; fn connect(&mut self, node: NodeId, addr: radicle::node::Address) -> Result<(), Error> { @@ -107,7 +114,7 @@ impl radicle::node::Handle for Handle { Ok(()) } - fn fetch(&mut self, id: Id) -> Result { + fn fetch(&mut self, id: Id) -> Result { let (sender, receiver) = chan::bounded(1); self.command(service::Command::Fetch(id, sender))?; receiver.recv().map_err(Error::from) diff --git a/radicle-node/src/control.rs b/radicle-node/src/control.rs index 3775473f..d6c85115 100644 --- a/radicle-node/src/control.rs +++ b/radicle-node/src/control.rs @@ -12,7 +12,7 @@ use radicle::node::Handle; use crate::client; use crate::identity::Id; use crate::node; -use crate::service::FetchLookup; +use crate::node::FetchLookup; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -23,10 +23,7 @@ pub enum Error { } /// Listen for commands on the control socket, and process them. -pub fn listen< - P: AsRef, - H: Handle, ->( +pub fn listen, H: Handle>( path: P, mut handle: H, ) -> Result<(), Error> { @@ -41,6 +38,7 @@ pub fn listen< log::info!("Binding control socket {}..", path.as_ref().display()); + // TODO: Move socket binding to main thread. let listener = UnixListener::bind(path).map_err(Error::Bind)?; for incoming in listener.incoming() { match incoming { @@ -85,7 +83,7 @@ enum DrainError { Shutdown, } -fn drain>( +fn drain>( stream: &UnixStream, handle: &mut H, ) -> Result<(), DrainError> { @@ -220,7 +218,7 @@ fn drain>( Ok(()) } -fn fetch>( +fn fetch>( id: Id, mut writer: W, handle: &mut H, diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index 77699aaa..63b47c00 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -12,30 +12,29 @@ use std::collections::hash_map::Entry; use std::collections::{BTreeMap, HashMap, HashSet}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; -use std::{fmt, io, net, str}; +use std::{fmt, net, str}; use crossbeam_channel as chan; use fastrand::Rng; use localtime::{LocalDuration, LocalTime}; use log::*; use nonempty::NonEmpty; -use radicle::node::{Address, Features}; -use radicle::storage::{Namespaces, ReadStorage}; use crate::address; use crate::address::AddressBook; use crate::clock::Timestamp; use crate::crypto; use crate::crypto::{Signer, Verified}; -use crate::git; use crate::identity::{Doc, Id}; use crate::node; +use crate::node::{Address, Features, FetchLookup, FetchResult}; use crate::prelude::*; use crate::service::message::{Announcement, AnnouncementMessage, Ping}; use crate::service::message::{NodeAnnouncement, RefsAnnouncement}; use crate::service::session::Protocol; use crate::storage; use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteStorage}; +use crate::storage::{Namespaces, ReadStorage}; use crate::Link; pub use crate::node::NodeId; @@ -97,48 +96,6 @@ pub enum Error { Routing(#[from] routing::Error), } -/// Error returned by [`Command::Fetch`]. -#[derive(thiserror::Error, Debug)] -pub enum FetchError { - #[error(transparent)] - Git(#[from] git::raw::Error), - #[error(transparent)] - Storage(#[from] storage::Error), - #[error(transparent)] - Fetch(#[from] storage::FetchError), - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Project(#[from] storage::ProjectError), -} - -/// Result of looking up seeds in our routing table. -/// This object is sent back to the caller who initiated the fetch. -#[derive(Debug)] -pub enum FetchLookup { - /// Found seeds for the given project. - Found { - seeds: NonEmpty, - results: chan::Receiver, - }, - /// Can't fetch because no seeds were found for this project. - NotFound, - /// Can't fetch because the project isn't tracked. - NotTracking, - /// Error trying to find seeds. - Error(FetchError), -} - -/// Result of a fetch request from a specific seed. -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub struct FetchResult { - pub rid: Id, - pub remote: NodeId, - pub namespaces: Namespaces, - pub result: Result, FetchError>, -} - /// Function used to query internal service state. pub type QueryState = dyn Fn(&dyn ServiceState) -> Result<(), CommandError> + Send + Sync; diff --git a/radicle-node/src/test/handle.rs b/radicle-node/src/test/handle.rs index fcf9dc7b..75f8510e 100644 --- a/radicle-node/src/test/handle.rs +++ b/radicle-node/src/test/handle.rs @@ -5,8 +5,8 @@ use crossbeam_channel as chan; use crate::client::handle::Error; use crate::identity::Id; +use crate::node::FetchLookup; use crate::service; -use crate::service::FetchLookup; use crate::service::NodeId; #[derive(Default, Clone)] @@ -19,7 +19,6 @@ pub struct Handle { impl radicle::node::Handle for Handle { type Error = Error; type Sessions = service::Sessions; - type FetchLookup = FetchLookup; fn connect(&mut self, _node: NodeId, _addr: radicle::node::Address) -> Result<(), Error> { unimplemented!(); diff --git a/radicle-node/src/test/simulator.rs b/radicle-node/src/test/simulator.rs index 1faf8cf0..5e7ed415 100644 --- a/radicle-node/src/test/simulator.rs +++ b/radicle-node/src/test/simulator.rs @@ -13,10 +13,10 @@ use localtime::{LocalDuration, LocalTime}; use log::*; use crate::crypto::Signer; +use crate::node::{FetchError, FetchResult}; use crate::prelude::Address; use crate::service::reactor::Io; use crate::service::{DisconnectReason, Event, Message, NodeId}; -use crate::service::{FetchError, FetchResult}; use crate::storage::{WriteRepository, WriteStorage}; use crate::test::peer::Service; use crate::Link; diff --git a/radicle-node/src/tests/e2e.rs b/radicle-node/src/tests/e2e.rs index b0ecacbf..b7e69aac 100644 --- a/radicle-node/src/tests/e2e.rs +++ b/radicle-node/src/tests/e2e.rs @@ -10,15 +10,15 @@ use radicle::crypto::test::signer::MockSigner; use radicle::crypto::Signer; use radicle::git::refname; use radicle::identity::Id; +use radicle::node::FetchLookup; use radicle::node::Handle as _; use radicle::profile::Home; -use radicle::storage::{ReadStorage, WriteStorage}; +use radicle::storage::{ReadRepository, ReadStorage, WriteStorage}; use radicle::test::fixtures; use radicle::Storage; use radicle::{assert_matches, rad}; use crate::node::NodeId; -use crate::service::FetchLookup; use crate::storage::git::transport; use crate::test::logger; use crate::{client, client::handle::Handle, client::Runtime, service}; @@ -34,6 +34,7 @@ struct Node { struct NodeHandle { id: NodeId, storage: Storage, + signer: MockSigner, addr: net::SocketAddr, thread: ManuallyDrop>>, handle: ManuallyDrop>, @@ -117,6 +118,7 @@ impl Node { NodeHandle { id, storage: self.storage, + signer: self.signer, addr, handle, thread, @@ -355,27 +357,64 @@ fn test_replication() { log::debug!(target: "test", "Fetch complete with {}", result.remote); - // TODO: Have simpler way of listing all refs. - let inventory = alice.handle.inventory().unwrap(); - let alice_refs = alice - .storage - .repository(acme) + let alice_repo = alice.storage.repository(acme).unwrap(); + let bob_repo = bob.storage.repository(acme).unwrap(); + + let alice_refs = alice_repo + .references() .unwrap() - .remotes() + .collect::, _>>() + .unwrap(); + let bob_refs = bob_repo + .references() .unwrap() - .map(|r| r.unwrap()) - .collect::>(); - let bob_refs = bob - .storage - .repository(acme) - .unwrap() - .remotes() - .unwrap() - .map(|r| r.unwrap()) - .collect::>(); + .collect::, _>>() + .unwrap(); assert_eq!(inventory.try_iter().next(), Some(acme)); assert_eq!(alice_refs, bob_refs); assert_matches!(alice.storage.repository(acme).unwrap().verify(), Ok(())); } + +#[test] +fn test_clone() { + logger::init(log::Level::Debug); + + let tmp = tempfile::tempdir().unwrap(); + let alice = Node::new(tmp.path()); + let mut bob = Node::new(tmp.path()); + let acme = bob.project("acme"); + + let mut alice = alice.spawn(service::Config::default()); + let bob = bob.spawn(service::Config::default()); + + alice.connect(&bob); + converge([&alice, &bob]); + + transport::local::register(alice.storage.clone()); + + let repo = rad::clone( + acme, + tmp.path().join("clone"), + &alice.signer, + &alice.storage, + &mut (*alice.handle), + ) + .unwrap(); + + // Makes test finish faster. + drop(alice); + + let head = repo.head().unwrap(); + let oid = head.target().unwrap(); + + let (_, canonical) = bob + .storage + .repository(acme) + .unwrap() + .canonical_head() + .unwrap(); + + assert_eq!(oid, *canonical); +} diff --git a/radicle-node/src/worker.rs b/radicle-node/src/worker.rs index 500b420e..ec8229f7 100644 --- a/radicle-node/src/worker.rs +++ b/radicle-node/src/worker.rs @@ -14,8 +14,8 @@ use radicle::{git, Storage}; use reactor::poller::popol; use crate::client::handle::Handle; +use crate::node::{FetchError, FetchResult}; use crate::service::reactor::Fetch; -use crate::service::{FetchError, FetchResult}; use crate::wire::{WireReader, WireSession, WireWriter}; /// Worker request. @@ -70,7 +70,7 @@ impl Worker { .worker_result(WorkerResp { result, session }) .is_err() { - log::error!("Unable to report fetch result: worker channel disconnected"); + log::error!(target: "worker", "Unable to report fetch result: worker channel disconnected"); } } @@ -144,9 +144,9 @@ impl Worker { log::debug!(target: "worker", "Fetch for {} exited with status {:?}", fetch.repo, status.code()); if let Some(status) = status.code() { - log::debug!(target: "worker", "Upload pack for {} exited with status {:?}", fetch.repo, status); + log::debug!(target: "worker", "Fetch for {} exited with status {:?}", fetch.repo, status); } else { - log::debug!(target: "worker", "Upload pack for {} exited with unknown status", fetch.repo); + log::debug!(target: "worker", "Fetch for {} exited with unknown status", fetch.repo); } if !status.success() { @@ -221,9 +221,9 @@ impl Worker { let status = child.wait()?; if let Some(status) = status.code() { - log::debug!(target: "worker", "Upload pack for {} exited with status {:?}", fetch.repo, status); + log::debug!(target: "worker", "Upload-pack for {} exited with status {:?}", fetch.repo, status); } else { - log::debug!(target: "worker", "Upload pack for {} exited with unknown status", fetch.repo); + log::debug!(target: "worker", "Upload-pack for {} exited with unknown status", fetch.repo); } if !status.success() { @@ -231,7 +231,7 @@ impl Worker { stderr.read_to_end(&mut err)?; let err = String::from_utf8_lossy(&err); - log::debug!(target: "worker", "Upload pack for {}: stderr: {}", fetch.repo, err); + log::debug!(target: "worker", "Upload-pack for {}: stderr: {}", fetch.repo, err); } Ok(vec![]) diff --git a/radicle/src/git.rs b/radicle/src/git.rs index 837305e9..50d7490d 100644 --- a/radicle/src/git.rs +++ b/radicle/src/git.rs @@ -41,7 +41,7 @@ pub enum RefError { #[error("expected ref to begin with 'refs/namespaces' but found '{0}'")] MissingNamespace(format::RefString), #[error("ref name contains invalid namespace identifier '{name}'")] - Id { + InvalidNamespace { name: format::RefString, #[source] err: Box, @@ -224,7 +224,7 @@ where .namespace() .as_str() .parse() - .map_err(|err| RefError::Id { + .map_err(|err| RefError::InvalidNamespace { name: input.to_owned(), err: Box::new(err), })?; @@ -300,7 +300,7 @@ pub fn configure_remote<'r>( url: &Url, ) -> Result, git2::Error> { let fetch = format!("+refs/heads/*:refs/remotes/{name}/*"); - let remote = repo.remote_with_fetch(name, url.to_string().as_str(), &fetch)?; + let remote = repo.remote_with_fetch(name, dbg!(url.to_string().as_str()), &fetch)?; Ok(remote) } diff --git a/radicle/src/node.rs b/radicle/src/node.rs index 16c0f92a..78ff5782 100644 --- a/radicle/src/node.rs +++ b/radicle/src/node.rs @@ -2,15 +2,20 @@ mod features; use amplify::WrapperMut; use std::io::{BufRead, BufReader, Write}; +use std::ops::Deref; use std::os::unix::net::UnixStream; use std::path::Path; use std::{io, net}; +use crossbeam_channel as chan; use cyphernet::addr::{HostName, NetAddr}; +use nonempty::NonEmpty; use crate::crypto::PublicKey; +use crate::git; use crate::identity::Id; -use crossbeam_channel as chan; +use crate::storage; +use crate::storage::{Namespaces, RefUpdate}; pub use features::Features; @@ -50,6 +55,56 @@ impl From for Address { } } +/// Result of a fetch request from a specific seed. +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub struct FetchResult { + pub rid: Id, + pub remote: NodeId, + pub namespaces: Namespaces, + pub result: Result, FetchError>, +} + +impl Deref for FetchResult { + type Target = Result, FetchError>; + + fn deref(&self) -> &Self::Target { + &self.result + } +} + +/// Error returned by fetch. +#[derive(thiserror::Error, Debug)] +pub enum FetchError { + #[error(transparent)] + Git(#[from] git::raw::Error), + #[error(transparent)] + Storage(#[from] storage::Error), + #[error(transparent)] + Fetch(#[from] storage::FetchError), + #[error(transparent)] + Io(#[from] io::Error), + #[error(transparent)] + Project(#[from] storage::ProjectError), +} + +/// Result of looking up seeds in our routing table. +/// This object is sent back to the caller who initiated the fetch. +#[derive(Debug)] +pub enum FetchLookup { + /// Found seeds for the given project. + Found { + seeds: NonEmpty, + results: chan::Receiver, + }, + /// Can't fetch because no seeds were found for this project. + NotFound, + /// Can't fetch because the project isn't tracked. + NotTracking, + /// Error trying to find seeds. + Error(FetchError), +} + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("failed to connect to node: {0}")] @@ -62,8 +117,6 @@ pub enum Error { /// A handle to send commands to the node or request information. pub trait Handle { - /// The result of a fetch request. - type FetchLookup; /// The peer sessions type. type Sessions; /// The error returned by all methods. @@ -72,7 +125,7 @@ pub trait Handle { /// Connect to a peer. fn connect(&mut self, node: NodeId, addr: Address) -> Result<(), Self::Error>; /// Retrieve or update the project from network. - fn fetch(&mut self, id: Id) -> Result; + fn fetch(&mut self, id: Id) -> Result; /// Start tracking the given project. Doesn't do anything if the project is already /// tracked. fn track_repo(&mut self, id: Id) -> Result; @@ -130,19 +183,18 @@ impl Node { impl Handle for Node { type Sessions = (); - type FetchLookup = (); type Error = Error; fn connect(&mut self, _node: NodeId, _addr: Address) -> Result<(), Error> { todo!() } - fn fetch(&mut self, id: Id) -> Result<(), Error> { + fn fetch(&mut self, id: Id) -> Result { for line in self.call("fetch", &[id.urn()])? { let line = line?; log::debug!("node: {}", line); } - Ok(()) + todo!() } fn track_node(&mut self, id: NodeId, alias: Option) -> Result { diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index 18381105..e9620821 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -11,8 +11,8 @@ use crate::git; use crate::identity::doc; use crate::identity::doc::{DocError, Id}; use crate::identity::project::Project; -use crate::node; use crate::node::NodeId; +use crate::node::{self, FetchLookup}; use crate::storage::git::transport::{self, remote}; use crate::storage::git::{ProjectError, Repository, Storage}; use crate::storage::refs::SignedRefs; @@ -193,29 +193,55 @@ pub fn fork( } #[derive(Error, Debug)] -pub enum CloneError { +pub enum CloneError { #[error("node: {0}")] Node(#[from] node::Error), + #[error("fetch: {0}")] + Fetch(#[from] node::FetchError), #[error("fork: {0}")] Fork(#[from] ForkError), #[error("checkout: {0}")] Checkout(#[from] CheckoutError), #[error("identity document error: {0}")] Doc(#[from] DocError), + #[error("handle error: {0}")] + Handle(H::Error), } -pub fn clone, G: Signer, S: storage::WriteStorage, H: node::Handle>( +pub fn clone, G: Signer, H: node::Handle>( proj: Id, path: P, signer: &G, - storage: &S, + storage: &Storage, handle: &mut H, -) -> Result -where - CloneError: From, -{ - let _ = handle.track_repo(proj)?; - let _ = handle.fetch(proj)?; +) -> Result> { + let _ = handle.track_repo(proj).map_err(CloneError::Handle)?; + let lookup = handle.fetch(proj).map_err(CloneError::Handle)?; + + match lookup { + FetchLookup::Found { seeds, results } => { + // TODO: If none of them succeeds, output an error. Otherwise tell the caller + // how many succeeded. + for result in results.iter().take(seeds.len()) { + match &*result { + Ok(_updates) => {} + Err(_err) => {} + } + } + } + FetchLookup::NotFound => { + // TODO: Return error instead. + panic!("clone: Repository not found in routing table"); + } + FetchLookup::NotTracking => { + // SAFETY: Since we track it above, this shouldn't trigger unless there's a bug. + panic!("clone: Repository is not tracked"); + } + FetchLookup::Error(err) => { + return Err(err.into()); + } + } + let _ = fork(proj, signer, storage)?; let working = checkout(proj, signer.public_key(), path, storage)?; diff --git a/radicle/src/storage.rs b/radicle/src/storage.rs index 18207ea5..60c2449c 100644 --- a/radicle/src/storage.rs +++ b/radicle/src/storage.rs @@ -303,7 +303,7 @@ pub trait ReadRepository { remote: &RemoteId, reference: &Qualified, ) -> Result; - fn references(&self, remote: &RemoteId) -> Result; + fn references_of(&self, remote: &RemoteId) -> Result; fn remote(&self, remote: &RemoteId) -> Result, refs::Error>; fn remotes(&self) -> Result, refs::Error>; /// Return the project associated with this repository. diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index 56fd6f1b..52436363 100644 --- a/radicle/src/storage/git.rs +++ b/radicle/src/storage/git.rs @@ -30,6 +30,36 @@ pub static NAMESPACES_GLOB: Lazy = pub static SIGREFS_GLOB: Lazy = Lazy::new(|| refspec::pattern!("refs/namespaces/*/rad/sigrefs")); +/// A parsed Git reference. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Ref { + pub oid: git::Oid, + pub name: RefString, + pub namespace: Option, +} + +impl<'a> TryFrom> for Ref { + type Error = RefError; + + fn try_from(r: git2::Reference) -> Result { + let name = r.name().ok_or(RefError::InvalidName)?; + let (namespace, name) = match git::parse_ref_namespaced::(name) { + Ok((namespace, refname)) => (Some(namespace), refname.to_ref_string()), + Err(RefError::MissingNamespace(refname)) => (None, refname), + Err(err) => return Err(err), + }; + let Some(oid) = r.target() else { + // Ignore symbolic refs, eg. `HEAD`. + return Err(RefError::Symbolic(name)); + }; + Ok(Self { + namespace, + name, + oid: oid.into(), + }) + } +} + // TODO: Is this is the wrong place for this type? #[derive(Error, Debug)] pub enum ProjectError { @@ -261,6 +291,27 @@ impl Repository { Ok(()) } + /// Iterate over all references. + pub fn references( + &self, + ) -> Result> + '_, git2::Error> { + let refs = self + .backend + .references()? + .map(|reference| { + let r = reference?; + + match Ref::try_from(r) { + Err(RefError::Symbolic(_)) => Ok(None), + Err(err) => Err(err.into()), + Ok(r) => Ok(Some(r)), + } + }) + .filter_map(Result::transpose); + + Ok(refs) + } + pub fn identity(&self, remote: &RemoteId) -> Result, IdentityError> { Identity::load(remote, self) } @@ -448,7 +499,7 @@ impl ReadRepository for Repository { Ok(Remote::new(*remote, refs)) } - fn references(&self, remote: &RemoteId) -> Result { + fn references_of(&self, remote: &RemoteId) -> Result { // TODO: Only return known refs, eg. heads/ rad/ tags/ etc.. let entries = self .backend @@ -670,7 +721,7 @@ impl WriteRepository for Repository { fn sign_refs(&self, signer: &G) -> Result, Error> { let remote = signer.public_key(); - let refs = self.references(remote)?; + let refs = self.references_of(remote)?; let signed = refs.signed(signer)?; signed.save(remote, self)?; @@ -936,7 +987,7 @@ mod tests { let signed = project.sign_refs(&signer).unwrap(); let remote = project.remote(&alice).unwrap(); - let mut unsigned = project.references(&alice).unwrap(); + let mut unsigned = project.references_of(&alice).unwrap(); // The signed refs doesn't contain the signature ref itself. let sigref = (*SIGREFS_BRANCH).to_ref_string(); diff --git a/radicle/src/test/storage.rs b/radicle/src/test/storage.rs index 0b6e12dd..fd04c0ed 100644 --- a/radicle/src/test/storage.rs +++ b/radicle/src/test/storage.rs @@ -116,7 +116,7 @@ impl ReadRepository for MockRepository { todo!() } - fn references(&self, _remote: &RemoteId) -> Result { + fn references_of(&self, _remote: &RemoteId) -> Result { todo!() }