cli/seed: Use clap

This commit is contained in:
Erik Kundt 2025-10-15 01:04:19 +02:00
parent 80198d68b6
commit d9ae29deaa
4 changed files with 124 additions and 140 deletions

View File

@ -90,7 +90,10 @@ const COMMANDS: &[CommandItem] = &[
about: crate::commands::clean::ABOUT, about: crate::commands::clean::ABOUT,
}, },
CommandItem::Lexopt(crate::commands::rad_self::HELP), CommandItem::Lexopt(crate::commands::rad_self::HELP),
CommandItem::Lexopt(crate::commands::seed::HELP), CommandItem::Clap {
name: "seed",
about: crate::commands::seed::ABOUT,
},
CommandItem::Lexopt(crate::commands::follow::HELP), CommandItem::Lexopt(crate::commands::follow::HELP),
CommandItem::Lexopt(crate::commands::unblock::HELP), CommandItem::Lexopt(crate::commands::unblock::HELP),
CommandItem::Clap { CommandItem::Clap {

View File

@ -1,10 +1,5 @@
use std::collections::BTreeSet; mod args;
use std::ffi::OsString;
use std::time;
use anyhow::anyhow;
use nonempty::NonEmpty;
use radicle::node::policy; use radicle::node::policy;
use radicle::node::policy::{Policy, Scope}; use radicle::node::policy::{Policy, Scope};
use radicle::node::Handle; use radicle::node::Handle;
@ -12,155 +7,34 @@ use radicle::{prelude::*, Node};
use radicle_term::Element as _; use radicle_term::Element as _;
use crate::commands::sync; use crate::commands::sync;
use crate::node::SyncSettings;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help { pub use args::Args;
name: "seed", pub(crate) use args::ABOUT;
description: "Manage repository seeding policies",
version: env!("RADICLE_VERSION"),
usage: r#"
Usage
rad seed [<rid>...] [--[no-]fetch] [--from <nid>] [--scope <scope>] [<option>...] pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
The `seed` command, when no Repository ID (<rid>) is provided, will list the
repositories being seeded.
When a Repository ID (<rid>) is provided it updates or creates the seeding policy for
that repository. To delete a seeding policy, use the `rad unseed` command.
When seeding a repository, a scope can be specified: this can be either `all` or
`followed`. When using `all`, all remote nodes will be followed for that repository.
On the other hand, with `followed`, only the repository delegates will be followed,
plus any remote that is explicitly followed via `rad follow <nid>`.
Options
--[no-]fetch Fetch repository after updating seeding policy
--from <nid> Fetch from the given node (may be specified multiple times)
--timeout <secs> Fetch timeout in seconds (default: 9)
--scope <scope> Peer follow scope for this repository
--verbose, -v Verbose output
--help Print help
"#,
};
#[derive(Debug)]
pub enum Operation {
Seed {
rids: NonEmpty<RepoId>,
fetch: bool,
seeds: BTreeSet<NodeId>,
timeout: time::Duration,
scope: Scope,
},
List,
}
#[derive(Debug)]
pub struct Options {
pub op: Operation,
pub verbose: bool,
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args);
let mut rids: Vec<RepoId> = Vec::new();
let mut scope: Option<Scope> = None;
let mut fetch: Option<bool> = None;
let mut timeout = time::Duration::from_secs(9);
let mut seeds: BTreeSet<NodeId> = BTreeSet::new();
let mut verbose = false;
while let Some(arg) = parser.next()? {
match &arg {
Value(val) => {
let rid = term::args::rid(val)?;
rids.push(rid);
}
Long("scope") => {
let val = parser.value()?;
scope = Some(term::args::parse_value("scope", val)?);
}
Long("fetch") => {
fetch = Some(true);
}
Long("no-fetch") => {
fetch = Some(false);
}
Long("from") => {
let val = parser.value()?;
let nid = term::args::nid(&val)?;
seeds.insert(nid);
}
Long("timeout") | Short('t') => {
let value = parser.value()?;
let secs = term::args::parse_value("timeout", value)?;
timeout = time::Duration::from_secs(secs);
}
Long("verbose") | Short('v') => verbose = true,
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
let op = match NonEmpty::from_vec(rids) {
Some(rids) => Operation::Seed {
rids,
fetch: fetch.unwrap_or(true),
scope: scope.unwrap_or(Scope::All),
timeout,
seeds,
},
None => Operation::List,
};
Ok((Options { op, verbose }, vec![]))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket());
match options.op { match args::Operation::from(args) {
Operation::Seed { args::Operation::List => seeding(&profile)?,
args::Operation::Seed {
rids, rids,
fetch, should_fetch,
settings,
scope, scope,
timeout,
seeds,
} => { } => {
let settings = settings.with_profile(&profile);
for rid in rids { for rid in rids {
update(rid, scope, &mut node, &profile)?; update(rid, scope, &mut node, &profile)?;
if fetch && node.is_running() { if should_fetch && node.is_running() {
if let Err(e) = sync::fetch( if let Err(e) = sync::fetch(rid, settings.clone(), &mut node, &profile) {
rid,
SyncSettings::default()
.seeds(seeds.clone())
.timeout(timeout)
.with_profile(&profile),
&mut node,
&profile,
) {
term::error(e); term::error(e);
} }
} }
} }
} }
Operation::List => seeding(&profile)?,
} }
Ok(()) Ok(())

View File

@ -0,0 +1,104 @@
use std::time;
use clap::Parser;
use nonempty::NonEmpty;
use radicle::node::policy::Scope;
use radicle::prelude::*;
use crate::node::SyncSettings;
use crate::terminal;
pub(crate) const ABOUT: &str = "Manage repository seeding policies";
const LONG_ABOUT: &str = r#"
The `seed` command, when no Repository ID is provided, will list the
repositories being seeded.
When a Repository ID is provided it updates or creates the seeding policy for
that repository. To delete a seeding policy, use the `rad unseed` command.
When seeding a repository, a scope can be specified: this can be either `all` or
`followed`. When using `all`, all remote nodes will be followed for that repository.
On the other hand, with `followed`, only the repository delegates will be followed,
plus any remote that is explicitly followed via `rad follow <nid>`.
"#;
#[derive(Parser, Debug)]
#[command(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
pub struct Args {
#[arg(value_name = "RID", num_args = 1..)]
pub(super) rids: Option<Vec<RepoId>>,
/// Fetch repository after updating seeding policy
#[arg(long, overrides_with("no_fetch"), hide(true))]
fetch: bool,
/// Do not fetch repository after updating seeding policy
#[arg(long, overrides_with("fetch"))]
no_fetch: bool,
/// Fetch from the given node (may be specified multiple times)
#[arg(long, value_name = "NID", action = clap::ArgAction::Append)]
pub(super) from: Vec<NodeId>,
/// Fetch timeout in seconds
#[arg(long, short, value_name = "SECS", default_value_t = 9)]
timeout: u64,
/// Peer follow scope for this repository
#[arg(
long,
default_value_t = Scope::All,
value_parser = terminal::args::ScopeParser
)]
pub(super) scope: Scope,
/// Verbose output
#[arg(long, short)]
pub(super) verbose: bool,
}
pub(super) enum Operation {
List,
Seed {
rids: NonEmpty<RepoId>,
should_fetch: bool,
settings: SyncSettings,
scope: Scope,
},
}
impl From<Args> for Operation {
fn from(args: Args) -> Self {
let should_fetch = args.should_fetch();
let timeout = args.timeout();
let Args {
rids, from, scope, ..
} = args;
match rids.and_then(NonEmpty::from_vec) {
Some(rids) => Operation::Seed {
rids,
should_fetch,
settings: SyncSettings::default().seeds(from).timeout(timeout),
scope,
},
None => Self::List,
}
}
}
impl Args {
fn timeout(&self) -> time::Duration {
time::Duration::from_secs(self.timeout)
}
fn should_fetch(&self) -> bool {
match (self.fetch, self.no_fetch) {
(true, false) => true,
(false, true) => false,
// Default it to fetch
(_, _) => true,
}
}
}

View File

@ -68,6 +68,7 @@ enum Commands {
Ls(ls::Args), Ls(ls::Args),
Path(path::Args), Path(path::Args),
Publish(publish::Args), Publish(publish::Args),
Seed(seed::Args),
Stats(stats::Args), Stats(stats::Args),
Unfollow(unfollow::Args), Unfollow(unfollow::Args),
Unseed(unseed::Args), Unseed(unseed::Args),
@ -306,7 +307,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
term::run_command_args::<sync::Options, _>(sync::HELP, sync::run, args.to_vec()); term::run_command_args::<sync::Options, _>(sync::HELP, sync::run, args.to_vec());
} }
"seed" => { "seed" => {
term::run_command_args::<seed::Options, _>(seed::HELP, seed::run, args.to_vec()); if let Some(Commands::Seed(args)) = CliArgs::parse().command {
term::run_command_fn(seed::run, args);
}
} }
"unblock" => { "unblock" => {
term::run_command_args::<unblock::Options, _>( term::run_command_args::<unblock::Options, _>(