radicle_cli/commands/
publish.rs1use std::ffi::OsString;
2
3use anyhow::{anyhow, Context as _};
4
5use radicle::identity::{Identity, Visibility};
6use radicle::node::Handle as _;
7use radicle::prelude::RepoId;
8use radicle::storage::{SignRepository, ValidateRepository, WriteRepository, WriteStorage};
9
10use crate::terminal as term;
11use crate::terminal::args::{Args, Error, Help};
12
13pub const HELP: Help = Help {
14 name: "publish",
15 description: "Publish a repository to the network",
16 version: env!("RADICLE_VERSION"),
17 usage: r#"
18Usage
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
33Options
34
35 --help Print help
36"#,
37};
38
39#[derive(Default, Debug)]
40pub struct Options {
41 pub rid: Option<RepoId>,
42}
43
44impl 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
69pub 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 repository")?,
76 };
77
78 let repo = profile.storage.repository_mut(rid)?;
79 let mut identity = Identity::load_mut(&repo)?;
80 let doc = identity.doc();
81
82 if doc.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().into()) {
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 let doc = doc.clone().with_edits(|doc| {
105 doc.visibility = Visibility::Public;
106 })?;
107
108 identity.update("Publish repository", "", &doc, &signer)?;
109 repo.sign_refs(&signer)?;
110 repo.set_identity_head()?;
111 let validations = repo.validate()?;
112
113 if !validations.is_empty() {
114 for err in validations {
115 term::error(format!("validation error: {err}"));
116 }
117 anyhow::bail!("fatal: repository storage is corrupt");
118 }
119 let mut node = radicle::Node::new(profile.socket());
120 let spinner = term::spinner("Updating inventory..");
121
122 profile.add_inventory(rid, &mut node)?;
124 spinner.finish();
125
126 term::success!(
127 "Repository is now {}",
128 term::format::visibility(doc.visibility())
129 );
130
131 if !node.is_running() {
132 term::warning(format!(
133 "Your node is not running. Start your node with {} to announce your repository \
134 to the network",
135 term::format::command("rad node start")
136 ));
137 }
138
139 Ok(())
140}