cli/clone: Use clap
This commit is contained in:
parent
7c89045ea0
commit
3992d519c3
|
|
@ -1,10 +1,7 @@
|
||||||
#![allow(clippy::or_fun_call)]
|
pub mod args;
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use radicle::issue::cache::Issues as _;
|
use radicle::issue::cache::Issues as _;
|
||||||
use radicle::patch::cache::Patches as _;
|
use radicle::patch::cache::Patches as _;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
@ -26,119 +23,12 @@ use crate::commands::sync;
|
||||||
use crate::node::SyncSettings;
|
use crate::node::SyncSettings;
|
||||||
use crate::project;
|
use crate::project;
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
|
||||||
use crate::terminal::Element as _;
|
use crate::terminal::Element as _;
|
||||||
|
|
||||||
pub const HELP: Help = Help {
|
pub use args::Args;
|
||||||
name: "clone",
|
pub(crate) use args::ABOUT;
|
||||||
description: "Clone a Radicle repository",
|
|
||||||
version: env!("RADICLE_VERSION"),
|
|
||||||
usage: r#"
|
|
||||||
Usage
|
|
||||||
|
|
||||||
rad clone <rid> [<directory>] [--scope <scope>] [<option>...]
|
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
|
|
||||||
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<()> {
|
|
||||||
let profile = ctx.profile()?;
|
let profile = ctx.profile()?;
|
||||||
let mut node = radicle::Node::new(profile.socket());
|
let mut node = radicle::Node::new(profile.socket());
|
||||||
|
|
||||||
|
|
@ -154,16 +44,16 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
doc,
|
doc,
|
||||||
project: proj,
|
project: proj,
|
||||||
} = clone(
|
} = clone(
|
||||||
options.id,
|
args.repo,
|
||||||
options.directory.clone(),
|
args.directory.clone(),
|
||||||
options.scope,
|
args.scope,
|
||||||
options.sync.with_profile(&profile),
|
SyncSettings::from(args.sync).with_profile(&profile),
|
||||||
&mut node,
|
&mut node,
|
||||||
&profile,
|
&profile,
|
||||||
options.bare,
|
args.bare,
|
||||||
)?
|
)?
|
||||||
.print_or_success()
|
.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
|
let delegates = doc
|
||||||
.delegates()
|
.delegates()
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -171,7 +61,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
.filter(|id| id != profile.id())
|
.filter(|id| id != profile.id())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let default_branch = proj.default_branch().clone();
|
let default_branch = proj.default_branch().clone();
|
||||||
let path = if !options.bare {
|
let path = if !args.bare {
|
||||||
working.workdir().unwrap()
|
working.workdir().unwrap()
|
||||||
} else {
|
} else {
|
||||||
working.path()
|
working.path()
|
||||||
|
|
@ -181,7 +71,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
radicle::git::configure_repository(&working)?;
|
radicle::git::configure_repository(&working)?;
|
||||||
checkout::setup_remotes(
|
checkout::setup_remotes(
|
||||||
project::SetupRemote {
|
project::SetupRemote {
|
||||||
rid: options.id,
|
rid: args.repo,
|
||||||
tracking: Some(default_branch),
|
tracking: Some(default_branch),
|
||||||
repo: &working,
|
repo: &working,
|
||||||
fetch: true,
|
fetch: true,
|
||||||
|
|
@ -211,7 +101,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
])]);
|
])]);
|
||||||
info.print();
|
info.print();
|
||||||
|
|
||||||
let location = options
|
let location = args
|
||||||
.directory
|
.directory
|
||||||
.map_or(proj.name().to_string(), |loc| loc.display().to_string());
|
.map_or(proj.name().to_string(), |loc| loc.display().to_string());
|
||||||
term::info!(
|
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,
|
about: crate::commands::block::ABOUT,
|
||||||
},
|
},
|
||||||
CommandItem::Lexopt(crate::commands::checkout::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::config::HELP),
|
||||||
CommandItem::Clap {
|
CommandItem::Clap {
|
||||||
name: "debug",
|
name: "debug",
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ struct CliArgs {
|
||||||
enum Commands {
|
enum Commands {
|
||||||
Block(block::Args),
|
Block(block::Args),
|
||||||
Clean(clean::Args),
|
Clean(clean::Args),
|
||||||
|
Clone(clone::Args),
|
||||||
Debug(debug::Args),
|
Debug(debug::Args),
|
||||||
Issue(issue::Args),
|
Issue(issue::Args),
|
||||||
Path(path::Args),
|
Path(path::Args),
|
||||||
|
|
@ -192,7 +193,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
"clone" => {
|
"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" => {
|
"cob" => {
|
||||||
term::run_command_args::<cob::Options, _>(cob::HELP, cob::run, args.to_vec());
|
term::run_command_args::<cob::Options, _>(cob::HELP, cob::run, args.to_vec());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue