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:
parent
d6d280d450
commit
f2e74fdad2
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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`");
|
||||
|
|
|
|||
|
|
@ -1004,7 +1004,7 @@ fn test_replication_via_seed() {
|
|||
"Radicle Heartwood Protocol & Stack",
|
||||
"--default-branch",
|
||||
"master",
|
||||
"--announce",
|
||||
"--public",
|
||||
],
|
||||
working.join("alice"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}'");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue