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::{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    /// Enable debug information when synchronizing
66    #[arg(long)]
67    pub(super) debug: bool,
68
69    /// Enable verbose information when synchronizing
70    #[arg(long, short)]
71    pub(super) verbose: bool,
72}
73
74#[derive(Parser, Debug)]
75pub(super) struct SyncArgs {
76    /// Enable fetching [default: true]
77    ///
78    /// Providing `--announce` without `--fetch` will disable fetching
79    #[arg(long, short, conflicts_with = "inventory")]
80    fetch: bool,
81
82    /// Enable announcing [default: true]
83    ///
84    /// Providing `--fetch` without `--announce` will disable announcing
85    #[arg(long, short, conflicts_with = "inventory")]
86    announce: bool,
87
88    /// Synchronize with the given node (may be specified multiple times)
89    #[arg(
90        long = "seed",
91        value_name = "NID",
92        action = clap::ArgAction::Append,
93        conflicts_with = "inventory",
94    )]
95    seeds: Vec<NodeId>,
96
97    /// How long to wait while synchronizing
98    ///
99    /// Valid arguments are for example "10s", "5min" or "2h 37min"
100    #[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    /// The repository to perform the synchronizing for [default: cwd]
110    #[arg(value_name = "RID")]
111    repo: Option<RepoId>,
112
113    /// Synchronize with a specific number of seeds
114    ///
115    /// The value must be greater than zero
116    #[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    /// Synchronize with an upper bound number of seeds
127    ///
128    /// The value must be greater than zero
129    #[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    /// Enable announcing inventory [default: false]
138    ///
139    /// `--inventory` is a standalone mode and is not compatible with the other
140    /// options
141    ///
142    /// <RID> is ignored with `--inventory`
143    #[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    /// Display the sync status of a repository
179    #[clap(alias = "s")]
180    Status {
181        /// The repository to display the status for [default: cwd]
182        #[arg(value_name = "RID")]
183        repo: Option<RepoId>,
184        /// Sort the table by column
185        #[arg(long, value_name = "FIELD", value_enum, default_value_t)]
186        sort_by: SortBy,
187    },
188}
189
190/// Sort the status table by the provided field
191#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
192pub(super) enum SortBy {
193    /// The NID of the entry
194    Nid,
195    /// The alias of the entry
196    Alias,
197    /// The status of the entry
198    #[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
215/// Whether we are performing a fetch/announce of a repository or only
216/// announcing the node's inventory
217pub(super) enum SyncMode {
218    /// Fetch and/or announce a repositories references
219    Repo {
220        /// The repository being synchronized
221        repo: Option<RepoId>,
222        /// The settings for fetch/announce
223        settings: SyncSettings,
224        /// The direction of the synchronization
225        direction: SyncDirection,
226    },
227    /// Announce the node's inventory
228    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/// The direction of the [`SyncMode`]
258#[derive(Debug, PartialEq, Eq)]
259pub(super) enum SyncDirection {
260    /// Only fetching
261    Fetch,
262    /// Only announcing
263    Announce,
264    /// Both fetching and announcing
265    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}