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