radicle_cli/commands/
watch.rs

1use std::ffi::OsString;
2use std::{thread, time};
3
4use anyhow::{anyhow, Context as _};
5
6use radicle::git;
7use radicle::prelude::{NodeId, RepoId};
8use radicle::storage::{ReadRepository, ReadStorage};
9
10use crate::terminal as term;
11use crate::terminal::args::{Args, Error, Help};
12
13pub const HELP: Help = Help {
14    name: "wait",
15    description: "Wait for some state to be updated",
16    version: env!("RADICLE_VERSION"),
17    usage: r#"
18Usage
19
20    rad watch -r <ref> [-t <oid>] [--repo <rid>] [<option>...]
21
22    Watches a Git reference, and optionally exits when it reaches a target value.
23    If no target value is passed, exits when the target changes.
24
25Options
26
27        --repo      <rid>       The repository to watch (default: `rad .`)
28        --node      <nid>       The namespace under which this reference exists
29                                (default: NID of the profile)
30    -r, --ref       <ref>       The fully-qualified Git reference (branch, tag, etc.) to watch,
31                                eg. 'refs/heads/master'
32    -t, --target    <oid>       The target OID (commit hash) that when reached,
33                                will cause the command to exit
34    -i, --interval  <millis>    How often, in milliseconds, to check the reference target
35                                (default: 1000)
36        --timeout   <millis>    Timeout, in milliseconds (default: none)
37    -h, --help                  Print help
38"#,
39};
40
41pub struct Options {
42    rid: Option<RepoId>,
43    refstr: git::RefString,
44    target: Option<git::Oid>,
45    nid: Option<NodeId>,
46    interval: time::Duration,
47    timeout: time::Duration,
48}
49
50impl Args for Options {
51    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
52        use lexopt::prelude::*;
53
54        let mut parser = lexopt::Parser::from_args(args);
55        let mut rid = None;
56        let mut nid: Option<NodeId> = None;
57        let mut target: Option<git::Oid> = None;
58        let mut refstr: Option<git::RefString> = None;
59        let mut interval: Option<time::Duration> = None;
60        let mut timeout: time::Duration = time::Duration::MAX;
61
62        while let Some(arg) = parser.next()? {
63            match arg {
64                Long("repo") => {
65                    let value = parser.value()?;
66                    let value = term::args::rid(&value)?;
67
68                    rid = Some(value);
69                }
70                Long("node") => {
71                    let value = parser.value()?;
72                    let value = term::args::nid(&value)?;
73
74                    nid = Some(value);
75                }
76                Long("ref") | Short('r') => {
77                    let value = parser.value()?;
78                    let value = term::args::refstring("ref", value)?;
79
80                    refstr = Some(value);
81                }
82                Long("target") | Short('t') => {
83                    let value = parser.value()?;
84                    let value = term::args::oid(&value)?;
85
86                    target = Some(value);
87                }
88                Long("interval") | Short('i') => {
89                    let value = parser.value()?;
90                    let value = term::args::milliseconds(&value)?;
91
92                    interval = Some(value);
93                }
94                Long("timeout") => {
95                    let value = parser.value()?;
96                    let value = term::args::milliseconds(&value)?;
97
98                    timeout = value;
99                }
100                Long("help") | Short('h') => {
101                    return Err(Error::Help.into());
102                }
103                _ => anyhow::bail!(arg.unexpected()),
104            }
105        }
106
107        Ok((
108            Options {
109                rid,
110                refstr: refstr.ok_or_else(|| anyhow!("a reference must be provided"))?,
111                nid,
112                target,
113                interval: interval.unwrap_or(time::Duration::from_secs(1)),
114                timeout,
115            },
116            vec![],
117        ))
118    }
119}
120
121pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
122    let profile = ctx.profile()?;
123    let storage = &profile.storage;
124    let qualified = options
125        .refstr
126        .qualified()
127        .ok_or_else(|| anyhow!("reference must be fully-qualified, eg. 'refs/heads/master'"))?;
128    let nid = options.nid.unwrap_or(profile.public_key);
129    let rid = match options.rid {
130        Some(rid) => rid,
131        None => {
132            let (_, rid) =
133                radicle::rad::cwd().context("Current directory is not a Radicle repository")?;
134            rid
135        }
136    };
137    let repo = storage.repository(rid)?;
138    let now = time::SystemTime::now();
139
140    if let Some(target) = options.target {
141        while reference(&repo, &nid, &qualified)? != Some(target) {
142            thread::sleep(options.interval);
143            if now.elapsed()? >= options.timeout {
144                anyhow::bail!("timed out after {}ms", options.timeout.as_millis());
145            }
146        }
147    } else {
148        let initial = reference(&repo, &nid, &qualified)?;
149
150        loop {
151            thread::sleep(options.interval);
152            let oid = reference(&repo, &nid, &qualified)?;
153            if oid != initial {
154                term::info!("{}", oid.unwrap_or(git::raw::Oid::zero().into()));
155                break;
156            }
157            if now.elapsed()? >= options.timeout {
158                anyhow::bail!("timed out after {}ms", options.timeout.as_millis());
159            }
160        }
161    }
162    Ok(())
163}
164
165fn reference<R: ReadRepository>(
166    repo: &R,
167    nid: &NodeId,
168    qual: &git::Qualified,
169) -> Result<Option<git::Oid>, git::raw::Error> {
170    match repo.reference_oid(nid, qual) {
171        Ok(oid) => Ok(Some(oid)),
172        Err(e) if git::ext::is_not_found_err(&e) => Ok(None),
173        Err(e) => Err(e),
174    }
175}