cli/watch: Use clap
This commit is contained in:
parent
efe10f95be
commit
dfd35480be
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue