cli/watch: Use clap

This commit is contained in:
Erik Kundt 2025-10-14 12:00:02 +02:00
parent efe10f95be
commit dfd35480be
5 changed files with 99 additions and 122 deletions

View File

@ -34,6 +34,7 @@ Common `rad` commands used in various situations:
remote Manage a repository's remotes remote Manage a repository's remotes
stats Displays aggregated repository and node metrics stats Displays aggregated repository and node metrics
sync Sync repositories to the network sync Sync repositories to the network
watch Wait for some state to be updated
See `rad <command> --help` to learn about a specific command. See `rad <command> --help` to learn about a specific command.

View File

@ -104,6 +104,10 @@ const COMMANDS: &[CommandItem] = &[
about: crate::commands::stats::ABOUT, about: crate::commands::stats::ABOUT,
}, },
CommandItem::Lexopt(crate::commands::sync::HELP), CommandItem::Lexopt(crate::commands::sync::HELP),
CommandItem::Clap {
name: "watch",
about: crate::commands::watch::ABOUT,
},
]; ];
#[derive(Default)] #[derive(Default)]

View File

@ -1,133 +1,28 @@
use std::ffi::OsString; mod args;
use std::{thread, time}; use std::{thread, time};
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
use radicle::git; use radicle::git;
use radicle::git::raw::ErrorExt as _; use radicle::git::raw::ErrorExt as _;
use radicle::prelude::{NodeId, RepoId}; use radicle::prelude::NodeId;
use radicle::storage::{ReadRepository, ReadStorage}; use radicle::storage::{ReadRepository, ReadStorage};
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: "wait", pub(crate) use args::ABOUT;
description: "Wait for some state to be updated",
version: env!("RADICLE_VERSION"),
usage: r#"
Usage
rad watch -r <ref> [-t <oid>] [--repo <rid>] [<option>...] pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
Watches a Git reference, and optionally exits when it reaches a target value.
If no target value is passed, exits when the target changes.
Options
--repo <rid> The repository to watch (default: `rad .`)
--node <nid> The namespace under which this reference exists
(default: NID of the profile)
-r, --ref <ref> The fully-qualified Git reference (branch, tag, etc.) to watch,
eg. 'refs/heads/master'
-t, --target <oid> The target OID (commit hash) that when reached,
will cause the command to exit
-i, --interval <millis> How often, in milliseconds, to check the reference target
(default: 1000)
--timeout <millis> Timeout, in milliseconds (default: none)
-h, --help Print help
"#,
};
pub struct Options {
rid: Option<RepoId>,
refstr: git::fmt::RefString,
target: Option<git::Oid>,
nid: Option<NodeId>,
interval: time::Duration,
timeout: time::Duration,
}
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 rid = None;
let mut nid: Option<NodeId> = None;
let mut target: Option<git::Oid> = None;
let mut refstr: Option<git::fmt::RefString> = None;
let mut interval: Option<time::Duration> = None;
let mut timeout: time::Duration = time::Duration::MAX;
while let Some(arg) = parser.next()? {
match arg {
Long("repo") => {
let value = parser.value()?;
let value = term::args::rid(&value)?;
rid = Some(value);
}
Long("node") => {
let value = parser.value()?;
let value = term::args::nid(&value)?;
nid = Some(value);
}
Long("ref") | Short('r') => {
let value = parser.value()?;
let value = term::args::refstring("ref", value)?;
refstr = Some(value);
}
Long("target") | Short('t') => {
let value = parser.value()?;
let value = term::args::oid(&value)?;
target = Some(value);
}
Long("interval") | Short('i') => {
let value = parser.value()?;
let value = term::args::milliseconds(&value)?;
interval = Some(value);
}
Long("timeout") => {
let value = parser.value()?;
let value = term::args::milliseconds(&value)?;
timeout = value;
}
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
_ => anyhow::bail!(arg.unexpected()),
}
}
Ok((
Options {
rid,
refstr: refstr.ok_or_else(|| anyhow!("a reference must be provided"))?,
nid,
target,
interval: interval.unwrap_or(time::Duration::from_secs(1)),
timeout,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let storage = &profile.storage; let storage = &profile.storage;
let qualified = options let qualified = args
.refstr .refstr
.qualified() .qualified()
.ok_or_else(|| anyhow!("reference must be fully-qualified, eg. 'refs/heads/master'"))?; .ok_or_else(|| anyhow!("reference must be fully-qualified, eg. 'refs/heads/master'"))?;
let nid = options.nid.unwrap_or(profile.public_key); let nid = args.node.unwrap_or(profile.public_key);
let rid = match options.rid { let rid = match args.repo {
Some(rid) => rid, Some(rid) => rid,
None => { None => {
let (_, rid) = let (_, rid) =
@ -137,26 +32,28 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}; };
let repo = storage.repository(rid)?; let repo = storage.repository(rid)?;
let now = time::SystemTime::now(); let now = time::SystemTime::now();
let timeout = args.timeout();
let interval = args.interval();
if let Some(target) = options.target { if let Some(target) = args.target {
while reference(&repo, &nid, &qualified)? != Some(target) { while reference(&repo, &nid, &qualified)? != Some(target) {
thread::sleep(options.interval); thread::sleep(interval);
if now.elapsed()? >= options.timeout { if now.elapsed()? >= timeout {
anyhow::bail!("timed out after {}ms", options.timeout.as_millis()); anyhow::bail!("timed out after {}ms", timeout.as_millis());
} }
} }
} else { } else {
let initial = reference(&repo, &nid, &qualified)?; let initial = reference(&repo, &nid, &qualified)?;
loop { loop {
thread::sleep(options.interval); thread::sleep(interval);
let oid = reference(&repo, &nid, &qualified)?; let oid = reference(&repo, &nid, &qualified)?;
if oid != initial { if oid != initial {
term::info!("{}", oid.unwrap_or(git::raw::Oid::zero().into())); term::info!("{}", oid.unwrap_or(git::raw::Oid::zero().into()));
break; break;
} }
if now.elapsed()? >= options.timeout { if now.elapsed()? >= timeout {
anyhow::bail!("timed out after {}ms", options.timeout.as_millis()); anyhow::bail!("timed out after {}ms", timeout.as_millis());
} }
} }
} }

View File

@ -0,0 +1,72 @@
use std::time;
#[allow(rustdoc::broken_intra_doc_links)]
use clap::Parser;
use radicle::git;
use radicle::git::fmt::RefString;
use radicle::prelude::{NodeId, RepoId};
pub(crate) const ABOUT: &str = "Wait for some state to be updated";
const LONG_ABOUT: &str = r#"
Watches a Git reference, and optionally exits when it reaches a target value.
If no target value is passed, exits when the target changes."#;
fn parse_refstr(refstr: &str) -> Result<RefString, git::fmt::Error> {
RefString::try_from(refstr)
}
#[derive(Parser, Debug)]
#[command(about = ABOUT, long_about = LONG_ABOUT,disable_version_flag = true)]
pub struct Args {
/// The repository to watch, defaults to `rad .`
#[arg(long)]
pub(super) repo: Option<RepoId>,
/// The fully-qualified Git reference (branch, tag, etc.) to watch
///
/// [example value: 'refs/heads/master']
#[arg(long, short, alias = "ref", value_name = "REF", value_parser = parse_refstr)]
pub(super) refstr: git::fmt::RefString,
/// The target OID (commit hash) that when reached, will cause the command to exit
#[arg(long, short, value_name = "OID")]
pub(super) target: Option<git::Oid>,
/// The namespace under which this reference exists, defaults to the profiles' NID
#[arg(long, short, value_name = "NID")]
pub(super) node: Option<NodeId>,
/// How often, in milliseconds, to check the reference target
#[arg(long, short, value_name = "MILLIS", default_value_t = 1000)]
interval: u64,
/// Timeout, in milliseconds
#[arg(long, value_name = "MILLIS")]
timeout: Option<u64>,
}
impl Args {
/// Provide the interval duration in milliseconds.
pub(super) fn interval(&self) -> time::Duration {
time::Duration::from_millis(self.interval)
}
/// Provide the timeout duration in milliseconds.
pub(super) fn timeout(&self) -> time::Duration {
time::Duration::from_millis(self.timeout.unwrap_or(u64::MAX))
}
}
#[cfg(test)]
mod test {
use super::Args;
use clap::Parser;
#[test]
fn should_parse_ref_str() {
let args = Args::try_parse_from(["watch", "--ref", "refs/heads/master"]);
assert!(args.is_ok())
}
}

View File

@ -58,6 +58,7 @@ enum Commands {
Stats(stats::Args), Stats(stats::Args),
Unfollow(unfollow::Args), Unfollow(unfollow::Args),
Unseed(unseed::Args), Unseed(unseed::Args),
Watch(watch::Args),
} }
#[derive(Debug)] #[derive(Debug)]
@ -314,7 +315,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
} }
} }
"watch" => { "watch" => {
term::run_command_args::<watch::Options, _>(watch::HELP, watch::run, args.to_vec()) if let Some(Commands::Watch(args)) = CliArgs::parse().command {
term::run_command_fn(watch::run, args);
}
} }
other => { other => {
let exe = format!("{NAME}-{exe}"); let exe = format!("{NAME}-{exe}");