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.
This commit is contained in:
cloudhead 2023-09-01 21:26:43 +02:00
parent d6d280d450
commit f2e74fdad2
No known key found for this signature in database
12 changed files with 141 additions and 101 deletions

View File

@ -1,16 +1,18 @@
Alice initializes a *private* repo. Alice initializes a *private* repo.
``` ~alice ``` ~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 . Initializing private radicle 👾 project in .
✓ Project heartwood created ✓ Project heartwood created.
✓ Syncing inventory..
✓ Announcing inventory..
Your project's Repository ID (RID) is rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu. 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. Bob tries to clone it, and even though he's connected to Alice, it fails.

View File

@ -3,14 +3,17 @@ To create your first radicle project, navigate to a git repository, and run
the `init` command: 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 ✓ Project heartwood created.
✓ Syncing inventory..
✓ Announcing inventory..
Your project's Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji. 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`.
``` ```

View File

@ -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. `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", "name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack", "description": "Radicle Heartwood Protocol & Stack",
@ -15,9 +15,11 @@ Initializing radicle 👾 project in .
} }
Your project's Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji. 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: Projects can be listed with the `ls` command:

View File

@ -3,16 +3,19 @@ Let's look at how patch updates work.
Alice creates a project and Bob clones it. Alice creates a project and Bob clones it.
``` ~alice ``` ~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 ✓ Project heartwood created.
✓ Syncing inventory..
✓ Announcing inventory..
Your project's Repository ID (RID) is rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK. 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 ``` ~bob

View File

@ -84,7 +84,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
if let Ok(version) = radicle::git::version() { if let Ok(version) = radicle::git::version() {
if version < radicle::git::VERSION_REQUIRED { if version < radicle::git::VERSION_REQUIRED {
term::warning(&format!( term::warning(format!(
"Your git version is unsupported, please upgrade to {} or later", "Your git version is unsupported, please upgrade to {} or later",
radicle::git::VERSION_REQUIRED, radicle::git::VERSION_REQUIRED,
)); ));

View File

@ -522,7 +522,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
fn warn_out_of_date(revision: &Revision, previous: &Identity<Oid>) { fn warn_out_of_date(revision: &Revision, previous: &Identity<Oid>) {
if revision.current != previous.current { if revision.current != previous.current {
term::warning("Revision is out of date"); 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"); term::tip!("Consider using 'rad id rebase' to update the proposal to the latest identity");
} }
} }

View File

@ -1,18 +1,21 @@
#![allow(clippy::or_fun_call)] #![allow(clippy::or_fun_call)]
#![allow(clippy::collapsible_else_if)]
use std::convert::TryFrom; use std::convert::TryFrom;
use std::env; use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{anyhow, bail, Context as _}; use anyhow::{anyhow, bail, Context as _};
use serde_json as json; use serde_json as json;
use radicle::crypto::ssh; use radicle::crypto::{ssh, Verified};
use radicle::git::RefString; use radicle::git::RefString;
use radicle::identity::Visibility; use radicle::identity::Visibility;
use radicle::node::tracking::Scope; use radicle::node::tracking::Scope;
use radicle::node::{Handle, NodeId}; use radicle::node::{Handle, NodeId};
use radicle::profile; use radicle::prelude::Doc;
use radicle::{profile, Node};
use crate::git; use crate::git;
use crate::terminal as term; use crate::terminal as term;
@ -34,10 +37,10 @@ Options
--description <string> Description of the project --description <string> Description of the project
--default-branch <name> The default branch of the project --default-branch <name> The default branch of the project
--scope <scope> Tracking scope (default: all) --scope <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 -u, --set-upstream Setup the upstream of the default branch
--setup-signing Setup the radicle key as a signing key for this repository --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 --no-confirm Don't ask for confirmation during setup
-v, --verbose Verbose mode -v, --verbose Verbose mode
--help Print help --help Print help
@ -51,11 +54,10 @@ pub struct Options {
pub description: Option<String>, pub description: Option<String>,
pub branch: Option<String>, pub branch: Option<String>,
pub interactive: Interactive, pub interactive: Interactive,
pub visibility: Visibility, pub visibility: Option<Visibility>,
pub setup_signing: bool, pub setup_signing: bool,
pub scope: Scope, pub scope: Scope,
pub set_upstream: bool, pub set_upstream: bool,
pub announce: bool,
pub verbose: bool, pub verbose: bool,
pub track: bool, pub track: bool,
} }
@ -73,11 +75,10 @@ impl Args for Options {
let mut interactive = Interactive::Yes; let mut interactive = Interactive::Yes;
let mut set_upstream = false; let mut set_upstream = false;
let mut setup_signing = false; let mut setup_signing = false;
let mut announce = false;
let mut scope = Scope::All; let mut scope = Scope::All;
let mut track = true; let mut track = true;
let mut verbose = false; let mut verbose = false;
let mut visibility = Visibility::default(); let mut visibility = None;
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
match arg { match arg {
@ -124,9 +125,6 @@ impl Args for Options {
Long("setup-signing") => { Long("setup-signing") => {
setup_signing = true; setup_signing = true;
} }
Long("announce") => {
announce = true;
}
Long("no-confirm") => { Long("no-confirm") => {
interactive = Interactive::No; interactive = Interactive::No;
} }
@ -134,7 +132,10 @@ impl Args for Options {
track = false; track = false;
} }
Long("private") => { Long("private") => {
visibility = Visibility::private([]); visibility = Some(Visibility::private([]));
}
Long("public") => {
visibility = Some(Visibility::Public);
} }
Long("verbose") | Short('v') => { Long("verbose") | Short('v') => {
verbose = true; verbose = true;
@ -159,7 +160,6 @@ impl Args for Options {
interactive, interactive,
set_upstream, set_upstream,
setup_signing, setup_signing,
announce,
track, track,
visibility, visibility,
verbose, verbose,
@ -190,24 +190,30 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
term::headline(format!( term::headline(format!(
"Initializing{}radicle 👾 project in {}", "Initializing{}radicle 👾 project in {}",
if !options.visibility.is_public() { if let Some(visibility) = &options.visibility {
term::format::yellow(" private ") if !visibility.is_public() {
term::format::yellow(" private ")
} else {
term::format::positive(" public ")
}
} else { } else {
term::format::default(" ") term::format::default(" ")
}, },
if path == cwd { if path == cwd {
term::format::highlight(".").to_string() term::format::tertiary(".").to_string()
} else { } 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 Ok((remote, _)) = git::rad_remote(&repo) {
if let Some(remote) = remote.url() { if let Some(remote) = remote.url() {
bail!("repository is already initialized with remote {remote}"); bail!("repository is already initialized with remote {remote}");
} }
} }
// TODO: Move up.
let signer = term::signer(profile)?; let signer = term::signer(profile)?;
let head: String = repo let head: String = repo
.head() .head()
@ -241,6 +247,16 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
}); });
let branch = RefString::try_from(branch.clone()) let branch = RefString::try_from(branch.clone())
.map_err(|e| anyhow!("invalid branch name {:?}: {}", branch, e))?; .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 node = radicle::Node::new(profile.socket());
let mut spinner = term::spinner("Initializing..."); let mut spinner = term::spinner("Initializing...");
@ -251,7 +267,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
&name, &name,
&description, &description,
branch, branch,
options.visibility, visibility,
&signer, &signer,
&profile.storage, &profile.storage,
) { ) {
@ -265,7 +281,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
} }
spinner.message(format!( spinner.message(format!(
"Project {} created", "Project {} created.",
term::format::highlight(proj.name()) term::format::highlight(proj.name())
)); ));
spinner.finish(); spinner.finish();
@ -291,24 +307,6 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
self::setup_signing(profile.id(), &repo, interactive)?; 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::blank();
term::info!( term::info!(
"Your project's Repository ID {} is {}.", "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::format::highlight(id.urn())
); );
term::info!( 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::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::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) => { Err(err) => {
spinner.failed(); spinner.failed();
@ -340,6 +342,44 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
Ok(()) Ok(())
} }
pub fn announce(doc: Doc<Verified>, 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. /// Setup radicle key as commit signing key in repository.
pub fn setup_signing( pub fn setup_signing(
node_id: &NodeId, node_id: &NodeId,

View File

@ -132,7 +132,7 @@ impl Args for Options {
let mut rid: Option<Id> = None; let mut rid: Option<Id> = None;
let mut json: bool = false; let mut json: bool = false;
let mut addr: Option<PeerAddr<NodeId, Address>> = None; let mut addr: Option<PeerAddr<NodeId, Address>> = None;
let mut lines: usize = 10; let mut lines: usize = 60;
let mut count: usize = usize::MAX; let mut count: usize = usize::MAX;
let mut timeout = time::Duration::MAX; let mut timeout = time::Duration::MAX;
let mut verbose = false; let mut verbose = false;

View File

@ -101,7 +101,7 @@ fn untrack(rid: &Id, profile: &Profile) -> anyhow::Result<()> {
}; };
if let Err(e) = 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"); term::warning("Make sure to untrack this repository when your node is running");
} else { } else {
term::success!("Untracked {rid}") term::success!("Untracked {rid}")
@ -116,7 +116,7 @@ fn remove_remote(rid: &Id) -> anyhow::Result<()> {
.map_err(|err| err.into()) .map_err(|err| err.into())
.and_then(|repo| git::remove_remote(&repo, rid)) .and_then(|repo| git::remove_remote(&repo, rid))
{ {
term::warning(&format!( term::warning(format!(
"Attempted to remove 'rad' remote from working copy: {e}" "Attempted to remove 'rad' remote from working copy: {e}"
)); ));
term::warning("In case a working copy exists, make sure to `git remote remove rad`"); term::warning("In case a working copy exists, make sure to `git remote remove rad`");

View File

@ -1004,7 +1004,7 @@ fn test_replication_via_seed() {
"Radicle Heartwood Protocol & Stack", "Radicle Heartwood Protocol & Stack",
"--default-branch", "--default-branch",
"master", "master",
"--announce", "--public",
], ],
working.join("alice"), working.join("alice"),
) )

View File

@ -28,7 +28,9 @@ pub static CONFIG: Lazy<RenderConfig> = Lazy::new(|| RenderConfig {
prompt_prefix: Styled::new("?").with_fg(Color::LightBlue), prompt_prefix: Styled::new("?").with_fg(Color::LightBlue),
answered_prompt_prefix: Styled::new("").with_fg(Color::LightGreen), answered_prompt_prefix: Styled::new("").with_fg(Color::LightGreen),
answer: StyleSheet::new(), 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), help_message: StyleSheet::new().with_fg(Color::DarkGrey),
default_value: StyleSheet::new().with_fg(Color::LightBlue), default_value: StyleSheet::new().with_fg(Color::LightBlue),
error_message: ErrorMessageRenderConfig::default_colored() error_message: ErrorMessageRenderConfig::default_colored()
@ -152,7 +154,7 @@ pub fn subcommand(msg: impl fmt::Display) {
println!("{}", style(format!("Running `{msg}`...")).dim()); println!("{}", style(format!("Running `{msg}`...")).dim());
} }
pub fn warning(warning: &str) { pub fn warning(warning: impl fmt::Display) {
println!( println!(
"{} {} {warning}", "{} {} {warning}",
WARNING_PREFIX, WARNING_PREFIX,
@ -237,23 +239,16 @@ pub fn passphrase_stdin() -> Result<Passphrase, anyhow::Error> {
Ok(Passphrase::from(input.trim_end().to_owned())) Ok(Passphrase::from(input.trim_end().to_owned()))
} }
pub fn select<'a, T>( pub fn select<'a, T>(prompt: &str, options: &'a [T], help: &str) -> Result<&'a T, InquireError>
prompt: &str,
options: &'a [T],
active: &'a T,
) -> Result<Option<&'a T>, InquireError>
where where
T: fmt::Display + Eq + PartialEq, T: fmt::Display + Eq + PartialEq,
{ {
let active = options.iter().position(|o| o == active); let selection = Select::new(prompt, options.iter().collect::<Vec<_>>())
let selection = .with_vim_mode(true)
Select::new(prompt, options.iter().collect::<Vec<_>>()).with_render_config(*CONFIG); .with_help_message(help)
.with_render_config(*CONFIG);
if let Some(active) = active { selection.with_starting_cursor(0).prompt()
selection.with_starting_cursor(active).prompt_skippable()
} else {
selection.prompt_skippable()
}
} }
pub fn markdown(content: &str) { pub fn markdown(content: &str) {

View File

@ -13,16 +13,16 @@ fn main() -> anyhow::Result<()> {
"editor", "editor",
"prompt", "prompt",
], ],
&"spinner", "Choose wisely!",
)?; )?;
match demo { match *demo {
Some(&"confirm") => { "confirm" => {
if terminal::confirm("Would you like to proceed?") { if terminal::confirm("Would you like to proceed?") {
terminal::success!("You said 'yes'"); terminal::success!("You said 'yes'");
} }
} }
Some(&"editor") => { "editor" => {
let output = terminal::editor::Editor::new() let output = terminal::editor::Editor::new()
.extension("rs") .extension("rs")
.edit("// Enter code here."); .edit("// Enter code here.");
@ -40,7 +40,7 @@ fn main() -> anyhow::Result<()> {
} }
} }
} }
Some(&"spinner") => { "spinner" => {
let mut spinner = terminal::spinner("Spinning turbines.."); let mut spinner = terminal::spinner("Spinning turbines..");
thread::sleep(time::Duration::from_secs(1)); thread::sleep(time::Duration::from_secs(1));
spinner.message("Still spinning.."); spinner.message("Still spinning..");
@ -51,27 +51,22 @@ fn main() -> anyhow::Result<()> {
spinner.finish(); spinner.finish();
} }
Some(&"spinner-drop") => { "spinner-drop" => {
let _spinner = terminal::spinner("Spinning turbines.."); let _spinner = terminal::spinner("Spinning turbines..");
thread::sleep(time::Duration::from_secs(3)); thread::sleep(time::Duration::from_secs(3));
} }
Some(&"spinner-error") => { "spinner-error" => {
let spinner = terminal::spinner("Spinning turbines.."); let spinner = terminal::spinner("Spinning turbines..");
thread::sleep(time::Duration::from_secs(3)); thread::sleep(time::Duration::from_secs(3));
spinner.error("broken turbine"); spinner.error("broken turbine");
} }
Some(&"prompt") => { "prompt" => {
let fruit = terminal::io::select( let fruit = terminal::io::select(
"Enter your favorite fruit:", "Enter your favorite fruit:",
&["apple", "pear", "banana", "strawberry"], &["apple", "pear", "banana", "strawberry"],
&"apple", "Choose wisely!",
)?; )?;
terminal::success!("You have chosen '{fruit}'");
if let Some(fruit) = fruit {
terminal::success!("You have chosen '{fruit}'");
} else {
terminal::info!("Ok, bye.");
}
} }
_ => {} _ => {}
} }