radicle_cli/commands/
watch.rs1use 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}