/ radicle-cli / src / commands / track.rs
track.rs
  1  use std::ffi::OsString;
  2  use std::time;
  3  
  4  use anyhow::anyhow;
  5  
  6  use radicle::node;
  7  use radicle::node::tracking::{Alias, Scope};
  8  use radicle::node::{Handle, NodeId};
  9  use radicle::{prelude::*, Node};
 10  
 11  use crate::commands::rad_sync as sync;
 12  use crate::terminal as term;
 13  use crate::terminal::args::{Args, Error, Help};
 14  
 15  pub const HELP: Help = Help {
 16      name: "track",
 17      description: "Manage repository and node tracking policy",
 18      version: env!("CARGO_PKG_VERSION"),
 19      usage: r#"
 20  Usage
 21  
 22      rad track <nid> [--alias <name>] [<option>...]
 23      rad track <rid> [--[no-]fetch] [--scope <scope>] [<option>...]
 24  
 25      The `track` command takes either an NID or an RID. Based on the argument, it will
 26      either update the tracking policy of a node (NID), or a repository (RID).
 27  
 28      When tracking a repository, a scope can be specified: this can be either `all` or
 29      `trusted`. When using `all`, all remote nodes will be tracked for that repository.
 30      On the other hand, with `trusted`, only the repository delegates will be tracked,
 31      plus any remote that is explicitly tracked via `rad track <nid>`.
 32  
 33  Options
 34  
 35      --alias <name>         Associate an alias to a tracked node
 36      --[no-]fetch           Fetch refs after tracking
 37      --scope <scope>        Node (remote) tracking scope for a repository
 38      --verbose, -v          Verbose output
 39      --help                 Print help
 40  "#,
 41  };
 42  
 43  #[derive(Debug)]
 44  pub enum Operation {
 45      TrackNode { nid: NodeId, alias: Option<Alias> },
 46      TrackRepo { rid: Id, scope: Scope },
 47  }
 48  
 49  #[derive(Debug)]
 50  pub struct Options {
 51      pub op: Operation,
 52      pub fetch: bool,
 53      pub verbose: bool,
 54  }
 55  
 56  impl Args for Options {
 57      fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
 58          use lexopt::prelude::*;
 59  
 60          let mut parser = lexopt::Parser::from_args(args);
 61          let mut op: Option<Operation> = None;
 62          let mut fetch = true;
 63          let mut verbose = false;
 64  
 65          while let Some(arg) = parser.next()? {
 66              match (&arg, &mut op) {
 67                  (Value(val), None) => {
 68                      if let Ok(rid) = term::args::rid(val) {
 69                          op = Some(Operation::TrackRepo {
 70                              rid,
 71                              scope: Scope::default(),
 72                          });
 73                      } else if let Ok(did) = term::args::did(val) {
 74                          op = Some(Operation::TrackNode {
 75                              nid: did.into(),
 76                              alias: None,
 77                          });
 78                      } else if let Ok(nid) = term::args::nid(val) {
 79                          op = Some(Operation::TrackNode { nid, alias: None });
 80                      }
 81                  }
 82                  (Long("alias"), Some(Operation::TrackNode { alias, .. })) => {
 83                      let name = parser.value()?;
 84                      let name = term::args::alias(&name)?;
 85  
 86                      *alias = Some(name.to_owned());
 87                  }
 88                  (Long("scope"), Some(Operation::TrackRepo { scope, .. })) => {
 89                      let val = parser.value()?;
 90  
 91                      *scope = term::args::parse_value("scope", val)?;
 92                  }
 93                  (Long("fetch"), Some(Operation::TrackRepo { .. })) => fetch = true,
 94                  (Long("no-fetch"), Some(Operation::TrackRepo { .. })) => fetch = false,
 95                  (Long("verbose") | Short('v'), _) => verbose = true,
 96                  (Long("help") | Short('h'), _) => {
 97                      return Err(Error::Help.into());
 98                  }
 99                  _ => {
100                      return Err(anyhow!(arg.unexpected()));
101                  }
102              }
103          }
104  
105          Ok((
106              Options {
107                  op: op.ok_or_else(|| anyhow!("either a NID or an RID must be specified"))?,
108                  fetch,
109                  verbose,
110              },
111              vec![],
112          ))
113      }
114  }
115  
116  pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
117      let profile = ctx.profile()?;
118      let mut node = radicle::Node::new(profile.socket());
119  
120      match options.op {
121          Operation::TrackNode { nid, alias } => {
122              if let Err(node::Error::Connect(_)) = track_node(nid, alias, &mut node) {
123                  anyhow::bail!("to track another node, your node must be running. To start it, run `rad node start`");
124              }
125          }
126          Operation::TrackRepo { rid, scope } => {
127              if let Err(node::Error::Connect(_)) = track_repo(rid, scope, &mut node) {
128                  anyhow::bail!("to track a repository, your node must be running. To start it, run `rad node start`");
129              }
130  
131              if options.fetch {
132                  sync::fetch(
133                      rid,
134                      sync::RepoSync::default(),
135                      time::Duration::from_secs(6),
136                      &mut node,
137                  )?;
138              }
139          }
140      }
141      Ok(())
142  }
143  
144  pub fn track_repo(rid: Id, scope: Scope, node: &mut Node) -> Result<(), node::Error> {
145      let tracked = node.track_repo(rid, scope)?;
146      let outcome = if tracked { "updated" } else { "exists" };
147  
148      term::success!(
149          "Tracking policy {outcome} for {} with scope '{scope}'",
150          term::format::tertiary(rid),
151      );
152  
153      Ok(())
154  }
155  
156  pub fn track_node(nid: NodeId, alias: Option<Alias>, node: &mut Node) -> Result<(), node::Error> {
157      let tracked = node.track_node(nid, alias.clone())?;
158      let outcome = if tracked { "updated" } else { "exists" };
159  
160      if let Some(alias) = alias {
161          term::success!(
162              "Tracking policy {outcome} for {} ({alias})",
163              term::format::tertiary(nid),
164          );
165      } else {
166          term::success!(
167              "Tracking policy {outcome} for {}",
168              term::format::tertiary(nid),
169          );
170      }
171  
172      Ok(())
173  }