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 many seconds to wait while synchronizing
93    #[arg(
94        long,
95        short,
96        default_value_t = 9,
97        value_name = "SECS",
98        conflicts_with = "inventory"
99    )]
100    timeout: u64,
101
102    /// The repository to perform the synchronizing for [default: cwd]
103    rid: Option<RepoId>,
104
105    /// Synchronize with a specific number of seeds
106    ///
107    /// The value must be greater than zero
108    #[arg(
109        long,
110        short,
111        value_name = "COUNT",
112        value_parser = replicas_non_zero,
113        conflicts_with = "inventory",
114        default_value_t = radicle::node::sync::DEFAULT_REPLICATION_FACTOR,
115    )]
116    replicas: usize,
117
118    /// Synchronize with an upper bound number of seeds
119    ///
120    /// The value must be greater than zero
121    #[arg(
122        long,
123        value_name = "COUNT",
124        value_parser = replicas_non_zero,
125        conflicts_with = "inventory",
126    )]
127    max_replicas: Option<usize>,
128
129    /// Enable announcing inventory [default: false]
130    ///
131    /// `--inventory` is a standalone mode and is not compatible with the other
132    /// options
133    ///
134    /// <RID> is ignored with `--inventory`
135    #[arg(long, short)]
136    inventory: bool,
137}
138
139impl SyncArgs {
140    fn direction(&self) -> SyncDirection {
141        match (self.fetch, self.announce) {
142            (true, true) | (false, false) => SyncDirection::Both,
143            (true, false) => SyncDirection::Fetch,
144            (false, true) => SyncDirection::Announce,
145        }
146    }
147
148    fn timeout(&self) -> time::Duration {
149        time::Duration::from_secs(self.timeout)
150    }
151
152    fn replication(&self) -> sync::ReplicationFactor {
153        match (self.replicas, self.max_replicas) {
154            (min, None) => sync::ReplicationFactor::must_reach(min),
155            (min, Some(max)) => sync::ReplicationFactor::range(min, max),
156        }
157    }
158}
159
160#[derive(Subcommand, Debug)]
161pub(super) enum Command {
162    /// Display the sync status of a repository
163    #[clap(alias = "s")]
164    Status {
165        /// The repository to display the status for [default: cwd]
166        rid: Option<RepoId>,
167        /// Sort the table by column
168        #[arg(long, value_name = "FIELD", value_enum, default_value_t)]
169        sort_by: SortBy,
170    },
171}
172
173/// Sort the status table by the provided field
174#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
175pub(super) enum SortBy {
176    /// The NID of the entry
177    Nid,
178    /// The alias of the entry
179    Alias,
180    /// The status of the entry
181    #[default]
182    Status,
183}
184
185impl FromStr for SortBy {
186    type Err = &'static str;
187
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        match s {
190            "nid" => Ok(Self::Nid),
191            "alias" => Ok(Self::Alias),
192            "status" => Ok(Self::Status),
193            _ => Err("invalid `--sort-by` field"),
194        }
195    }
196}
197
198/// Whether we are performing a fetch/announce of a repository or only
199/// announcing the node's inventory
200pub(super) enum SyncMode {
201    /// Fetch and/or announce a repositories references
202    Repo {
203        /// The repository being synchronized
204        rid: Option<RepoId>,
205        /// The settings for fetch/announce
206        settings: SyncSettings,
207        /// The direction of the synchronization
208        direction: SyncDirection,
209    },
210    /// Announce the node's inventory
211    Inventory,
212}
213
214impl From<SyncArgs> for SyncMode {
215    fn from(args: SyncArgs) -> Self {
216        if args.inventory {
217            Self::Inventory
218        } else {
219            assert!(!args.inventory);
220            let direction = args.direction();
221            let mut settings = SyncSettings::default()
222                .timeout(args.timeout())
223                .replicas(args.replication());
224            if !args.seeds.is_empty() {
225                settings.seeds = args.seeds.into_iter().collect();
226            }
227            Self::Repo {
228                rid: args.rid,
229                settings,
230                direction,
231            }
232        }
233    }
234}
235
236/// The direction of the [`SyncMode`]
237#[derive(Debug, PartialEq, Eq)]
238pub(super) enum SyncDirection {
239    /// Only fetching
240    Fetch,
241    /// Only announcing
242    Announce,
243    /// Both fetching and announcing
244    Both,
245}
246
247fn replicas_non_zero(s: &str) -> Result<usize, String> {
248    let r = usize::from_str(s).map_err(|_| format!("{s} is not a number"))?;
249    if r == 0 {
250        return Err(format!("{s} must be a value greater than zero"));
251    }
252    Ok(r)
253}