From f2e74fdad2f1cfe0d85c642e40ed3a43710f06d6 Mon Sep 17 00:00:00 2001 From: cloudhead Date: Fri, 1 Sep 2023 21:26:43 +0200 Subject: [PATCH] cli: Choose repo visibility on init This allows a user to choose private or public visibility on `rad init`. It also improves the output to tell the user what happened due to this choice. --- radicle-cli/examples/rad-init-private.md | 12 +- radicle-cli/examples/rad-init-sync.md | 15 ++- radicle-cli/examples/rad-init.md | 12 +- radicle-cli/examples/rad-patch-pull-update.md | 15 ++- radicle-cli/src/commands/auth.rs | 2 +- radicle-cli/src/commands/id.rs | 2 +- radicle-cli/src/commands/init.rs | 126 ++++++++++++------ radicle-cli/src/commands/node.rs | 2 +- radicle-cli/src/commands/rm.rs | 4 +- radicle-cli/tests/commands.rs | 2 +- radicle-term/src/io.rs | 25 ++-- radicle-tools/src/rad-cli-demo.rs | 25 ++-- 12 files changed, 141 insertions(+), 101 deletions(-) diff --git a/radicle-cli/examples/rad-init-private.md b/radicle-cli/examples/rad-init-private.md index 8046f67e..e41c724b 100644 --- a/radicle-cli/examples/rad-init-private.md +++ b/radicle-cli/examples/rad-init-private.md @@ -1,16 +1,18 @@ Alice initializes a *private* repo. ``` ~alice -$ rad init --name heartwood --description "radicle heartwood protocol & stack" --no-confirm --announce --private +$ rad init --name heartwood --description "radicle heartwood protocol & stack" --no-confirm --private Initializing private radicle 👾 project in . -✓ Project heartwood created -✓ Syncing inventory.. -✓ Announcing inventory.. +✓ Project heartwood created. Your project's Repository ID (RID) is rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu. -You can show it any time by running `rad .` +You can show it any time by running `rad .` from this directory. + +You have created a private repository. +This repository will only be visible to you, and to peers you explicitly allow. +To push changes, run `git push`. ``` Bob tries to clone it, and even though he's connected to Alice, it fails. diff --git a/radicle-cli/examples/rad-init-sync.md b/radicle-cli/examples/rad-init-sync.md index 256873d0..ca899a2a 100644 --- a/radicle-cli/examples/rad-init-sync.md +++ b/radicle-cli/examples/rad-init-sync.md @@ -3,14 +3,17 @@ To create your first radicle project, navigate to a git repository, and run the `init` command: ``` -$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm --announce --scope trusted +$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm --public --scope trusted -Initializing radicle 👾 project in . +Initializing public radicle 👾 project in . -✓ Project heartwood created -✓ Syncing inventory.. -✓ Announcing inventory.. +✓ Project heartwood created. Your project's Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji. -You can show it any time by running `rad .` +You can show it any time by running `rad .` from this directory. + +✓ Project successfully announced. + +Your project has been announced to the network and is now discoverable by peers. +To push changes, run `git push`. ``` diff --git a/radicle-cli/examples/rad-init.md b/radicle-cli/examples/rad-init.md index 1b3fc10d..cd0f6613 100644 --- a/radicle-cli/examples/rad-init.md +++ b/radicle-cli/examples/rad-init.md @@ -3,11 +3,11 @@ To create your first radicle project, navigate to a git repository, and run the `init` command. Make sure you have [authenticated](../rad-auth.md) beforehand. ``` -$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm --no-track -v +$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm --no-track --public -v -Initializing radicle 👾 project in . +Initializing public radicle 👾 project in . -✓ Project heartwood created +✓ Project heartwood created. { "name": "heartwood", "description": "Radicle Heartwood Protocol & Stack", @@ -15,9 +15,11 @@ Initializing radicle 👾 project in . } Your project's Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji. -You can show it any time by running `rad .` +You can show it any time by running `rad .` from this directory. -To publish your project to the network, run `git push` +Your project will be announced to the network when you start your node. +You can start your node with `rad node start`. +To push changes, run `git push`. ``` Projects can be listed with the `ls` command: diff --git a/radicle-cli/examples/rad-patch-pull-update.md b/radicle-cli/examples/rad-patch-pull-update.md index 38cc43ff..a8198e14 100644 --- a/radicle-cli/examples/rad-patch-pull-update.md +++ b/radicle-cli/examples/rad-patch-pull-update.md @@ -3,16 +3,19 @@ Let's look at how patch updates work. Alice creates a project and Bob clones it. ``` ~alice -$ rad init --name heartwood --description "radicle heartwood protocol & stack" --no-confirm --announce +$ rad init --name heartwood --description "radicle heartwood protocol & stack" --no-confirm --public -Initializing radicle 👾 project in . +Initializing public radicle 👾 project in . -✓ Project heartwood created -✓ Syncing inventory.. -✓ Announcing inventory.. +✓ Project heartwood created. Your project's Repository ID (RID) is rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK. -You can show it any time by running `rad .` +You can show it any time by running `rad .` from this directory. + +✓ Project successfully announced. + +Your project has been announced to the network and is now discoverable by peers. +To push changes, run `git push`. ``` ``` ~bob diff --git a/radicle-cli/src/commands/auth.rs b/radicle-cli/src/commands/auth.rs index 5572d578..0fb9e0df 100644 --- a/radicle-cli/src/commands/auth.rs +++ b/radicle-cli/src/commands/auth.rs @@ -84,7 +84,7 @@ pub fn init(options: Options) -> anyhow::Result<()> { if let Ok(version) = radicle::git::version() { if version < radicle::git::VERSION_REQUIRED { - term::warning(&format!( + term::warning(format!( "Your git version is unsupported, please upgrade to {} or later", radicle::git::VERSION_REQUIRED, )); diff --git a/radicle-cli/src/commands/id.rs b/radicle-cli/src/commands/id.rs index 17fe4202..ab16419f 100644 --- a/radicle-cli/src/commands/id.rs +++ b/radicle-cli/src/commands/id.rs @@ -522,7 +522,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { fn warn_out_of_date(revision: &Revision, previous: &Identity) { if revision.current != previous.current { term::warning("Revision is out of date"); - term::warning(&format!("{} =/= {}", revision.current, previous.current)); + term::warning(format!("{} =/= {}", revision.current, previous.current)); term::tip!("Consider using 'rad id rebase' to update the proposal to the latest identity"); } } diff --git a/radicle-cli/src/commands/init.rs b/radicle-cli/src/commands/init.rs index 68fbb62a..b268b4fd 100644 --- a/radicle-cli/src/commands/init.rs +++ b/radicle-cli/src/commands/init.rs @@ -1,18 +1,21 @@ #![allow(clippy::or_fun_call)] +#![allow(clippy::collapsible_else_if)] use std::convert::TryFrom; use std::env; use std::ffi::OsString; use std::path::PathBuf; +use std::str::FromStr; use anyhow::{anyhow, bail, Context as _}; use serde_json as json; -use radicle::crypto::ssh; +use radicle::crypto::{ssh, Verified}; use radicle::git::RefString; use radicle::identity::Visibility; use radicle::node::tracking::Scope; use radicle::node::{Handle, NodeId}; -use radicle::profile; +use radicle::prelude::Doc; +use radicle::{profile, Node}; use crate::git; use crate::terminal as term; @@ -34,10 +37,10 @@ Options --description Description of the project --default-branch The default branch of the project --scope Tracking scope (default: all) - --private Set repository visibility to *private* (default: public) + --private Set repository visibility to *private* + --public Set repository visibility to *public* -u, --set-upstream Setup the upstream of the default branch --setup-signing Setup the radicle key as a signing key for this repository - --announce Announce the new project to the network --no-confirm Don't ask for confirmation during setup -v, --verbose Verbose mode --help Print help @@ -51,11 +54,10 @@ pub struct Options { pub description: Option, pub branch: Option, pub interactive: Interactive, - pub visibility: Visibility, + pub visibility: Option, pub setup_signing: bool, pub scope: Scope, pub set_upstream: bool, - pub announce: bool, pub verbose: bool, pub track: bool, } @@ -73,11 +75,10 @@ impl Args for Options { let mut interactive = Interactive::Yes; let mut set_upstream = false; let mut setup_signing = false; - let mut announce = false; let mut scope = Scope::All; let mut track = true; let mut verbose = false; - let mut visibility = Visibility::default(); + let mut visibility = None; while let Some(arg) = parser.next()? { match arg { @@ -124,9 +125,6 @@ impl Args for Options { Long("setup-signing") => { setup_signing = true; } - Long("announce") => { - announce = true; - } Long("no-confirm") => { interactive = Interactive::No; } @@ -134,7 +132,10 @@ impl Args for Options { track = false; } Long("private") => { - visibility = Visibility::private([]); + visibility = Some(Visibility::private([])); + } + Long("public") => { + visibility = Some(Visibility::Public); } Long("verbose") | Short('v') => { verbose = true; @@ -159,7 +160,6 @@ impl Args for Options { interactive, set_upstream, setup_signing, - announce, track, visibility, verbose, @@ -190,24 +190,30 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> term::headline(format!( "Initializing{}radicle 👾 project in {}", - if !options.visibility.is_public() { - term::format::yellow(" private ") + if let Some(visibility) = &options.visibility { + if !visibility.is_public() { + term::format::yellow(" private ") + } else { + term::format::positive(" public ") + } } else { term::format::default(" ") }, if path == cwd { - term::format::highlight(".").to_string() + term::format::tertiary(".").to_string() } else { - term::format::highlight(path.display()).to_string() + term::format::tertiary(path.display()).to_string() } )); + // TODO: Move up. if let Ok((remote, _)) = git::rad_remote(&repo) { if let Some(remote) = remote.url() { bail!("repository is already initialized with remote {remote}"); } } + // TODO: Move up. let signer = term::signer(profile)?; let head: String = repo .head() @@ -241,6 +247,16 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> }); let branch = RefString::try_from(branch.clone()) .map_err(|e| anyhow!("invalid branch name {:?}: {}", branch, e))?; + let visibility = if let Some(v) = options.visibility { + v + } else { + let selected = term::select( + "Visibility", + &["public", "private"], + "Public repositories are accessible by anyone on the network after initialization", + )?; + Visibility::from_str(selected)? + }; let mut node = radicle::Node::new(profile.socket()); let mut spinner = term::spinner("Initializing..."); @@ -251,7 +267,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> &name, &description, branch, - options.visibility, + visibility, &signer, &profile.storage, ) { @@ -265,7 +281,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> } spinner.message(format!( - "Project {} created", + "Project {} created.", term::format::highlight(proj.name()) )); spinner.finish(); @@ -291,24 +307,6 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> self::setup_signing(profile.id(), &repo, interactive)?; } - if node.is_running() { - let spinner = term::spinner("Syncing inventory.."); - if let Err(e) = node.sync_inventory() { - spinner.error(e); - } else { - spinner.finish(); - } - } - - if options.announce { - let spinner = term::spinner("Announcing inventory.."); - if let Err(e) = node.announce_inventory() { - spinner.error(e); - } else { - spinner.finish(); - } - } - term::blank(); term::info!( "Your project's Repository ID {} is {}.", @@ -316,17 +314,21 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> term::format::highlight(id.urn()) ); term::info!( - "You can show it any time by running {}", + "You can show it any time by running {} from this directory.", term::format::command("rad .") ); + term::blank(); - if !options.announce { + // Announce inventory to network. + if let Err(e) = announce(doc, &mut node) { + term::blank(); + term::warning(format!( + "There was an error announcing your project to the network: {e}" + )); + term::warning("Try again with `rad sync --announce`, or check your logs with `rad node logs`."); term::blank(); - term::info!( - "To publish your project to the network, run {}", - term::format::command(push_cmd) - ); } + term::info!("To push changes, run {}.", term::format::command(push_cmd)); } Err(err) => { spinner.failed(); @@ -340,6 +342,44 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> Ok(()) } +pub fn announce(doc: Doc, node: &mut Node) -> 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") + ); + } + } else { + // TODO: Tell users how to make the project public. + term::info!( + "You have created a {} repository.", + term::format::yellow("private") + ); + term::info!( + "This repository will only be visible to you, \ + and to peers you explicitly allow.", + ); + } + + Ok(()) +} + /// Setup radicle key as commit signing key in repository. pub fn setup_signing( node_id: &NodeId, diff --git a/radicle-cli/src/commands/node.rs b/radicle-cli/src/commands/node.rs index 4180f79b..600806cc 100644 --- a/radicle-cli/src/commands/node.rs +++ b/radicle-cli/src/commands/node.rs @@ -132,7 +132,7 @@ impl Args for Options { let mut rid: Option = None; let mut json: bool = false; let mut addr: Option> = None; - let mut lines: usize = 10; + let mut lines: usize = 60; let mut count: usize = usize::MAX; let mut timeout = time::Duration::MAX; let mut verbose = false; diff --git a/radicle-cli/src/commands/rm.rs b/radicle-cli/src/commands/rm.rs index 0ac592be..9b214cc0 100644 --- a/radicle-cli/src/commands/rm.rs +++ b/radicle-cli/src/commands/rm.rs @@ -101,7 +101,7 @@ fn untrack(rid: &Id, profile: &Profile) -> anyhow::Result<()> { }; if let Err(e) = result { - term::warning(&format!("Failed to untrack repository: {e}")); + term::warning(format!("Failed to untrack repository: {e}")); term::warning("Make sure to untrack this repository when your node is running"); } else { term::success!("Untracked {rid}") @@ -116,7 +116,7 @@ fn remove_remote(rid: &Id) -> anyhow::Result<()> { .map_err(|err| err.into()) .and_then(|repo| git::remove_remote(&repo, rid)) { - term::warning(&format!( + term::warning(format!( "Attempted to remove 'rad' remote from working copy: {e}" )); term::warning("In case a working copy exists, make sure to `git remote remove rad`"); diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 27f20361..240c80b5 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -1004,7 +1004,7 @@ fn test_replication_via_seed() { "Radicle Heartwood Protocol & Stack", "--default-branch", "master", - "--announce", + "--public", ], working.join("alice"), ) diff --git a/radicle-term/src/io.rs b/radicle-term/src/io.rs index 67306f00..68ebd5d5 100644 --- a/radicle-term/src/io.rs +++ b/radicle-term/src/io.rs @@ -28,7 +28,9 @@ pub static CONFIG: Lazy = Lazy::new(|| RenderConfig { prompt_prefix: Styled::new("?").with_fg(Color::LightBlue), answered_prompt_prefix: Styled::new("✓").with_fg(Color::LightGreen), answer: StyleSheet::new(), - highlighted_option_prefix: Styled::new("*").with_fg(Color::LightYellow), + highlighted_option_prefix: Styled::new("✓").with_fg(Color::LightYellow), + selected_option: Some(StyleSheet::new().with_fg(Color::LightYellow)), + option: StyleSheet::new(), help_message: StyleSheet::new().with_fg(Color::DarkGrey), default_value: StyleSheet::new().with_fg(Color::LightBlue), error_message: ErrorMessageRenderConfig::default_colored() @@ -152,7 +154,7 @@ pub fn subcommand(msg: impl fmt::Display) { println!("{}", style(format!("Running `{msg}`...")).dim()); } -pub fn warning(warning: &str) { +pub fn warning(warning: impl fmt::Display) { println!( "{} {} {warning}", WARNING_PREFIX, @@ -237,23 +239,16 @@ pub fn passphrase_stdin() -> Result { Ok(Passphrase::from(input.trim_end().to_owned())) } -pub fn select<'a, T>( - prompt: &str, - options: &'a [T], - active: &'a T, -) -> Result, InquireError> +pub fn select<'a, T>(prompt: &str, options: &'a [T], help: &str) -> Result<&'a T, InquireError> where T: fmt::Display + Eq + PartialEq, { - let active = options.iter().position(|o| o == active); - let selection = - Select::new(prompt, options.iter().collect::>()).with_render_config(*CONFIG); + let selection = Select::new(prompt, options.iter().collect::>()) + .with_vim_mode(true) + .with_help_message(help) + .with_render_config(*CONFIG); - if let Some(active) = active { - selection.with_starting_cursor(active).prompt_skippable() - } else { - selection.prompt_skippable() - } + selection.with_starting_cursor(0).prompt() } pub fn markdown(content: &str) { diff --git a/radicle-tools/src/rad-cli-demo.rs b/radicle-tools/src/rad-cli-demo.rs index 14eee0e5..618662b7 100644 --- a/radicle-tools/src/rad-cli-demo.rs +++ b/radicle-tools/src/rad-cli-demo.rs @@ -13,16 +13,16 @@ fn main() -> anyhow::Result<()> { "editor", "prompt", ], - &"spinner", + "Choose wisely!", )?; - match demo { - Some(&"confirm") => { + match *demo { + "confirm" => { if terminal::confirm("Would you like to proceed?") { terminal::success!("You said 'yes'"); } } - Some(&"editor") => { + "editor" => { let output = terminal::editor::Editor::new() .extension("rs") .edit("// Enter code here."); @@ -40,7 +40,7 @@ fn main() -> anyhow::Result<()> { } } } - Some(&"spinner") => { + "spinner" => { let mut spinner = terminal::spinner("Spinning turbines.."); thread::sleep(time::Duration::from_secs(1)); spinner.message("Still spinning.."); @@ -51,27 +51,22 @@ fn main() -> anyhow::Result<()> { spinner.finish(); } - Some(&"spinner-drop") => { + "spinner-drop" => { let _spinner = terminal::spinner("Spinning turbines.."); thread::sleep(time::Duration::from_secs(3)); } - Some(&"spinner-error") => { + "spinner-error" => { let spinner = terminal::spinner("Spinning turbines.."); thread::sleep(time::Duration::from_secs(3)); spinner.error("broken turbine"); } - Some(&"prompt") => { + "prompt" => { let fruit = terminal::io::select( "Enter your favorite fruit:", &["apple", "pear", "banana", "strawberry"], - &"apple", + "Choose wisely!", )?; - - if let Some(fruit) = fruit { - terminal::success!("You have chosen '{fruit}'"); - } else { - terminal::info!("Ok, bye."); - } + terminal::success!("You have chosen '{fruit}'"); } _ => {} }