cli: Show URL on `rad init`
This commit is contained in:
parent
6eb2772235
commit
13db55014b
|
|
@ -0,0 +1,19 @@
|
|||
When initializing a project without any peer connections, we get this output:
|
||||
|
||||
```
|
||||
$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm --public --scope followed
|
||||
|
||||
Initializing public radicle 👾 project in .
|
||||
|
||||
✓ Project heartwood created.
|
||||
|
||||
Your project's Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji.
|
||||
You can show it any time by running `rad .` from this directory.
|
||||
|
||||
✗ Announcing.. <canceled>
|
||||
|
||||
You are not connected to any peers. Your project will be announced as soon as your node establishes a connection with the network.
|
||||
Check for peer connections with `rad node status`.
|
||||
|
||||
To push changes, run `git push`.
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
Let's try initializing a new project with a preferred seed configured.
|
||||
|
||||
```
|
||||
$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm --public --scope followed
|
||||
|
||||
Initializing public radicle 👾 project in .
|
||||
|
||||
✓ Project heartwood created.
|
||||
|
||||
Your project's Repository ID (RID) is rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW.
|
||||
You can show it any time by running `rad .` from this directory.
|
||||
|
||||
✓ Project successfully synced to 1 node(s).
|
||||
|
||||
Your project has been synced to the network and is now discoverable by peers.
|
||||
View it in your browser at:
|
||||
|
||||
https://app.radicle.xyz/nodes/[...]/rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW
|
||||
|
||||
To push changes, run `git push`.
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
Sometimes, `init` will fail to sync with the network. This is not a big deal,
|
||||
as the node will keep attempting to sync in the background.
|
||||
|
||||
```
|
||||
$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm --public --scope followed
|
||||
|
||||
Initializing public radicle 👾 project in .
|
||||
|
||||
✓ Project heartwood created.
|
||||
|
||||
Your project's Repository ID (RID) is rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW.
|
||||
You can show it any time by running `rad .` from this directory.
|
||||
|
||||
✓ Project successfully announced to the network.
|
||||
|
||||
Your project has been announced to the network and is now discoverable by peers.
|
||||
You can check for any nodes that have replicated your project by running `rad sync status`.
|
||||
|
||||
To push changes, run `git push`.
|
||||
```
|
||||
|
|
@ -12,8 +12,10 @@ Initializing public radicle 👾 project in .
|
|||
Your project's Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji.
|
||||
You can show it any time by running `rad .` from this directory.
|
||||
|
||||
✓ Project successfully announced.
|
||||
✓ Project successfully announced to the network.
|
||||
|
||||
Your project has been announced to the network and is now discoverable by peers.
|
||||
You can check for any nodes that have replicated your project by running `rad sync status`.
|
||||
|
||||
To push changes, run `git push`.
|
||||
```
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ Initializing public radicle 👾 project in .
|
|||
Your project's Repository ID (RID) is rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK.
|
||||
You can show it any time by running `rad .` from this directory.
|
||||
|
||||
✓ Project successfully announced.
|
||||
✓ Project successfully announced to the network.
|
||||
|
||||
Your project has been announced to the network and is now discoverable by peers.
|
||||
You can check for any nodes that have replicated your project by running `rad sync status`.
|
||||
|
||||
To push changes, run `git push`.
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
#![allow(clippy::or_fun_call)]
|
||||
#![allow(clippy::collapsible_else_if)]
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::{env, io, time};
|
||||
|
||||
use anyhow::{anyhow, bail, Context as _};
|
||||
use serde_json as json;
|
||||
|
||||
use radicle::crypto::{ssh, Verified};
|
||||
use radicle::git::RefString;
|
||||
use radicle::identity::Visibility;
|
||||
use radicle::identity::{Id, Visibility};
|
||||
use radicle::node::policy::Scope;
|
||||
use radicle::node::{Handle, NodeId};
|
||||
use radicle::node::{Event, Handle, NodeId};
|
||||
use radicle::prelude::Doc;
|
||||
use radicle::{profile, Node};
|
||||
|
||||
use crate as cli;
|
||||
use crate::commands;
|
||||
use crate::git;
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
|
|
@ -274,7 +276,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
|||
&signer,
|
||||
&profile.storage,
|
||||
) {
|
||||
Ok((id, doc, _)) => {
|
||||
Ok((rid, doc, _)) => {
|
||||
let proj = doc.project()?;
|
||||
|
||||
spinner.message(format!(
|
||||
|
|
@ -290,7 +292,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
|||
// It's important to seed our own repositories to make sure that our node signals
|
||||
// interest for them. This ensures that messages relating to them are relayed to us.
|
||||
if options.seed {
|
||||
cli::project::seed(id, options.scope, &mut node, profile)?;
|
||||
cli::project::seed(rid, options.scope, &mut node, profile)?;
|
||||
}
|
||||
|
||||
if options.set_upstream || git::branch_remote(&repo, proj.default_branch()).is_err() {
|
||||
|
|
@ -314,7 +316,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
|||
term::info!(
|
||||
"Your project's Repository ID {} is {}.",
|
||||
term::format::dim("(RID)"),
|
||||
term::format::highlight(id.urn())
|
||||
term::format::highlight(rid.urn())
|
||||
);
|
||||
term::info!(
|
||||
"You can show it any time by running {} from this directory.",
|
||||
|
|
@ -323,7 +325,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
|||
term::blank();
|
||||
|
||||
// Announce inventory to network.
|
||||
if let Err(e) = announce(doc, &mut node) {
|
||||
if let Err(e) = announce(rid, doc, &mut node, &profile.config) {
|
||||
term::blank();
|
||||
term::warning(format!(
|
||||
"There was an error announcing your project to the network: {e}"
|
||||
|
|
@ -342,28 +344,168 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn announce(doc: Doc<Verified>, node: &mut Node) -> anyhow::Result<()> {
|
||||
#[derive(Debug)]
|
||||
enum SyncResult<T> {
|
||||
NodeStopped,
|
||||
NoPeersConnected,
|
||||
NotSynced,
|
||||
Synced { result: T },
|
||||
}
|
||||
|
||||
fn sync(
|
||||
rid: Id,
|
||||
node: &mut Node,
|
||||
config: &profile::Config,
|
||||
) -> Result<SyncResult<Option<String>>, radicle::node::Error> {
|
||||
if !node.is_running() {
|
||||
return Ok(SyncResult::NodeStopped);
|
||||
}
|
||||
let mut spinner = term::spinner("Updating inventory..");
|
||||
let events = node.subscribe(time::Duration::from_secs(3))?;
|
||||
let sessions = node.sessions()?;
|
||||
|
||||
node.sync_inventory()?;
|
||||
spinner.message("Announcing..");
|
||||
|
||||
if !sessions.iter().any(|s| s.is_connected()) {
|
||||
return Ok(SyncResult::NoPeersConnected);
|
||||
}
|
||||
|
||||
// Connect to preferred seeds in case we aren't connected.
|
||||
for seed in &config.preferred_seeds {
|
||||
if !sessions.iter().any(|s| s.nid == seed.id) {
|
||||
commands::rad_node::control::connect(
|
||||
node,
|
||||
seed.id,
|
||||
seed.addr.clone(),
|
||||
radicle::node::DEFAULT_TIMEOUT,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
// Announce our new inventory to connected nodes.
|
||||
node.announce_inventory()?;
|
||||
|
||||
spinner.message("Syncing..");
|
||||
|
||||
let mut replicas = HashSet::new();
|
||||
for e in events {
|
||||
match e {
|
||||
Ok(Event::RefsSynced {
|
||||
remote, rid: rid_, ..
|
||||
}) if rid == rid_ => {
|
||||
replicas.insert(remote);
|
||||
// If we manage to replicate to one of our preferred seeds, we can stop waiting.
|
||||
if config.preferred_seeds.iter().any(|s| s.id == remote) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
// Some other irrelevant event received.
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::TimedOut => {
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
spinner.error(&e);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !replicas.is_empty() {
|
||||
spinner.message(format!(
|
||||
"Project successfully synced to {} node(s).",
|
||||
replicas.len()
|
||||
));
|
||||
spinner.finish();
|
||||
|
||||
for seed in &config.preferred_seeds {
|
||||
if replicas.contains(&seed.id) {
|
||||
return Ok(SyncResult::Synced {
|
||||
result: Some(
|
||||
config
|
||||
.public_explorer
|
||||
.url(seed.addr.host.to_string().as_str(), &rid),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(SyncResult::Synced { result: None })
|
||||
} else {
|
||||
spinner.message("Project successfully announced to the network.");
|
||||
spinner.finish();
|
||||
|
||||
Ok(SyncResult::NotSynced)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn announce(
|
||||
rid: Id,
|
||||
doc: Doc<Verified>,
|
||||
node: &mut Node,
|
||||
config: &profile::Config,
|
||||
) -> anyhow::Result<()> {
|
||||
if doc.visibility.is_public() {
|
||||
if node.is_running() {
|
||||
let mut spinner = term::spinner("Updating inventory..");
|
||||
|
||||
node.sync_inventory()?;
|
||||
spinner.message("Announcing..");
|
||||
node.announce_inventory()?;
|
||||
spinner.message("Project successfully announced.");
|
||||
spinner.finish();
|
||||
|
||||
term::blank();
|
||||
term::info!(
|
||||
"Your project has been announced to the network and is \
|
||||
now discoverable by peers.",
|
||||
);
|
||||
} else {
|
||||
term::info!("Your project will be announced to the network when you start your node.");
|
||||
term::info!(
|
||||
"You can start your node with {}.",
|
||||
term::format::command("rad node start")
|
||||
);
|
||||
match sync(rid, node, config) {
|
||||
Ok(SyncResult::Synced {
|
||||
result: Some(url), ..
|
||||
}) => {
|
||||
term::blank();
|
||||
term::info!(
|
||||
"Your project has been synced to the network and is \
|
||||
now discoverable by peers.",
|
||||
);
|
||||
term::info!("View it in your browser at:");
|
||||
term::blank();
|
||||
term::indented(term::format::tertiary(url));
|
||||
term::blank();
|
||||
}
|
||||
Ok(SyncResult::Synced { result: None, .. }) => {
|
||||
term::blank();
|
||||
term::info!(
|
||||
"Your project has been synced to the network and is \
|
||||
now discoverable by peers.",
|
||||
);
|
||||
if !config.preferred_seeds.is_empty() {
|
||||
term::info!(
|
||||
"Unfortunately, you were unable to replicate your project to \
|
||||
your preferred seeds."
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(SyncResult::NotSynced) => {
|
||||
term::blank();
|
||||
term::info!(
|
||||
"Your project has been announced to the network and is \
|
||||
now discoverable by peers.",
|
||||
);
|
||||
term::info!(
|
||||
"You can check for any nodes that have replicated your project by running \
|
||||
`rad sync status`."
|
||||
);
|
||||
term::blank();
|
||||
}
|
||||
Ok(SyncResult::NoPeersConnected) => {
|
||||
term::blank();
|
||||
term::info!(
|
||||
"You are not connected to any peers. Your project will be announced as soon as \
|
||||
your node establishes a connection with the network.");
|
||||
term::info!("Check for peer connections with `rad node status`.");
|
||||
term::blank();
|
||||
}
|
||||
Ok(SyncResult::NodeStopped) => {
|
||||
term::info!(
|
||||
"Your project will be announced to the network when you start your node."
|
||||
);
|
||||
term::info!(
|
||||
"You can start your node with {}.",
|
||||
term::format::command("rad node start")
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
term::info!(
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ use crate::terminal::args::{Args, Error, Help};
|
|||
use crate::terminal::Element as _;
|
||||
|
||||
#[path = "node/control.rs"]
|
||||
mod control;
|
||||
pub mod control;
|
||||
#[path = "node/events.rs"]
|
||||
mod events;
|
||||
pub mod events;
|
||||
#[path = "node/policies.rs"]
|
||||
mod policies;
|
||||
pub mod policies;
|
||||
#[path = "node/routing.rs"]
|
||||
mod routing;
|
||||
pub mod routing;
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "node",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use radicle::node::routing::Store as _;
|
|||
use radicle::node::Handle as _;
|
||||
use radicle::node::{Alias, DEFAULT_TIMEOUT};
|
||||
use radicle::prelude::Id;
|
||||
use radicle::profile;
|
||||
use radicle::profile::Home;
|
||||
use radicle::storage::{ReadStorage, RemoteRepository};
|
||||
use radicle::test::fixtures;
|
||||
|
|
@ -16,7 +17,7 @@ use radicle::test::fixtures;
|
|||
use radicle_cli_test::TestFormula;
|
||||
use radicle_node::service::policy::{Policy, Scope};
|
||||
use radicle_node::service::Event;
|
||||
use radicle_node::test::environment::{Config, Environment};
|
||||
use radicle_node::test::environment::{Config, Environment, Node};
|
||||
#[allow(unused_imports)]
|
||||
use radicle_node::test::logger;
|
||||
|
||||
|
|
@ -432,7 +433,7 @@ fn rad_node() {
|
|||
fixtures::repository(working.path().join("alice"));
|
||||
|
||||
test(
|
||||
"examples/rad-init-sync.md",
|
||||
"examples/rad-init-sync-not-connected.md",
|
||||
&working.path().join("alice"),
|
||||
Some(&alice.home),
|
||||
[],
|
||||
|
|
@ -963,6 +964,93 @@ fn rad_clone_unknown() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_init_sync_not_connected() {
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
let alice = alice.spawn();
|
||||
|
||||
fixtures::repository(working.path().join("alice"));
|
||||
|
||||
test(
|
||||
"examples/rad-init-sync-not-connected.md",
|
||||
&working.path().join("alice"),
|
||||
Some(&alice.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_init_sync_preferred() {
|
||||
let mut environment = Environment::new();
|
||||
let mut alice = environment
|
||||
.node(Config {
|
||||
policy: Policy::Allow,
|
||||
..Config::test(Alias::new("alice"))
|
||||
})
|
||||
.spawn();
|
||||
|
||||
let bob = environment.profile(profile::Config {
|
||||
preferred_seeds: vec![alice.address()],
|
||||
..config::profile("bob")
|
||||
});
|
||||
let mut bob = Node::new(bob).spawn();
|
||||
let working = environment.tmp().join("working");
|
||||
|
||||
bob.connect(&alice);
|
||||
alice.handle.follow(bob.id, None).unwrap();
|
||||
|
||||
fixtures::repository(working.join("bob"));
|
||||
|
||||
// Necessary for now, if we don't want the new inventry announcement to be considered stale
|
||||
// for Alice.
|
||||
// TODO: Find a way to advance internal clocks instead.
|
||||
thread::sleep(time::Duration::from_millis(3));
|
||||
|
||||
// Bob initializes a repo after her node has started, and after bob has connected to it.
|
||||
test(
|
||||
"examples/rad-init-sync-preferred.md",
|
||||
&working.join("bob"),
|
||||
Some(&bob.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_init_sync_timeout() {
|
||||
let mut environment = Environment::new();
|
||||
let mut alice = environment
|
||||
.node(Config {
|
||||
policy: Policy::Block,
|
||||
..Config::test(Alias::new("alice"))
|
||||
})
|
||||
.spawn();
|
||||
|
||||
let bob = environment.profile(profile::Config {
|
||||
preferred_seeds: vec![alice.address()],
|
||||
..config::profile("bob")
|
||||
});
|
||||
let mut bob = Node::new(bob).spawn();
|
||||
let working = environment.tmp().join("working");
|
||||
|
||||
bob.connect(&alice);
|
||||
alice.handle.follow(bob.id, None).unwrap();
|
||||
|
||||
fixtures::repository(working.join("bob"));
|
||||
|
||||
// Bob initializes a repo after her node has started, and after bob has connected to it.
|
||||
test(
|
||||
"examples/rad-init-sync-timeout.md",
|
||||
&working.join("bob"),
|
||||
Some(&bob.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_init_sync_and_clone() {
|
||||
let mut environment = Environment::new();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use radicle::crypto::{KeyPair, Seed, Signer};
|
|||
use radicle::git;
|
||||
use radicle::git::refname;
|
||||
use radicle::identity::{Id, Visibility};
|
||||
use radicle::node::config::ConnectAddress;
|
||||
use radicle::node::policy::store as policy;
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::node::Database;
|
||||
|
|
@ -152,7 +153,7 @@ pub struct Node<G> {
|
|||
}
|
||||
|
||||
impl Node<MemorySigner> {
|
||||
fn new(profile: Profile) -> Self {
|
||||
pub fn new(profile: Profile) -> Self {
|
||||
let signer = MemorySigner::load(&profile.keystore, None).unwrap();
|
||||
let id = *profile.id();
|
||||
let policies_db = profile.home.node().join(POLICIES_DB_FILE);
|
||||
|
|
@ -227,6 +228,11 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Get the full address of this node.
|
||||
pub fn address(&self) -> ConnectAddress {
|
||||
(self.id, node::Address::from(self.addr)).into()
|
||||
}
|
||||
|
||||
/// Get routing table entries.
|
||||
pub fn routing(&self) -> impl Iterator<Item = (Id, NodeId)> {
|
||||
Database::reader(self.home.node().join(node::NODE_DB_FILE))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::io::Write;
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{fmt, io, thread, time};
|
||||
|
|
@ -105,7 +105,13 @@ impl Spinner {
|
|||
/// Create a new spinner with the given message. Sends animation output to `stderr` and success or
|
||||
/// failure messages to `stdout`.
|
||||
pub fn spinner(message: impl ToString) -> Spinner {
|
||||
spinner_to(message, io::stdout(), io::stderr())
|
||||
let (stdout, stderr) = (io::stdout(), io::stderr());
|
||||
|
||||
if stderr.is_terminal() {
|
||||
spinner_to(message, stdout, stderr)
|
||||
} else {
|
||||
spinner_to(message, stdout, io::sink())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new spinner with the given message, and send output to the given writers.
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ impl FromStr for Alias {
|
|||
}
|
||||
|
||||
/// Options passed to the "connect" node command.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ConnectOptions {
|
||||
/// Establish a persistent connection.
|
||||
pub persistent: bool,
|
||||
|
|
@ -283,6 +283,15 @@ pub struct ConnectOptions {
|
|||
pub timeout: time::Duration,
|
||||
}
|
||||
|
||||
impl Default for ConnectOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
persistent: false,
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a command, on the node control socket.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue