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
$ 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.

View File

@ -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`.
```

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.
```
$ 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:

View File

@ -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

View File

@ -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,
));

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>) {
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");
}
}

View File

@ -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 <string> Description of the project
--default-branch <name> The default branch of the project
--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
--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<String>,
pub branch: Option<String>,
pub interactive: Interactive,
pub visibility: Visibility,
pub visibility: Option<Visibility>,
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<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.
pub fn setup_signing(
node_id: &NodeId,

View File

@ -132,7 +132,7 @@ impl Args for Options {
let mut rid: Option<Id> = None;
let mut json: bool = false;
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 timeout = time::Duration::MAX;
let mut verbose = false;

View File

@ -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`");

View File

@ -1004,7 +1004,7 @@ fn test_replication_via_seed() {
"Radicle Heartwood Protocol & Stack",
"--default-branch",
"master",
"--announce",
"--public",
],
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),
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<Passphrase, anyhow::Error> {
Ok(Passphrase::from(input.trim_end().to_owned()))
}
pub fn select<'a, T>(
prompt: &str,
options: &'a [T],
active: &'a T,
) -> Result<Option<&'a T>, 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::<Vec<_>>()).with_render_config(*CONFIG);
let selection = Select::new(prompt, options.iter().collect::<Vec<_>>())
.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) {

View File

@ -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}'");
}
_ => {}
}