cli/clone: Use clap
This commit is contained in:
parent
7c89045ea0
commit
3992d519c3
|
|
@ -1,10 +1,7 @@
|
|||
#![allow(clippy::or_fun_call)]
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time;
|
||||
pub mod args;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use thiserror::Error;
|
||||
|
|
@ -26,119 +23,12 @@ use crate::commands::sync;
|
|||
use crate::node::SyncSettings;
|
||||
use crate::project;
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
use crate::terminal::Element as _;
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "clone",
|
||||
description: "Clone a Radicle repository",
|
||||
version: env!("RADICLE_VERSION"),
|
||||
usage: r#"
|
||||
Usage
|
||||
pub use args::Args;
|
||||
pub(crate) use args::ABOUT;
|
||||
|
||||
rad clone <rid> [<directory>] [--scope <scope>] [<option>...]
|
||||
|
||||
The `clone` command will use your local node's routing table to find seeds from
|
||||
which it can clone the repository.
|
||||
|
||||
For private repositories, use the `--seed` options, to clone directly
|
||||
from known seeds in the privacy set.
|
||||
|
||||
Options
|
||||
|
||||
--bare Make a bare repository
|
||||
--scope <scope> Follow scope: `followed` or `all` (default: all)
|
||||
-s, --seed <nid> Clone from this seed (may be specified multiple times)
|
||||
--timeout <secs> Timeout for fetching repository (default: 9)
|
||||
--help Print help
|
||||
|
||||
"#,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
/// The RID of the repository.
|
||||
id: RepoId,
|
||||
/// The target directory for the repository to be cloned into.
|
||||
directory: Option<PathBuf>,
|
||||
/// The seeding scope of the repository.
|
||||
scope: Scope,
|
||||
/// Sync settings.
|
||||
sync: SyncSettings,
|
||||
bare: 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 id: Option<RepoId> = None;
|
||||
let mut scope = Scope::All;
|
||||
let mut sync = SyncSettings::default();
|
||||
let mut directory = None;
|
||||
let mut bare = false;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("seed") | Short('s') => {
|
||||
let value = parser.value()?;
|
||||
let value = term::args::nid(&value)?;
|
||||
|
||||
sync.seeds.insert(value);
|
||||
}
|
||||
Long("scope") => {
|
||||
let value = parser.value()?;
|
||||
|
||||
scope = term::args::parse_value("scope", value)?;
|
||||
}
|
||||
Long("timeout") => {
|
||||
let value = parser.value()?;
|
||||
let secs = term::args::number(&value)?;
|
||||
|
||||
sync.timeout = time::Duration::from_secs(secs as u64);
|
||||
}
|
||||
Long("no-confirm") => {
|
||||
// We keep this flag here for consistency though it doesn't have any effect,
|
||||
// since the command is fully non-interactive.
|
||||
}
|
||||
Long("bare") => {
|
||||
bare = true;
|
||||
}
|
||||
Long("help") | Short('h') => {
|
||||
return Err(Error::Help.into());
|
||||
}
|
||||
Value(val) if id.is_none() => {
|
||||
let val = val.to_string_lossy();
|
||||
let val = val.strip_prefix("rad://").unwrap_or(&val);
|
||||
let val = RepoId::from_str(val)?;
|
||||
|
||||
id = Some(val);
|
||||
}
|
||||
// Parse <directory> once <rid> has been parsed
|
||||
Value(val) if id.is_some() && directory.is_none() => {
|
||||
directory = Some(Path::new(&val).to_path_buf());
|
||||
}
|
||||
_ => return Err(anyhow!(arg.unexpected())),
|
||||
}
|
||||
}
|
||||
let id =
|
||||
id.ok_or_else(|| anyhow!("to clone, an RID must be provided; see `rad clone --help`"))?;
|
||||
|
||||
Ok((
|
||||
Options {
|
||||
id,
|
||||
directory,
|
||||
scope,
|
||||
sync,
|
||||
bare,
|
||||
},
|
||||
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 mut node = radicle::Node::new(profile.socket());
|
||||
|
||||
|
|
@ -154,16 +44,16 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
doc,
|
||||
project: proj,
|
||||
} = clone(
|
||||
options.id,
|
||||
options.directory.clone(),
|
||||
options.scope,
|
||||
options.sync.with_profile(&profile),
|
||||
args.repo,
|
||||
args.directory.clone(),
|
||||
args.scope,
|
||||
SyncSettings::from(args.sync).with_profile(&profile),
|
||||
&mut node,
|
||||
&profile,
|
||||
options.bare,
|
||||
args.bare,
|
||||
)?
|
||||
.print_or_success()
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to clone {}", options.id))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to clone {}", args.repo))?;
|
||||
let delegates = doc
|
||||
.delegates()
|
||||
.iter()
|
||||
|
|
@ -171,7 +61,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
.filter(|id| id != profile.id())
|
||||
.collect::<Vec<_>>();
|
||||
let default_branch = proj.default_branch().clone();
|
||||
let path = if !options.bare {
|
||||
let path = if !args.bare {
|
||||
working.workdir().unwrap()
|
||||
} else {
|
||||
working.path()
|
||||
|
|
@ -181,7 +71,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
radicle::git::configure_repository(&working)?;
|
||||
checkout::setup_remotes(
|
||||
project::SetupRemote {
|
||||
rid: options.id,
|
||||
rid: args.repo,
|
||||
tracking: Some(default_branch),
|
||||
repo: &working,
|
||||
fetch: true,
|
||||
|
|
@ -211,7 +101,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
])]);
|
||||
info.print();
|
||||
|
||||
let location = options
|
||||
let location = args
|
||||
.directory
|
||||
.map_or(proj.name().to_string(), |loc| loc.display().to_string());
|
||||
term::info!(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
use std::path::PathBuf;
|
||||
use std::time;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::node::SyncSettings;
|
||||
use radicle::identity::doc::RepoId;
|
||||
use radicle::identity::IdError;
|
||||
use radicle::node::policy::Scope;
|
||||
use radicle::prelude::*;
|
||||
|
||||
pub(crate) const ABOUT: &str = "Clone a Radicle repository";
|
||||
|
||||
const LONG_ABOUT: &str = r#"
|
||||
The `clone` command will use your local node's routing table to find seeds from
|
||||
which it can clone the repository.
|
||||
|
||||
For private repositories, use the `--seed` options, to clone directly
|
||||
from known seeds in the privacy set."#;
|
||||
|
||||
/// Parse an RID, optionally stripping "rad://" prefix.
|
||||
fn parse_rid(value: &str) -> Result<RepoId, IdError> {
|
||||
value.strip_prefix("rad://").unwrap_or(value).parse()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub(super) struct SyncArgs {
|
||||
/// Clone from this seed (may be specified multiple times)
|
||||
#[arg(short, long = "seed", value_name = "NID", action = clap::ArgAction::Append)]
|
||||
seeds: Vec<NodeId>,
|
||||
|
||||
/// Timeout for fetching repository in seconds
|
||||
#[arg(long, default_value_t = 9, value_name = "SECS")]
|
||||
timeout: usize,
|
||||
}
|
||||
|
||||
impl From<SyncArgs> for SyncSettings {
|
||||
fn from(args: SyncArgs) -> Self {
|
||||
SyncSettings {
|
||||
timeout: time::Duration::from_secs(args.timeout as u64),
|
||||
seeds: args.seeds.into_iter().collect(),
|
||||
..SyncSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
|
||||
pub struct Args {
|
||||
/// ID of the repository to clone
|
||||
///
|
||||
/// [example values: rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH, rad://z3Tr6bC7ctEg2EHmLvknUr29mEDLH]
|
||||
#[arg(value_name = "RID", value_parser = parse_rid)]
|
||||
pub(super) repo: RepoId,
|
||||
|
||||
/// The target directory for the repository to be cloned into
|
||||
#[arg(value_name = "PATH")]
|
||||
pub(super) directory: Option<PathBuf>,
|
||||
|
||||
/// Follow scope
|
||||
#[arg(long, default_value_t = Scope::All, value_name = "SCOPE", value_parser = ScopeParser)]
|
||||
pub(super) scope: Scope,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(super) sync: SyncArgs,
|
||||
|
||||
/// Make a bare repository
|
||||
#[arg(long)]
|
||||
pub(super) bare: bool,
|
||||
|
||||
// We keep this flag here for consistency though it doesn't have any effect,
|
||||
// since the command is fully non-interactive.
|
||||
#[arg(long, hide = true)]
|
||||
pub(super) no_confirm: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Args;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn should_parse_rid_non_urn() {
|
||||
let args = Args::try_parse_from(["clone", "z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
|
||||
assert!(args.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_parse_rid_urn() {
|
||||
let args = Args::try_parse_from(["clone", "rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
|
||||
assert!(args.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_parse_rid_url() {
|
||||
let args = Args::try_parse_from(["clone", "rad://z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
|
||||
assert!(args.is_ok())
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,10 @@ const COMMANDS: &[CommandItem] = &[
|
|||
about: crate::commands::block::ABOUT,
|
||||
},
|
||||
CommandItem::Lexopt(crate::commands::checkout::HELP),
|
||||
CommandItem::Lexopt(crate::commands::clone::HELP),
|
||||
CommandItem::Clap {
|
||||
name: "clone",
|
||||
about: crate::commands::clone::ABOUT,
|
||||
},
|
||||
CommandItem::Lexopt(crate::commands::config::HELP),
|
||||
CommandItem::Clap {
|
||||
name: "debug",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ struct CliArgs {
|
|||
enum Commands {
|
||||
Block(block::Args),
|
||||
Clean(clean::Args),
|
||||
Clone(clone::Args),
|
||||
Debug(debug::Args),
|
||||
Issue(issue::Args),
|
||||
Path(path::Args),
|
||||
|
|
@ -192,7 +193,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
|
|||
);
|
||||
}
|
||||
"clone" => {
|
||||
term::run_command_args::<clone::Options, _>(clone::HELP, clone::run, args.to_vec());
|
||||
if let Some(Commands::Clone(args)) = CliArgs::parse().command {
|
||||
term::run_command_fn(clone::run, args);
|
||||
}
|
||||
}
|
||||
"cob" => {
|
||||
term::run_command_args::<cob::Options, _>(cob::HELP, cob::run, args.to_vec());
|
||||
|
|
|
|||
Loading…
Reference in New Issue