radicle_cli/commands/
publish.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use std::ffi::OsString;

use anyhow::{anyhow, Context as _};

use radicle::identity::{Identity, Visibility};
use radicle::node::Handle as _;
use radicle::prelude::RepoId;
use radicle::storage::{SignRepository, ValidateRepository, WriteRepository, WriteStorage};

use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};

pub const HELP: Help = Help {
    name: "publish",
    description: "Publish a repository to the network",
    version: env!("RADICLE_VERSION"),
    usage: r#"
Usage

    rad publish [<rid>] [<option>...]

    Publishing a private repository makes it public and discoverable
    on the network.

    By default, this command will publish the current repository.
    If an `<rid>` is specified, that repository will be published instead.

    Note that this command can only be run for repositories with a
    single delegate. The delegate must be the currently authenticated
    user. For repositories with more than one delegate, the `rad id`
    command must be used.

Options

    --help                    Print help
"#,
};

#[derive(Default, Debug)]
pub struct Options {
    pub rid: Option<RepoId>,
}

impl Args for Options {
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
        use lexopt::prelude::*;

        let mut parser = lexopt::Parser::from_args(args);
        let mut rid = None;

        while let Some(arg) = parser.next()? {
            match arg {
                Long("help") | Short('h') => {
                    return Err(Error::Help.into());
                }
                Value(val) if rid.is_none() => {
                    rid = Some(term::args::rid(&val)?);
                }
                arg => {
                    return Err(anyhow!(arg.unexpected()));
                }
            }
        }

        Ok((Options { rid }, vec![]))
    }
}

pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    let profile = ctx.profile()?;
    let rid = match options.rid {
        Some(rid) => rid,
        None => radicle::rad::cwd()
            .map(|(_, rid)| rid)
            .context("Current directory is not a Radicle repository")?,
    };

    let repo = profile.storage.repository_mut(rid)?;
    let mut identity = Identity::load_mut(&repo)?;
    let doc = identity.doc();

    if doc.is_public() {
        return Err(Error::WithHint {
            err: anyhow!("repository is already public"),
            hint: "to announce the repository to the network, run `rad sync --inventory`",
        }
        .into());
    }
    if !doc.is_delegate(&profile.id().into()) {
        return Err(anyhow!("only the repository delegate can publish it"));
    }
    if doc.delegates().len() > 1 {
        return Err(Error::WithHint {
            err: anyhow!(
                "only repositories with a single delegate can be published with this command"
            ),
            hint: "see `rad id --help` to publish repositories with more than one delegate",
        }
        .into());
    }
    let signer = profile.signer()?;

    // Update identity document.
    let doc = doc.clone().with_edits(|doc| {
        doc.visibility = Visibility::Public;
    })?;

    identity.update("Publish repository", "", &doc, &signer)?;
    repo.sign_refs(&signer)?;
    repo.set_identity_head()?;
    let validations = repo.validate()?;

    if !validations.is_empty() {
        for err in validations {
            term::error(format!("validation error: {err}"));
        }
        anyhow::bail!("fatal: repository storage is corrupt");
    }
    let mut node = radicle::Node::new(profile.socket());
    let spinner = term::spinner("Updating inventory..");

    // The repository is now part of our inventory.
    profile.add_inventory(rid, &mut node)?;
    spinner.finish();

    term::success!(
        "Repository is now {}",
        term::format::visibility(doc.visibility())
    );

    if !node.is_running() {
        term::warning(format!(
            "Your node is not running. Start your node with {} to announce your repository \
            to the network",
            term::format::command("rad node start")
        ));
    }

    Ok(())
}