Skip to main content

radicle_cli/commands/clone/
args.rs

1use std::path::PathBuf;
2
3use clap::Parser;
4
5use radicle::identity::doc::RepoId;
6use radicle::identity::IdError;
7use radicle::node::policy::Scope;
8use radicle::prelude::*;
9use radicle::storage::refs;
10
11use crate::common_args::{
12    SignedReferencesFeatureLevel, SignedReferencesFeatureLevelParser,
13    ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM,
14};
15use crate::node::SyncSettings;
16use crate::terminal;
17
18const ABOUT: &str = "Clone a Radicle repository";
19
20const LONG_ABOUT: &str = r#"
21The `clone` command will use your local node's routing table to find seeds from
22which it can clone the repository.
23
24For private repositories, use the `--seed` options, to clone directly
25from known seeds in the privacy set."#;
26
27/// Parse an RID, optionally stripping "rad://" prefix.
28fn parse_rid(value: &str) -> Result<RepoId, IdError> {
29    value.strip_prefix("rad://").unwrap_or(value).parse()
30}
31
32#[derive(Debug, Parser)]
33pub(super) struct SyncArgs {
34    /// Clone from this seed (may be specified multiple times)
35    #[arg(short, long = "seed", value_name = "NID", action = clap::ArgAction::Append)]
36    seeds: Vec<NodeId>,
37
38    /// Timeout for fetching repository
39    ///
40    /// Valid arguments are for example "10s", "5min" or "2h 37min"
41    #[arg(long, value_parser = humantime::parse_duration, default_value = "9s")]
42    timeout: std::time::Duration,
43
44    #[arg(
45        long,
46        value_parser = SignedReferencesFeatureLevelParser,
47        help = ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM
48    )]
49    signed_refs_feature_level: Option<SignedReferencesFeatureLevel>,
50}
51
52impl From<SyncArgs> for SyncSettings {
53    fn from(args: SyncArgs) -> Self {
54        SyncSettings {
55            timeout: args.timeout,
56            seeds: args.seeds.into_iter().collect(),
57            signed_references_minimum_feature_level: args
58                .signed_refs_feature_level
59                .map(refs::FeatureLevel::from),
60            ..SyncSettings::default()
61        }
62    }
63}
64
65#[derive(Debug, Parser)]
66#[clap(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
67pub struct Args {
68    /// ID of the repository to clone
69    ///
70    /// [example values: rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH, rad://z3Tr6bC7ctEg2EHmLvknUr29mEDLH]
71    #[arg(value_name = "RID", value_parser = parse_rid)]
72    pub(super) repo: RepoId,
73
74    /// The target directory for the repository to be cloned into
75    #[arg(value_name = "PATH")]
76    pub(super) directory: Option<PathBuf>,
77
78    /// Follow scope
79    #[arg(
80        long,
81        value_parser = terminal::args::ScopeParser
82    )]
83    pub(super) scope: Option<Scope>,
84
85    #[clap(flatten)]
86    pub(super) sync: SyncArgs,
87
88    /// Make a bare repository
89    #[arg(long)]
90    pub(super) bare: bool,
91
92    // We keep this flag here for consistency though it doesn't have any effect,
93    // since the command is fully non-interactive.
94    #[arg(long, hide = true)]
95    pub(super) no_confirm: bool,
96}
97
98#[cfg(test)]
99mod test {
100    use super::Args;
101    use clap::Parser;
102
103    #[test]
104    fn should_parse_rid_non_urn() {
105        let args = Args::try_parse_from(["clone", "z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
106        assert!(args.is_ok())
107    }
108
109    #[test]
110    fn should_parse_rid_urn() {
111        let args = Args::try_parse_from(["clone", "rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
112        assert!(args.is_ok())
113    }
114
115    #[test]
116    fn should_parse_rid_url() {
117        let args = Args::try_parse_from(["clone", "rad://z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
118        assert!(args.is_ok())
119    }
120}