radicle_cli/commands/sync/
args.rs1use 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 #[arg(long)]
67 pub(super) debug: bool,
68
69 #[arg(long, short)]
71 pub(super) verbose: bool,
72}
73
74#[derive(Parser, Debug)]
75pub(super) struct SyncArgs {
76 #[arg(long, short, conflicts_with = "inventory")]
80 fetch: bool,
81
82 #[arg(long, short, conflicts_with = "inventory")]
86 announce: bool,
87
88 #[arg(
90 long = "seed",
91 value_name = "NID",
92 action = clap::ArgAction::Append,
93 conflicts_with = "inventory",
94 )]
95 seeds: Vec<NodeId>,
96
97 #[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 rid: Option<RepoId>,
111
112 #[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 #[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 #[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 #[clap(alias = "s")]
179 Status {
180 rid: Option<RepoId>,
182 #[arg(long, value_name = "FIELD", value_enum, default_value_t)]
184 sort_by: SortBy,
185 },
186}
187
188#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
190pub(super) enum SortBy {
191 Nid,
193 Alias,
195 #[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
213pub(super) enum SyncMode {
216 Repo {
218 rid: Option<RepoId>,
220 settings: SyncSettings,
222 direction: SyncDirection,
224 },
225 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#[derive(Debug, PartialEq, Eq)]
257pub(super) enum SyncDirection {
258 Fetch,
260 Announce,
262 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}