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};
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 #[arg(long)]
62 pub(super) debug: bool,
63
64 #[arg(long, short)]
66 pub(super) verbose: bool,
67}
68
69#[derive(Parser, Debug)]
70pub(super) struct SyncArgs {
71 #[arg(long, short, conflicts_with = "inventory")]
75 fetch: bool,
76
77 #[arg(long, short, conflicts_with = "inventory")]
81 announce: bool,
82
83 #[arg(
85 long = "seed",
86 value_name = "NID",
87 action = clap::ArgAction::Append,
88 conflicts_with = "inventory",
89 )]
90 seeds: Vec<NodeId>,
91
92 #[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 rid: Option<RepoId>,
106
107 #[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 #[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 #[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 #[clap(alias = "s")]
166 Status {
167 rid: Option<RepoId>,
169 #[arg(long, value_name = "FIELD", value_enum, default_value_t)]
171 sort_by: SortBy,
172 },
173}
174
175#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
177pub(super) enum SortBy {
178 Nid,
180 Alias,
182 #[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
200pub(super) enum SyncMode {
203 Repo {
205 rid: Option<RepoId>,
207 settings: SyncSettings,
209 direction: SyncDirection,
211 },
212 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#[derive(Debug, PartialEq, Eq)]
240pub(super) enum SyncDirection {
241 Fetch,
243 Announce,
245 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}