use std::str::FromStr;
use std::time;
use clap::{Parser, Subcommand, ValueEnum};
use radicle::{
node::{sync, NodeId},
prelude::RepoId,
storage::refs,
};
use crate::common_args::{
SignedReferencesFeatureLevel, SignedReferencesFeatureLevelParser,
ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM,
};
use crate::node::SyncSettings;
const ABOUT: &str = "Sync repositories to the network";
const LONG_ABOUT: &str = r#"
By default, the current repository is synchronized both ways.
If an <RID> is specified, that repository is synced instead.
The process begins by fetching changes from connected seeds,
followed by announcing local refs to peers, thereby prompting
them to fetch from us.
When `--fetch` is specified, any number of seeds may be given
using the `--seed` option, eg. `--seed <NID>@<ADDR>:<PORT>`.
When `--replicas` is specified, the given replication factor will try
to be matched. For example, `--replicas 5` will sync with 5 seeds.
The synchronization process can be configured using `--replicas <MIN>` and
`--replicas-max <MAX>`. If these options are used independently, then the
replication factor is taken as the given `<MIN>`/`<MAX>` value. If the
options are used together, then the replication factor has a minimum and
maximum bound.
For fetching, the synchronization process will be considered successful if
at least `<MIN>` seeds were fetched from *or* all preferred seeds were
fetched from. If `<MAX>` is specified then the process will continue and
attempt to sync with `<MAX>` seeds.
For reference announcing, the synchronization process will be considered
successful if at least `<MIN>` seeds were pushed to *and* all preferred
seeds were pushed to.
When `--fetch` or `--announce` are specified on their own, this command
will only fetch or announce.
If `--inventory` is specified, the node's inventory is announced to
the network. This mode does not take an `<RID>`.
"#;
#[derive(Parser, Debug)]
#[clap(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
pub struct Args {
#[clap(subcommand)]
pub(super) command: Option<Command>,
#[clap(flatten)]
pub(super) sync: SyncArgs,
#[arg(long)]
pub(super) debug: bool,
#[arg(long, short)]
pub(super) verbose: bool,
}
#[derive(Parser, Debug)]
pub(super) struct SyncArgs {
#[arg(long, short, conflicts_with = "inventory")]
fetch: bool,
#[arg(long, short, conflicts_with = "inventory")]
announce: bool,
#[arg(
long = "seed",
value_name = "NID",
action = clap::ArgAction::Append,
conflicts_with = "inventory",
)]
seeds: Vec<NodeId>,
#[arg(
long,
short,
default_value = "9s",
value_parser = humantime::parse_duration,
conflicts_with = "inventory"
)]
timeout: std::time::Duration,
rid: Option<RepoId>,
#[arg(
long,
short,
value_name = "COUNT",
value_parser = replicas_non_zero,
conflicts_with = "inventory",
default_value_t = radicle::node::sync::DEFAULT_REPLICATION_FACTOR,
)]
replicas: usize,
#[arg(
long,
value_name = "COUNT",
value_parser = replicas_non_zero,
conflicts_with = "inventory",
)]
max_replicas: Option<usize>,
#[arg(long, short)]
inventory: bool,
#[arg(
long,
requires = "fetch",
value_parser = SignedReferencesFeatureLevelParser,
help = ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM
)]
signed_refs_feature_level: Option<SignedReferencesFeatureLevel>,
}
impl SyncArgs {
fn direction(&self) -> SyncDirection {
match (self.fetch, self.announce) {
(true, true) | (false, false) => SyncDirection::Both,
(true, false) => SyncDirection::Fetch,
(false, true) => SyncDirection::Announce,
}
}
fn timeout(&self) -> time::Duration {
self.timeout
}
fn replication(&self) -> sync::ReplicationFactor {
match (self.replicas, self.max_replicas) {
(min, None) => sync::ReplicationFactor::must_reach(min),
(min, Some(max)) => sync::ReplicationFactor::range(min, max),
}
}
}
#[derive(Subcommand, Debug)]
pub(super) enum Command {
#[clap(alias = "s")]
Status {
rid: Option<RepoId>,
#[arg(long, value_name = "FIELD", value_enum, default_value_t)]
sort_by: SortBy,
},
}
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum SortBy {
Nid,
Alias,
#[default]
Status,
}
impl FromStr for SortBy {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"nid" => Ok(Self::Nid),
"alias" => Ok(Self::Alias),
"status" => Ok(Self::Status),
_ => Err("invalid `--sort-by` field"),
}
}
}
pub(super) enum SyncMode {
Repo {
rid: Option<RepoId>,
settings: SyncSettings,
direction: SyncDirection,
},
Inventory,
}
impl From<SyncArgs> for SyncMode {
fn from(args: SyncArgs) -> Self {
if args.inventory {
Self::Inventory
} else {
assert!(!args.inventory);
let direction = args.direction();
let timeout = args.timeout();
let replicas = args.replication();
let feature_level = args.signed_refs_feature_level.map(refs::FeatureLevel::from);
let mut settings = SyncSettings::default()
.timeout(timeout)
.replicas(replicas)
.minimum_feature_level(feature_level);
if !args.seeds.is_empty() {
settings.seeds = args.seeds.into_iter().collect();
}
Self::Repo {
rid: args.rid,
settings,
direction,
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum SyncDirection {
Fetch,
Announce,
Both,
}
fn replicas_non_zero(s: &str) -> Result<usize, String> {
let r = usize::from_str(s).map_err(|_| format!("{s} is not a number"))?;
if r == 0 {
return Err(format!("{s} must be a value greater than zero"));
}
Ok(r)
}