cli/init: use Clap
This commit is contained in:
parent
10e7b94c30
commit
8ba3087cb9
|
|
@ -58,7 +58,10 @@ const COMMANDS: &[CommandItem] = &[
|
|||
},
|
||||
CommandItem::Lexopt(crate::commands::help::HELP),
|
||||
CommandItem::Lexopt(crate::commands::id::HELP),
|
||||
CommandItem::Lexopt(crate::commands::init::HELP),
|
||||
CommandItem::Clap {
|
||||
name: "init",
|
||||
about: crate::commands::init::ABOUT,
|
||||
},
|
||||
CommandItem::Lexopt(crate::commands::inbox::HELP),
|
||||
CommandItem::Lexopt(crate::commands::inspect::HELP),
|
||||
CommandItem::Clap {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
#![allow(clippy::or_fun_call)]
|
||||
#![allow(clippy::collapsible_else_if)]
|
||||
|
||||
mod args;
|
||||
|
||||
pub use args::Args;
|
||||
pub(crate) use args::ABOUT;
|
||||
|
||||
use std::collections::HashSet;
|
||||
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 _};
|
||||
|
|
@ -18,7 +22,6 @@ use radicle::git::RefString;
|
|||
use radicle::identity::project::ProjectName;
|
||||
use radicle::identity::{Doc, RepoId, Visibility};
|
||||
use radicle::node::events::UploadPack;
|
||||
use radicle::node::policy::Scope;
|
||||
use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT};
|
||||
use radicle::storage::ReadStorage as _;
|
||||
use radicle::{profile, Node};
|
||||
|
|
@ -26,168 +29,12 @@ use radicle::{profile, Node};
|
|||
use crate::commands;
|
||||
use crate::git;
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
use crate::terminal::Interactive;
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "init",
|
||||
description: "Initialize a Radicle repository",
|
||||
version: env!("RADICLE_VERSION"),
|
||||
usage: r#"
|
||||
Usage
|
||||
|
||||
rad init [<path>] [<option>...]
|
||||
|
||||
Options
|
||||
|
||||
--name <string> Name of the repository
|
||||
--description <string> Description of the repository
|
||||
--default-branch <name> The default branch of the repository
|
||||
--scope <scope> Repository follow scope: `followed` or `all` (default: all)
|
||||
--private Set repository visibility to *private*
|
||||
--public Set repository visibility to *public*
|
||||
--existing <rid> Setup repository as an existing Radicle repository
|
||||
-u, --set-upstream Setup the upstream of the default branch
|
||||
--setup-signing Setup the radicle key as a signing key for this repository
|
||||
--no-confirm Don't ask for confirmation during setup
|
||||
--no-seed Don't seed this repository after initializing it
|
||||
-v, --verbose Verbose mode
|
||||
--help Print help
|
||||
"#,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Options {
|
||||
pub path: Option<PathBuf>,
|
||||
pub name: Option<ProjectName>,
|
||||
pub description: Option<String>,
|
||||
pub branch: Option<String>,
|
||||
pub interactive: Interactive,
|
||||
pub visibility: Option<Visibility>,
|
||||
pub existing: Option<RepoId>,
|
||||
pub setup_signing: bool,
|
||||
pub scope: Scope,
|
||||
pub set_upstream: bool,
|
||||
pub verbose: bool,
|
||||
pub seed: bool,
|
||||
}
|
||||
|
||||
impl Args for Options {
|
||||
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
||||
use lexopt::prelude::*;
|
||||
|
||||
let mut parser = lexopt::Parser::from_args(args);
|
||||
let mut path: Option<PathBuf> = None;
|
||||
|
||||
let mut name = None;
|
||||
let mut description = None;
|
||||
let mut branch = None;
|
||||
let mut interactive = Interactive::Yes;
|
||||
let mut set_upstream = false;
|
||||
let mut setup_signing = false;
|
||||
let mut scope = Scope::All;
|
||||
let mut existing = None;
|
||||
let mut seed = true;
|
||||
let mut verbose = false;
|
||||
let mut visibility = None;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("name") if name.is_none() => {
|
||||
let value = parser.value()?;
|
||||
let value = term::args::string(&value);
|
||||
let value = ProjectName::try_from(value)?;
|
||||
|
||||
name = Some(value);
|
||||
}
|
||||
Long("description") if description.is_none() => {
|
||||
let value = parser
|
||||
.value()?
|
||||
.to_str()
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"invalid repository description specified with `--description`"
|
||||
))?
|
||||
.to_owned();
|
||||
|
||||
description = Some(value);
|
||||
}
|
||||
Long("default-branch") if branch.is_none() => {
|
||||
let value = parser
|
||||
.value()?
|
||||
.to_str()
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"invalid branch specified with `--default-branch`"
|
||||
))?
|
||||
.to_owned();
|
||||
|
||||
branch = Some(value);
|
||||
}
|
||||
Long("scope") => {
|
||||
let value = parser.value()?;
|
||||
|
||||
scope = term::args::parse_value("scope", value)?;
|
||||
}
|
||||
Long("set-upstream") | Short('u') => {
|
||||
set_upstream = true;
|
||||
}
|
||||
Long("setup-signing") => {
|
||||
setup_signing = true;
|
||||
}
|
||||
Long("no-confirm") => {
|
||||
interactive = Interactive::No;
|
||||
}
|
||||
Long("no-seed") => {
|
||||
seed = false;
|
||||
}
|
||||
Long("private") => {
|
||||
visibility = Some(Visibility::private([]));
|
||||
}
|
||||
Long("public") => {
|
||||
visibility = Some(Visibility::Public);
|
||||
}
|
||||
Long("existing") if existing.is_none() => {
|
||||
let val = parser.value()?;
|
||||
let rid = term::args::rid(&val)?;
|
||||
|
||||
existing = Some(rid);
|
||||
}
|
||||
Long("verbose") | Short('v') => {
|
||||
verbose = true;
|
||||
}
|
||||
Long("help") | Short('h') => {
|
||||
return Err(Error::Help.into());
|
||||
}
|
||||
Value(val) if path.is_none() => {
|
||||
path = Some(val.into());
|
||||
}
|
||||
_ => anyhow::bail!(arg.unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
Options {
|
||||
path,
|
||||
name,
|
||||
description,
|
||||
branch,
|
||||
scope,
|
||||
existing,
|
||||
interactive,
|
||||
set_upstream,
|
||||
setup_signing,
|
||||
seed,
|
||||
visibility,
|
||||
verbose,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||
let profile = ctx.profile()?;
|
||||
let cwd = env::current_dir()?;
|
||||
let path = options.path.as_deref().unwrap_or(cwd.as_path());
|
||||
let path = args.path.as_deref().unwrap_or(cwd.as_path());
|
||||
let repo = match git::Repository::open(path) {
|
||||
Ok(r) => r,
|
||||
Err(e) if e.is_not_found() => {
|
||||
|
|
@ -201,20 +48,18 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(rid) = options.existing {
|
||||
init_existing(repo, rid, options, &profile)
|
||||
if let Some(rid) = args.existing {
|
||||
init_existing(repo, rid, args, &profile)
|
||||
} else {
|
||||
init(repo, options, &profile)
|
||||
init(repo, args, &profile)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
repo: git::Repository,
|
||||
options: Options,
|
||||
profile: &profile::Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
pub fn init(repo: git::Repository, args: Args, profile: &profile::Profile) -> anyhow::Result<()> {
|
||||
let path = dunce::canonicalize(repo.workdir().unwrap_or_else(|| repo.path()))?;
|
||||
let interactive = options.interactive;
|
||||
let interactive = args.interactive();
|
||||
let visibility = args.visibility();
|
||||
let seed = args.seed();
|
||||
|
||||
let default_branch = match find_default_branch(&repo) {
|
||||
Err(err @ DefaultBranchError::Head) => {
|
||||
|
|
@ -233,21 +78,22 @@ pub fn init(
|
|||
|
||||
term::headline(format!(
|
||||
"Initializing{}radicle 👾 repository in {}..",
|
||||
if let Some(visibility) = &options.visibility {
|
||||
term::format::spaced(term::format::visibility(visibility))
|
||||
} else {
|
||||
term::format::default(" ").into()
|
||||
match visibility {
|
||||
Some(ref visibility) => term::format::spaced(term::format::visibility(visibility)),
|
||||
None => term::format::default(" ").into(),
|
||||
},
|
||||
term::format::dim(path.display())
|
||||
));
|
||||
|
||||
let name: ProjectName = match options.name {
|
||||
let name: ProjectName = match args.name {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
let default = path
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.and_then(|f| ProjectName::try_from(f).ok());
|
||||
// TODO(finto): this is interactive without checking `interactive` –
|
||||
// this should check if interactive and use the default if not
|
||||
let name = term::input(
|
||||
"Name",
|
||||
default,
|
||||
|
|
@ -257,13 +103,13 @@ pub fn init(
|
|||
name.ok_or_else(|| anyhow::anyhow!("A project name is required."))?
|
||||
}
|
||||
};
|
||||
let description = match options.description {
|
||||
let description = match args.description {
|
||||
Some(desc) => desc,
|
||||
None => {
|
||||
term::input("Description", None, Some("You may leave this blank"))?.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
let branch = match options.branch {
|
||||
let branch = match args.branch {
|
||||
Some(branch) => branch,
|
||||
None if interactive.yes() => term::input(
|
||||
"Default branch",
|
||||
|
|
@ -275,9 +121,11 @@ pub fn init(
|
|||
};
|
||||
let branch = RefString::try_from(branch.clone())
|
||||
.map_err(|e| anyhow!("invalid branch name {:?}: {}", branch, e))?;
|
||||
let visibility = if let Some(v) = options.visibility {
|
||||
let visibility = if let Some(v) = visibility {
|
||||
v
|
||||
} else {
|
||||
// TODO(finto): this is interactive without checking `interactive` –
|
||||
// this should check if interactive and use the `private` if not
|
||||
let selected = term::select(
|
||||
"Visibility",
|
||||
&["public", "private"],
|
||||
|
|
@ -309,20 +157,20 @@ pub fn init(
|
|||
));
|
||||
spinner.finish();
|
||||
|
||||
if options.verbose {
|
||||
if args.verbose {
|
||||
term::blob(json::to_string_pretty(&proj)?);
|
||||
}
|
||||
// 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 {
|
||||
profile.seed(rid, options.scope, &mut node)?;
|
||||
if seed {
|
||||
profile.seed(rid, args.scope, &mut node)?;
|
||||
|
||||
if doc.is_public() {
|
||||
profile.add_inventory(rid, &mut node)?;
|
||||
}
|
||||
}
|
||||
|
||||
if options.set_upstream || git::branch_remote(&repo, proj.default_branch()).is_err() {
|
||||
if args.set_upstream || git::branch_remote(&repo, proj.default_branch()).is_err() {
|
||||
// Setup eg. `master` -> `rad/master`
|
||||
radicle::git::set_upstream(
|
||||
&repo,
|
||||
|
|
@ -334,7 +182,7 @@ pub fn init(
|
|||
push_cmd = format!("git push {} {branch}", *radicle::rad::REMOTE_NAME);
|
||||
}
|
||||
|
||||
if options.setup_signing {
|
||||
if args.setup_signing {
|
||||
// Setup radicle signing key.
|
||||
self::setup_signing(profile.id(), &repo, interactive)?;
|
||||
}
|
||||
|
|
@ -379,12 +227,13 @@ pub fn init(
|
|||
pub fn init_existing(
|
||||
working: git::Repository,
|
||||
rid: RepoId,
|
||||
options: Options,
|
||||
args: Args,
|
||||
profile: &profile::Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
let stored = profile.storage.repository(rid)?;
|
||||
let project = stored.project()?;
|
||||
let url = radicle::git::Url::from(rid);
|
||||
let interactive = args.interactive();
|
||||
|
||||
radicle::git::configure_repository(&working)?;
|
||||
radicle::git::configure_remote(
|
||||
|
|
@ -394,7 +243,7 @@ pub fn init_existing(
|
|||
&url.clone().with_namespace(profile.public_key),
|
||||
)?;
|
||||
|
||||
if options.set_upstream {
|
||||
if args.set_upstream {
|
||||
// Setup eg. `master` -> `rad/master`
|
||||
radicle::git::set_upstream(
|
||||
&working,
|
||||
|
|
@ -404,9 +253,9 @@ pub fn init_existing(
|
|||
)?;
|
||||
}
|
||||
|
||||
if options.setup_signing {
|
||||
if args.setup_signing {
|
||||
// Setup radicle signing key.
|
||||
self::setup_signing(profile.id(), &working, options.interactive)?;
|
||||
self::setup_signing(profile.id(), &working, interactive)?;
|
||||
}
|
||||
|
||||
term::success!(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use radicle::{
|
||||
identity::{project::ProjectName, Visibility},
|
||||
node::policy::Scope,
|
||||
prelude::RepoId,
|
||||
};
|
||||
use radicle_term::Interactive;
|
||||
|
||||
pub(crate) const ABOUT: &str = "Initialize a Radicle repository";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(about = ABOUT, disable_version_flag = true)]
|
||||
pub struct Args {
|
||||
/// Directory to be initialized
|
||||
pub(super) path: Option<PathBuf>,
|
||||
/// Name of the repository
|
||||
#[arg(long, value_name = "STRING")]
|
||||
pub(super) name: Option<ProjectName>,
|
||||
/// Description of the repository
|
||||
#[arg(long, value_name = "STRING")]
|
||||
pub(super) description: Option<String>,
|
||||
/// The default branch of the repository
|
||||
#[arg(long = "default-branch", value_name = "STRING")]
|
||||
pub(super) branch: Option<String>,
|
||||
/// Repository follow scope
|
||||
#[arg(
|
||||
long,
|
||||
default_value_t = Scope::All,
|
||||
value_name = "SCOPE",
|
||||
value_parser = ScopeParser,
|
||||
)]
|
||||
pub(super) scope: Scope,
|
||||
/// Set repository visibility to *private*
|
||||
#[arg(long, conflicts_with = "public")]
|
||||
private: bool,
|
||||
/// Set repository visibility to *public*
|
||||
#[arg(long, conflicts_with = "private")]
|
||||
public: bool,
|
||||
/// Setup repository as an existing Radicle repository
|
||||
///
|
||||
/// [example values: rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH, z3Tr6bC7ctEg2EHmLvknUr29mEDLH]
|
||||
#[arg(long, value_name = "RID")]
|
||||
pub(super) existing: Option<RepoId>,
|
||||
/// Setup the upstream of the default branch
|
||||
#[arg(short = 'u', long)]
|
||||
pub(super) set_upstream: bool,
|
||||
/// Setup the radicle key as a signing key for this repository
|
||||
#[arg(long)]
|
||||
pub(super) setup_signing: bool,
|
||||
/// Don't ask for confirmation during setup
|
||||
#[arg(long)]
|
||||
no_confirm: bool,
|
||||
/// Don't seed this repository after initializing it
|
||||
#[arg(long)]
|
||||
no_seed: bool,
|
||||
/// Verbose mode
|
||||
#[arg(short, long)]
|
||||
pub(super) verbose: bool,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub(super) fn interactive(&self) -> Interactive {
|
||||
if self.no_confirm {
|
||||
Interactive::No
|
||||
} else {
|
||||
Interactive::Yes
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn visibility(&self) -> Option<Visibility> {
|
||||
if self.private {
|
||||
debug_assert!(!self.public, "BUG: `private` and `public` should conflict");
|
||||
Some(Visibility::private([]))
|
||||
} else if self.public {
|
||||
Some(Visibility::Public)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn seed(&self) -> bool {
|
||||
!self.no_seed
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(finto): this is duplicated from `clone::args`. Consolidate these once
|
||||
// the `clap` migration has finished and we can organise the shared code.
|
||||
#[derive(Clone, Debug)]
|
||||
struct ScopeParser;
|
||||
|
||||
impl clap::builder::TypedValueParser for ScopeParser {
|
||||
type Value = Scope;
|
||||
|
||||
fn parse_ref(
|
||||
&self,
|
||||
cmd: &clap::Command,
|
||||
arg: Option<&clap::Arg>,
|
||||
value: &std::ffi::OsStr,
|
||||
) -> Result<Self::Value, clap::Error> {
|
||||
<Scope as std::str::FromStr>::from_str.parse_ref(cmd, arg, value)
|
||||
}
|
||||
|
||||
fn possible_values(
|
||||
&self,
|
||||
) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
|
||||
use clap::builder::PossibleValue;
|
||||
Some(Box::new(
|
||||
[PossibleValue::new("all"), PossibleValue::new("followed")].into_iter(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Args;
|
||||
use clap::error::ErrorKind;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn should_parse_rid_non_urn() {
|
||||
let args = Args::try_parse_from(["init", "--existing", "z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
|
||||
assert!(args.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_parse_rid_urn() {
|
||||
let args =
|
||||
Args::try_parse_from(["init", "--existing", "rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
|
||||
assert!(args.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_parse_rid_url() {
|
||||
let err =
|
||||
Args::try_parse_from(["init", "--existing", "rad://z3Tr6bC7ctEg2EHmLvknUr29mEDLH"])
|
||||
.unwrap_err();
|
||||
assert_eq!(err.kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ enum Commands {
|
|||
Clone(clone::Args),
|
||||
Debug(debug::Args),
|
||||
Fork(fork::Args),
|
||||
Init(init::Args),
|
||||
Issue(issue::Args),
|
||||
Path(path::Args),
|
||||
Publish(publish::Args),
|
||||
|
|
@ -231,7 +232,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
|
|||
term::run_command_args::<inbox::Options, _>(inbox::HELP, inbox::run, args.to_vec())
|
||||
}
|
||||
"init" => {
|
||||
term::run_command_args::<init::Options, _>(init::HELP, init::run, args.to_vec());
|
||||
if let Some(Commands::Init(args)) = CliArgs::parse().command {
|
||||
term::run_command_fn(init::run, args);
|
||||
}
|
||||
}
|
||||
"inspect" => {
|
||||
term::run_command_args::<inspect::Options, _>(
|
||||
|
|
|
|||
Loading…
Reference in New Issue