Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli/watch: Use clap
Merged did:key:z6MkgFq6...nBGz opened 6 months ago
5 files changed +99 -122 efe10f95 dfd35480
modified crates/radicle-cli/examples/rad-help.md
@@ -34,6 +34,7 @@ Common `rad` commands used in various situations:
	remote       Manage a repository's remotes
	stats        Displays aggregated repository and node metrics
	sync         Sync repositories to the network
+
	watch        Wait for some state to be updated

See `rad <command> --help` to learn about a specific command.

modified crates/radicle-cli/src/commands/help.rs
@@ -104,6 +104,10 @@ const COMMANDS: &[CommandItem] = &[
        about: crate::commands::stats::ABOUT,
    },
    CommandItem::Lexopt(crate::commands::sync::HELP),
+
    CommandItem::Clap {
+
        name: "watch",
+
        about: crate::commands::watch::ABOUT,
+
    },
];

#[derive(Default)]
modified crates/radicle-cli/src/commands/watch.rs
@@ -1,133 +1,28 @@
-
use std::ffi::OsString;
+
mod args;
+

use std::{thread, time};

use anyhow::{anyhow, Context as _};

use radicle::git;
use radicle::git::raw::ErrorExt as _;
-
use radicle::prelude::{NodeId, RepoId};
+
use radicle::prelude::NodeId;
use radicle::storage::{ReadRepository, ReadStorage};

use crate::terminal as term;
-
use crate::terminal::args::{Args, Error, Help};
-

-
pub const HELP: Help = Help {
-
    name: "wait",
-
    description: "Wait for some state to be updated",
-
    version: env!("RADICLE_VERSION"),
-
    usage: r#"
-
Usage
-

-
    rad watch -r <ref> [-t <oid>] [--repo <rid>] [<option>...]
-

-
    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 use args::Args;
+
pub(crate) use args::ABOUT;

-
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    let profile = ctx.profile()?;
    let storage = &profile.storage;
-
    let qualified = options
+
    let qualified = args
        .refstr
        .qualified()
        .ok_or_else(|| anyhow!("reference must be fully-qualified, eg. 'refs/heads/master'"))?;
-
    let nid = options.nid.unwrap_or(profile.public_key);
-
    let rid = match options.rid {
+
    let nid = args.node.unwrap_or(profile.public_key);
+
    let rid = match args.repo {
        Some(rid) => rid,
        None => {
            let (_, rid) =
@@ -137,26 +32,28 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    };
    let repo = storage.repository(rid)?;
    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) {
-
            thread::sleep(options.interval);
-
            if now.elapsed()? >= options.timeout {
-
                anyhow::bail!("timed out after {}ms", options.timeout.as_millis());
+
            thread::sleep(interval);
+
            if now.elapsed()? >= timeout {
+
                anyhow::bail!("timed out after {}ms", timeout.as_millis());
            }
        }
    } else {
        let initial = reference(&repo, &nid, &qualified)?;

        loop {
-
            thread::sleep(options.interval);
+
            thread::sleep(interval);
            let oid = reference(&repo, &nid, &qualified)?;
            if oid != initial {
                term::info!("{}", oid.unwrap_or(git::raw::Oid::zero().into()));
                break;
            }
-
            if now.elapsed()? >= options.timeout {
-
                anyhow::bail!("timed out after {}ms", options.timeout.as_millis());
+
            if now.elapsed()? >= timeout {
+
                anyhow::bail!("timed out after {}ms", timeout.as_millis());
            }
        }
    }
added crates/radicle-cli/src/commands/watch/args.rs
@@ -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())
+
    }
+
}
modified crates/radicle-cli/src/main.rs
@@ -58,6 +58,7 @@ enum Commands {
    Stats(stats::Args),
    Unfollow(unfollow::Args),
    Unseed(unseed::Args),
+
    Watch(watch::Args),
}

#[derive(Debug)]
@@ -314,7 +315,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
            }
        }
        "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 => {
            let exe = format!("{NAME}-{exe}");