radicle_cli/commands/sync/
args.rs1use std::str::FromStr;
2use std::time;
3
4use clap::{Parser, Subcommand, ValueEnum};
5
6use radicle::{
7 node::{NodeId, sync},
8 prelude::RepoId,
9 storage::refs,
10};
11
12use crate::common_args::{
13 ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM, SignedReferencesFeatureLevel,
14 SignedReferencesFeatureLevelParser,
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 #[arg(value_name = "RID")]
111 repo: Option<RepoId>,
112
113 #[arg(
117 long,
118 short,
119 value_name = "COUNT",
120 value_parser = replicas_non_zero,
121 conflicts_with = "inventory",
122 default_value_t = radicle::node::sync::DEFAULT_REPLICATION_FACTOR,
123 )]
124 replicas: usize,
125
126 #[arg(
130 long,
131 value_name = "COUNT",
132 value_parser = replicas_non_zero,
133 conflicts_with = "inventory",
134 )]
135 max_replicas: Option<usize>,
136
137 #[arg(long, short)]
144 inventory: bool,
145
146 #[arg(
147 long,
148 requires = "fetch",
149 value_parser = SignedReferencesFeatureLevelParser,
150 help = ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM
151 )]
152 signed_refs_feature_level: Option<SignedReferencesFeatureLevel>,
153}
154
155impl SyncArgs {
156 fn direction(&self) -> SyncDirection {
157 match (self.fetch, self.announce) {
158 (true, true) | (false, false) => SyncDirection::Both,
159 (true, false) => SyncDirection::Fetch,
160 (false, true) => SyncDirection::Announce,
161 }
162 }
163
164 fn timeout(&self) -> time::Duration {
165 self.timeout
166 }
167
168 fn replication(&self) -> sync::ReplicationFactor {
169 match (self.replicas, self.max_replicas) {
170 (min, None) => sync::ReplicationFactor::must_reach(min),
171 (min, Some(max)) => sync::ReplicationFactor::range(min, max),
172 }
173 }
174}
175
176#[derive(Subcommand, Debug)]
177pub(super) enum Command {
178 #[clap(alias = "s")]
180 Status {
181 #[arg(value_name = "RID")]
183 repo: Option<RepoId>,
184 #[arg(long, value_name = "FIELD", value_enum, default_value_t)]
186 sort_by: SortBy,
187 },
188}
189
190#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
192pub(super) enum SortBy {
193 Nid,
195 Alias,
197 #[default]
199 Status,
200}
201
202impl FromStr for SortBy {
203 type Err = &'static str;
204
205 fn from_str(s: &str) -> Result<Self, Self::Err> {
206 match s {
207 "nid" => Ok(Self::Nid),
208 "alias" => Ok(Self::Alias),
209 "status" => Ok(Self::Status),
210 _ => Err("invalid `--sort-by` field"),
211 }
212 }
213}
214
215pub(super) enum SyncMode {
218 Repo {
220 repo: Option<RepoId>,
222 settings: SyncSettings,
224 direction: SyncDirection,
226 },
227 Inventory,
229}
230
231impl From<SyncArgs> for SyncMode {
232 fn from(args: SyncArgs) -> Self {
233 if args.inventory {
234 Self::Inventory
235 } else {
236 assert!(!args.inventory);
237 let direction = args.direction();
238 let timeout = args.timeout();
239 let replicas = args.replication();
240 let feature_level = args.signed_refs_feature_level.map(refs::FeatureLevel::from);
241 let mut settings = SyncSettings::default()
242 .timeout(timeout)
243 .replicas(replicas)
244 .minimum_feature_level(feature_level);
245 if !args.seeds.is_empty() {
246 settings.seeds = args.seeds.into_iter().collect();
247 }
248 Self::Repo {
249 repo: args.repo,
250 settings,
251 direction,
252 }
253 }
254 }
255}
256
257#[derive(Debug, PartialEq, Eq)]
259pub(super) enum SyncDirection {
260 Fetch,
262 Announce,
264 Both,
266}
267
268fn replicas_non_zero(s: &str) -> Result<usize, String> {
269 let r = usize::from_str(s).map_err(|_| format!("{s} is not a number"))?;
270 if r == 0 {
271 return Err(format!("{s} must be a value greater than zero"));
272 }
273 Ok(r)
274}