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