radicle_cli/commands/
publish.rs

1use 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    // Update identity document.
104    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    // The repository is now part of our inventory.
123    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}