Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli/clone: Use clap
Merged lorenz opened 7 months ago

See issue/7fb03f234030b91c38cc4f5b48bd30cf5fd6a1de.

4 files changed +121 -126 7c89045e 3992d519
modified crates/radicle-cli/src/commands/clone.rs
@@ -1,10 +1,7 @@
-
#![allow(clippy::or_fun_call)]
-
use std::ffi::OsString;
+
pub mod args;
+

use std::path::{Path, PathBuf};
-
use std::str::FromStr;
-
use std::time;

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

-
    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 use args::Args;
+
pub(crate) use args::ABOUT;

-
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!(
added crates/radicle-cli/src/commands/clone/args.rs
@@ -0,0 +1,99 @@
+
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> {
+
    use std::str::FromStr as _;
+
    RepoId::from_str(value.strip_prefix("rad://").unwrap_or(value))
+
}
+

+
#[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
+
    #[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,
+
}
modified crates/radicle-cli/src/commands/help.rs
@@ -40,7 +40,10 @@ const COMMANDS: &[CommandItem] = &[
    CommandItem::Lexopt(crate::commands::auth::HELP),
    CommandItem::Lexopt(crate::commands::block::HELP),
    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::Lexopt(crate::commands::fork::HELP),
    CommandItem::Lexopt(crate::commands::help::HELP),
modified crates/radicle-cli/src/main.rs
@@ -46,6 +46,7 @@ struct CliArgs {
#[derive(Subcommand, Debug)]
enum Commands {
    Clean(clean::Args),
+
    Clone(clone::Args),
    Issue(issue::Args),
    Path(path::Args),
    Stats(stats::Args),
@@ -188,7 +189,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());