Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Add `watch` command
cloudhead committed 2 years ago
commit 472a1e65455131c942171b8706efc1add981ccfa
parent 5af8f706655c4e8c18fcf80e3c0818b6c9305247
6 files changed +212 -0
added radicle-cli/examples/rad-watch.md
@@ -0,0 +1,20 @@
+
The `rad watch` command allows you to watch a reference and return when it
+
reaches a target commit.
+

+
``` ~bob
+
$ git rev-parse refs/remotes/alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
```
+

+
``` ~alice
+
$ git rev-parse master
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
$ git commit --allow-empty -m "Minor update" -q
+
$ git rev-parse master
+
e09c4dc1b54443ceea715ea648afecdcfd1dd7d0
+
$ git push rad master
+
```
+

+
``` ~bob
+
$ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --ref 'refs/heads/master' --target e09c4dc1b54443ceea715ea648afecdcfd1dd7d0 --interval 500
+
```
modified radicle-cli/src/commands.rs
@@ -46,3 +46,5 @@ pub mod rad_self;
pub mod rad_sync;
#[path = "commands/unfollow.rs"]
pub mod rad_unfollow;
+
#[path = "commands/watch.rs"]
+
pub mod rad_watch;
added radicle-cli/src/commands/watch.rs
@@ -0,0 +1,146 @@
+
use std::ffi::OsString;
+
use std::{thread, time};
+

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

+
use radicle::git;
+
use radicle::prelude::{Id, 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!("CARGO_PKG_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: `rad self --nid`)
+
    -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)
+
    -h, --help                  Print help
+
"#,
+
};
+

+
pub struct Options {
+
    rid: Option<Id>,
+
    refstr: git::RefString,
+
    target: Option<git::Oid>,
+
    nid: Option<NodeId>,
+
    interval: 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::RefString> = None;
+
        let mut interval: Option<time::Duration> = None;
+

+
        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("help") | Short('h') => {
+
                    return Err(Error::Help.into());
+
                }
+
                _ => return Err(anyhow::anyhow!(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)),
+
            },
+
            vec![],
+
        ))
+
    }
+
}
+

+
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
    let profile = ctx.profile()?;
+
    let storage = &profile.storage;
+
    let qualified = options
+
        .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 {
+
        Some(rid) => rid,
+
        None => {
+
            let (_, rid) =
+
                radicle::rad::cwd().context("Current directory is not a radicle project")?;
+
            rid
+
        }
+
    };
+
    let repo = storage.repository(rid)?;
+

+
    if let Some(target) = options.target {
+
        while repo.reference_oid(&nid, &qualified)? != target {
+
            thread::sleep(options.interval);
+
        }
+
    } else {
+
        let initial = repo.reference_oid(&nid, &qualified)?;
+

+
        loop {
+
            thread::sleep(options.interval);
+
            let oid = repo.reference_oid(&nid, &qualified)?;
+
            if oid != initial {
+
                term::info!("{oid}");
+
                break;
+
            }
+
        }
+
    }
+
    Ok(())
+
}
modified radicle-cli/src/main.rs
@@ -255,6 +255,11 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
            rad_remote::run,
            args.to_vec(),
        ),
+
        "watch" => term::run_command_args::<rad_watch::Options, _>(
+
            rad_watch::HELP,
+
            rad_watch::run,
+
            args.to_vec(),
+
        ),
        other => {
            let exe = format!("{NAME}-{exe}");
            let status = process::Command::new(exe).args(args).status();
modified radicle-cli/src/terminal/args.rs
@@ -145,6 +145,14 @@ pub fn seconds(val: &OsString) -> anyhow::Result<time::Duration> {
    Ok(time::Duration::from_secs(secs))
}

+
pub fn milliseconds(val: &OsString) -> anyhow::Result<time::Duration> {
+
    let val = val.to_string_lossy();
+
    let secs =
+
        u64::from_str(&val).map_err(|_| anyhow!("invalid number of milliseconds '{}'", val))?;
+

+
    Ok(time::Duration::from_millis(secs))
+
}
+

pub fn string(val: &OsString) -> String {
    val.to_string_lossy().to_string()
}
modified radicle-cli/tests/commands.rs
@@ -1719,6 +1719,37 @@ fn rad_patch_fetch_1() {
}

#[test]
+
fn rad_watch() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node(Config::test(Alias::new("alice")));
+
    let bob = environment.node(Config::test(Alias::new("bob")));
+
    let working = environment.tmp().join("working");
+
    let (repo, _) = fixtures::repository(working.join("alice"));
+
    let rid = alice.project_from("heartwood", "Radicle Heartwood Protocol & Stack", &repo);
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.clone(rid, working.join("bob")).unwrap();
+

+
    formula(&environment.tmp(), "examples/rad-watch.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            working.join("alice"),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            working.join("bob").join("heartwood"),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
fn rad_patch_fetch_2() {
    let mut environment = Environment::new();
    let alice = environment.node(Config::test(Alias::new("alice")));