radicle_cli/commands/
publish.rs

1use 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    // Update identity document.
105    let doc = doc.clone().with_edits(|doc| {
106        doc.visibility = Visibility::Public;
107    })?;
108
109    // SAFETY: the `Title` here is guaranteed to be nonempty and does not
110    // contain `\n` or `\r`.
111    #[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    // The repository is now part of our inventory.
132    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}