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(
94 long,
95 short,
96 default_value_t = 9,
97 value_name = "SECS",
98 conflicts_with = "inventory"
99 )]
100 timeout: u64,
101
102 rid: Option<RepoId>,
104
105 #[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 #[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 #[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 #[clap(alias = "s")]
164 Status {
165 rid: Option<RepoId>,
167 #[arg(long, value_name = "FIELD", value_enum, default_value_t)]
169 sort_by: SortBy,
170 },
171}
172
173#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
175pub(super) enum SortBy {
176 Nid,
178 Alias,
180 #[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
198pub(super) enum SyncMode {
201 Repo {
203 rid: Option<RepoId>,
205 settings: SyncSettings,
207 direction: SyncDirection,
209 },
210 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#[derive(Debug, PartialEq, Eq)]
238pub(super) enum SyncDirection {
239 Fetch,
241 Announce,
243 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}