diff --git a/radicle-cli/examples/rad-init-private.md b/radicle-cli/examples/rad-init-private.md new file mode 100644 index 00000000..b98593c8 --- /dev/null +++ b/radicle-cli/examples/rad-init-private.md @@ -0,0 +1,49 @@ +Alice initializes a *private* repo. + +``` ~alice +$ rad init --name heartwood --description "radicle heartwood protocol & stack" --no-confirm --announce --private + +Initializing private radicle 👾 project in . + +✓ Project heartwood created +✓ Syncing inventory.. +✓ Announcing inventory.. + +Your project's Repository ID (RID) is rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu. +You can show it any time by running `rad .` +``` + +Bob tries to clone it, and even though he's connected to Alice, it fails. + +``` ~bob +$ rad track rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu +✓ Tracking policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'trusted' +``` +``` ~bob (fail) +$ rad sync rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --fetch --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 3 +✗ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi.. +✗ Sync failed: failed to call node: i/o: timed out reading from control socket +``` + +She allows Bob to view the repository. And when she syncs, one node (Bob) gets +the refs. + +``` ~alice +$ rad id edit --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q +e98cd6a0a3e94837b382e59e02b3ea83991a8244 +$ rad id accept e98cd6a0a3e94837b382e59e02b3ea83991a8244 -q +$ rad id commit e98cd6a0a3e94837b382e59e02b3ea83991a8244 -q +c568f8aac97db40a5e63e1261872bfbd9a3a61e4 +$ rad sync --announce --timeout 1 +✓ Synced with 1 node(s) +``` + +Bob can now fetch the private repo: + +``` ~bob +$ rad sync rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --fetch +✓ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi.. +✓ Fetched repository from 1 seed(s) +$ rad ls --private +heartwood rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu f2de534 radicle heartwood protocol & stack +``` diff --git a/radicle-cli/src/commands/sync.rs b/radicle-cli/src/commands/sync.rs index 01e1856c..f11c81a4 100644 --- a/radicle-cli/src/commands/sync.rs +++ b/radicle-cli/src/commands/sync.rs @@ -6,7 +6,8 @@ use anyhow::{anyhow, Context as _}; use radicle::node; use radicle::node::{FetchResult, FetchResults, Handle as _, Node}; -use radicle::prelude::{Id, NodeId}; +use radicle::prelude::{Id, NodeId, Profile}; +use radicle::storage::{ReadRepository, ReadStorage}; use crate::terminal as term; use crate::terminal::args::{Args, Error, Help}; @@ -192,13 +193,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let failed = results.failed().count(); if success == 0 { - term::error(format!("Failed to fetch repository from {failed} seed(s)")); + anyhow::bail!("repository fetch from {failed} seed(s) failed"); } else { term::success!("Fetched repository from {success} seed(s)"); } } if [SyncDirection::Announce, SyncDirection::Both].contains(&options.sync.direction) { - announce(rid, mode, options.timeout, node)?; + announce(rid, mode, options.timeout, node, &profile)?; } Ok(()) } @@ -208,9 +209,21 @@ fn announce( _mode: SyncMode, timeout: time::Duration, mut node: Node, + profile: &Profile, ) -> anyhow::Result<()> { - let seeds = node.seeds(rid)?; - let connected = seeds.connected().map(|s| s.nid).collect::>(); + let repo = profile.storage.repository(rid)?; + let (_, doc) = repo.identity_doc()?; + let connected: Vec<_> = if doc.visibility.is_public() { + let seeds = node.seeds(rid)?; + seeds.connected().map(|s| s.nid).collect() + } else { + node.sessions()? + .into_iter() + .filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid)) + .map(|s| s.nid) + .collect() + }; + if connected.is_empty() { term::info!("Not connected to any seeds."); return Ok(()); diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index deb71a36..a5f5b14b 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -1159,6 +1159,38 @@ fn rad_patch_pull_update() { .unwrap(); } +#[test] +fn rad_init_private() { + logger::init(log::Level::Debug); + + let mut environment = Environment::new(); + let alice = environment.node(Config::test(Alias::new("alice"))); + let bob = environment.node(Config::test(Alias::new("bob"))); + let working = environment.tmp().join("working"); + + fixtures::repository(working.join("alice")); + + let alice = alice.spawn(); + let mut bob = bob.spawn(); + + bob.connect(&alice).converge([&alice]); + + formula(&environment.tmp(), "examples/rad-init-private.md") + .unwrap() + .home( + "alice", + working.join("alice"), + [("RAD_HOME", alice.home.path().display())], + ) + .home( + "bob", + bob.home.path(), + [("RAD_HOME", bob.home.path().display())], + ) + .run() + .unwrap(); +} + #[test] fn framework_home() { logger::init(log::Level::Debug); diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index 68ff2ee3..412c2339 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -722,21 +722,6 @@ where } } - /// Called when a remote requests a repository be uploaded to it. - /// The upload is authorized if this function returns `true`. - pub fn upload(&mut self, rid: Id, remote: &NodeId) -> bool { - let Ok(repo) = self.storage.repository(rid) else { - return false; - }; - let Ok((_, doc)) = repo.identity_doc() else { - return false; - }; - if !doc.is_visible_to(remote) { - return false; - } - true - } - /// Inbound connection attempt. pub fn accepted(&mut self, addr: Address) -> bool { // Always accept trusted connections.