/ radicle-cli / src / commands / publish.rs
publish.rs
  1  use std::ffi::OsString;
  2  
  3  use anyhow::{anyhow, Context as _};
  4  
  5  use radicle::identity::{Identity, Visibility};
  6  use radicle::node::Handle as _;
  7  use radicle::prelude::Id;
  8  use radicle::storage::{ReadRepository, SignRepository, WriteRepository, WriteStorage};
  9  
 10  use crate::terminal as term;
 11  use crate::terminal::args::{Args, Error, Help};
 12  
 13  pub const HELP: Help = Help {
 14      name: "publish",
 15      description: "Publish a repository to the network",
 16      version: env!("CARGO_PKG_VERSION"),
 17      usage: r#"
 18  Usage
 19  
 20      rad publish [<rid>] [<option>...]
 21  
 22      Publishing a private repository makes it public and discoverable
 23      on the network.
 24  
 25      By default, this command will publish the current repository.
 26      If an `<rid>` is specified, that repository will be published instead.
 27  
 28      Note that this command can only be run for repositories with a
 29      single delegate. The delegate must be the currently authenticated
 30      user. For repositories with more than one delegate, the `rad id`
 31      command must be used.
 32  
 33  Options
 34  
 35      --help                    Print help
 36  "#,
 37  };
 38  
 39  #[derive(Default, Debug)]
 40  pub struct Options {
 41      pub rid: Option<Id>,
 42  }
 43  
 44  impl Args for Options {
 45      fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
 46          use lexopt::prelude::*;
 47  
 48          let mut parser = lexopt::Parser::from_args(args);
 49          let mut rid = None;
 50  
 51          while let Some(arg) = parser.next()? {
 52              match arg {
 53                  Long("help") | Short('h') => {
 54                      return Err(Error::Help.into());
 55                  }
 56                  Value(val) if rid.is_none() => {
 57                      rid = Some(term::args::rid(&val)?);
 58                  }
 59                  arg => {
 60                      return Err(anyhow!(arg.unexpected()));
 61                  }
 62              }
 63          }
 64  
 65          Ok((Options { rid }, vec![]))
 66      }
 67  }
 68  
 69  pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
 70      let profile = ctx.profile()?;
 71      let rid = match options.rid {
 72          Some(rid) => rid,
 73          None => radicle::rad::cwd()
 74              .map(|(_, rid)| rid)
 75              .context("Current directory is not a radicle project")?,
 76      };
 77  
 78      let repo = profile.storage.repository_mut(rid)?;
 79      let mut identity = Identity::load_mut(&repo)?;
 80      let mut doc = identity.doc().clone();
 81  
 82      if doc.visibility.is_public() {
 83          return Err(Error::WithHint {
 84              err: anyhow!("repository is already public"),
 85              hint: "to announce the repository to the network, run `rad sync --inventory`",
 86          }
 87          .into());
 88      }
 89      if !doc.is_delegate(profile.id()) {
 90          return Err(anyhow!("only the repository delegate can publish it"));
 91      }
 92      if doc.delegates.len() > 1 {
 93          return Err(Error::WithHint {
 94              err: anyhow!(
 95                  "only repositories with a single delegate can be published with this command"
 96              ),
 97              hint: "see `rad id --help` to publish repositories with more than one delegate",
 98          }
 99          .into());
100      }
101      let signer = profile.signer()?;
102  
103      // Update identity document.
104      doc.visibility = Visibility::Public;
105  
106      identity.update("Publish repository", "", &doc, &signer)?;
107      repo.sign_refs(&signer)?;
108      repo.set_identity_head()?;
109      repo.validate()?;
110  
111      term::success!(
112          "Repository is now {}",
113          term::format::visibility(&doc.visibility)
114      );
115  
116      let mut node = radicle::Node::new(profile.socket());
117      if node.is_running() {
118          let spinner = term::spinner("Announcing to network..");
119          node.announce_inventory()?;
120          spinner.finish();
121      } else {
122          term::warning(format!(
123              "Your node is not running. Start your node with {} to announce your repository \
124              to the network",
125              term::format::command("rad node start")
126          ));
127      }
128  
129      Ok(())
130  }