Skip to main content

radicle_cli/commands/sync/
args.rs

1use std::str::FromStr;
2use std::time;
3
4use clap::{Parser, Subcommand, ValueEnum};
5
6use radicle::{
7    node::{sync, NodeId},
8    prelude::RepoId,
9    storage::refs,
10};
11
12use crate::common_args::{
13    SignedReferencesFeatureLevel, SignedReferencesFeatureLevelParser,
14    ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM,
15};
16use crate::node::SyncSettings;
17
18const ABOUT: &str = "Sync repositories to the network";
19
20const LONG_ABOUT: &str = r#"
21By default, the current repository is synchronized both ways.
22If an <RID> is specified, that repository is synced instead.
23
24The process begins by fetching changes from connected seeds,
25followed by announcing local refs to peers, thereby prompting
26them to fetch from us.
27
28When `--fetch` is specified, any number of seeds may be given
29using the `--seed` option, eg. `--seed <NID>@<ADDR>:<PORT>`.
30
31When `--replicas` is specified, the given replication factor will try
32to be matched. For example, `--replicas 5` will sync with 5 seeds.
33
34The synchronization process can be configured using `--replicas <MIN>` and
35`--replicas-max <MAX>`. If these options are used independently, then the
36replication factor is taken as the given `<MIN>`/`<MAX>` value. If the
37options are used together, then the replication factor has a minimum and
38maximum bound.
39
40For fetching, the synchronization process will be considered successful if
41at least `<MIN>` seeds were fetched from *or* all preferred seeds were
42fetched from. If `<MAX>` is specified then the process will continue and
43attempt to sync with `<MAX>` seeds.
44
45For reference announcing, the synchronization process will be considered
46successful if at least `<MIN>` seeds were pushed to *and* all preferred
47seeds were pushed to.
48
49When `--fetch` or `--announce` are specified on their own, this command
50will only fetch or announce.
51
52If `--inventory` is specified, the node's inventory is announced to
53the network. This mode does not take an `<RID>`.
54"#;
55
56#[derive(Parser, Debug)]
57#[clap(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
58pub struct Args {
59    #[clap(subcommand)]
60    pub(super) command: Option<Command>,
61
62    #[clap(flatten)]
63    pub(super) sync: SyncArgs,
64
65    /// Enable debug information when synchronizing
66    #[arg(long)]
67    pub(super) debug: bool,
68
69    /// Enable verbose information when synchronizing
70    #[arg(long, short)]
71    pub(super) verbose: bool,
72}
73
74#[derive(Parser, Debug)]
75pub(super) struct SyncArgs {
76    /// Enable fetching [default: true]
77    ///
78    /// Providing `--announce` without `--fetch` will disable fetching
79    #[arg(long, short, conflicts_with = "inventory")]
80    fetch: bool,
81
82    /// Enable announcing [default: true]
83    ///
84    /// Providing `--fetch` without `--announce` will disable announcing
85    #[arg(long, short, conflicts_with = "inventory")]
86    announce: bool,
87
88    /// Synchronize with the given node (may be specified multiple times)
89    #[arg(
90        long = "seed",
91        value_name = "NID",
92        action = clap::ArgAction::Append,
93        conflicts_with = "inventory",
94    )]
95    seeds: Vec<NodeId>,
96
97    /// How long to wait while synchronizing
98    ///
99    /// Valid arguments are for example "10s", "5min" or "2h 37min"
100    #[arg(
101        long,
102        short,
103        default_value = "9s",
104        value_parser = humantime::parse_duration,
105        conflicts_with = "inventory"
106    )]
107    timeout: std::time::Duration,
108
109    /// The repository to perform the synchronizing for [default: cwd]
110    rid: Option<RepoId>,
111
112    /// Synchronize with a specific number of seeds
113    ///
114    /// The value must be greater than zero
115    #[arg(
116        long,
117        short,
118        value_name = "COUNT",
119        value_parser = replicas_non_zero,
120        conflicts_with = "inventory",
121        default_value_t = radicle::node::sync::DEFAULT_REPLICATION_FACTOR,
122    )]
123    replicas: usize,
124
125    /// Synchronize with an upper bound number of seeds
126    ///
127    /// The value must be greater than zero
128    #[arg(
129        long,
130        value_name = "COUNT",
131        value_parser = replicas_non_zero,
132        conflicts_with = "inventory",
133    )]
134    max_replicas: Option<usize>,
135
136    /// Enable announcing inventory [default: false]
137    ///
138    /// `--inventory` is a standalone mode and is not compatible with the other
139    /// options
140    ///
141    /// <RID> is ignored with `--inventory`
142    #[arg(long, short)]
143    inventory: bool,
144
145    #[arg(
146        long,
147        requires = "fetch",
148        value_parser = SignedReferencesFeatureLevelParser,
149        help = ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM
150    )]
151    signed_refs_feature_level: Option<SignedReferencesFeatureLevel>,
152}
153
154impl SyncArgs {
155    fn direction(&self) -> SyncDirection {
156        match (self.fetch, self.announce) {
157            (true, true) | (false, false) => SyncDirection::Both,
158            (true, false) => SyncDirection::Fetch,
159            (false, true) => SyncDirection::Announce,
160        }
161    }
162
163    fn timeout(&self) -> time::Duration {
164        self.timeout
165    }
166
167    fn replication(&self) -> sync::ReplicationFactor {
168        match (self.replicas, self.max_replicas) {
169            (min, None) => sync::ReplicationFactor::must_reach(min),
170            (min, Some(max)) => sync::ReplicationFactor::range(min, max),
171        }
172    }
173}
174
175#[derive(Subcommand, Debug)]
176pub(super) enum Command {
177    /// Display the sync status of a repository
178    #[clap(alias = "s")]
179    Status {
180        /// The repository to display the status for [default: cwd]
181        rid: Option<RepoId>,
182        /// Sort the table by column
183        #[arg(long, value_name = "FIELD", value_enum, default_value_t)]
184        sort_by: SortBy,
185    },
186}
187
188/// Sort the status table by the provided field
189#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
190pub(super) enum SortBy {
191    /// The NID of the entry
192    Nid,
193    /// The alias of the entry
194    Alias,
195    /// The status of the entry
196    #[default]
197    Status,
198}
199
200impl FromStr for SortBy {
201    type Err = &'static str;
202
203    fn from_str(s: &str) -> Result<Self, Self::Err> {
204        match s {
205            "nid" => Ok(Self::Nid),
206            "alias" => Ok(Self::Alias),
207            "status" => Ok(Self::Status),
208            _ => Err("invalid `--sort-by` field"),
209        }
210    }
211}
212
213/// Whether we are performing a fetch/announce of a repository or only
214/// announcing the node's inventory
215pub(super) enum SyncMode {
216    /// Fetch and/or announce a repositories references
217    Repo {
218        /// The repository being synchronized
219        rid: Option<RepoId>,
220        /// The settings for fetch/announce
221        settings: SyncSettings,
222        /// The direction of the synchronization
223        direction: SyncDirection,
224    },
225    /// Announce the node's inventory
226    Inventory,
227}
228
229impl From<SyncArgs> for SyncMode {
230    fn from(args: SyncArgs) -> Self {
231        if args.inventory {
232            Self::Inventory
233        } else {
234            assert!(!args.inventory);
235            let direction = args.direction();
236            let timeout = args.timeout();
237            let replicas = args.replication();
238            let feature_level = args.signed_refs_feature_level.map(refs::FeatureLevel::from);
239            let mut settings = SyncSettings::default()
240                .timeout(timeout)
241                .replicas(replicas)
242                .minimum_feature_level(feature_level);
243            if !args.seeds.is_empty() {
244                settings.seeds = args.seeds.into_iter().collect();
245            }
246            Self::Repo {
247                rid: args.rid,
248                settings,
249                direction,
250            }
251        }
252    }
253}
254
255/// The direction of the [`SyncMode`]
256#[derive(Debug, PartialEq, Eq)]
257pub(super) enum SyncDirection {
258    /// Only fetching
259    Fetch,
260    /// Only announcing
261    Announce,
262    /// Both fetching and announcing
263    Both,
264}
265
266fn replicas_non_zero(s: &str) -> Result<usize, String> {
267    let r = usize::from_str(s).map_err(|_| format!("{s} is not a number"))?;
268    if r == 0 {
269        return Err(format!("{s} must be a value greater than zero"));
270    }
271    Ok(r)
272}