upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/repo/accept.rs
blob: 5564b778813cbcaee97d0a06458ea83f55be957c (plain)
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
use std::sync::Arc;

use anyhow::{Context, Result};
use ngit::{
    accept_maintainership::{accept_maintainership_with_defaults, wait_for_grasp_servers},
    cli_interactor::cli_error,
    client::{Params, fetching_with_report, get_repo_ref_from_cache, send_events},
    repo_ref::{RepoRef, apply_grasp_infrastructure, latest_event_repo_ref},
};
use nostr::{
    ToBech32,
    nips::{nip01::Coordinate, nip19::Nip19Coordinate},
};
use nostr_sdk::{Kind, NostrSigner, RelayUrl};

use crate::{
    cli::{Cli, extract_signer_cli_arguments},
    client::{Client, Connect},
    git::{Repo, RepoActions},
    login,
    repo_ref::try_and_get_repo_coordinates_when_remote_unknown,
};

#[derive(Debug, clap::Args)]
pub struct SubCommandArgs {
    #[clap(short, long, value_parser, num_args = 1..)]
    /// where your git+nostr data is hosted (optional; uses your saved grasp
    /// server list or the trusted maintainer's servers if not specified)
    grasp_server: Vec<String>,
}

pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
    let git_repo = Repo::discover().context("failed to find a git repository")?;
    let git_repo_path = git_repo.get_path()?;
    let mut client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));

    let (signer, user_ref, _) = login::login_or_signup(
        &Some(&git_repo),
        &extract_signer_cli_arguments(cli_args).unwrap_or(None),
        &cli_args.password,
        Some(&client),
        false,
    )
    .await?;

    let my_pubkey = user_ref.public_key;

    let repo_coordinate = (try_and_get_repo_coordinates_when_remote_unknown(&git_repo).await).ok();

    let Some(repo_coordinate) = repo_coordinate else {
        return Err(cli_error(
            "no nostr repository found",
            &[],
            &["use `ngit repo init` to publish this repository to nostr"],
        ));
    };

    // Fetch latest data from relays
    fetching_with_report(git_repo_path, &client, &repo_coordinate).await?;

    let Some(repo_ref) =
        (get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinate).await).ok()
    else {
        return Err(cli_error(
            "no announcement found on relays for this repository",
            &[],
            &[
                "if you created this repository, use `ngit repo init` to publish an announcement",
                "if this is a relay or network issue, try again later",
            ],
        ));
    };

    // Validate state
    let trusted = repo_ref.trusted_maintainer;

    if trusted == my_pubkey {
        return Err(cli_error(
            "you are already the trusted maintainer of this repository",
            &[],
            &["use `ngit repo edit` to update your announcement"],
        ));
    }

    let has_announcement = repo_ref
        .events
        .keys()
        .any(|c| c.coordinate.public_key == my_pubkey);

    if has_announcement {
        return Err(cli_error(
            "you have already published a co-maintainer announcement for this repository",
            &[],
            &["use `ngit repo edit` to update your announcement"],
        ));
    }

    if !repo_ref.maintainers.contains(&my_pubkey) {
        let trusted_npub = trusted.to_bech32().unwrap_or_else(|_| trusted.to_hex());
        return Err(cli_error(
            "you have not been invited as a maintainer of this repository",
            &[("trusted maintainer", trusted_npub.as_str())],
            &["the trusted maintainer must add your npub to their announcement first"],
        ));
    }

    // Happy path: CoMaintainer state without an existing announcement
    let repo_name = &repo_ref.name;
    let trusted_npub = trusted.to_bech32().unwrap_or_else(|_| trusted.to_hex());
    println!("accepting co-maintainership of '{repo_name}' (offered by {trusted_npub})");
    println!("publishing your repository announcement to nostr...");

    if args.grasp_server.is_empty() {
        // Use the existing defaults logic from the library
        accept_maintainership_with_defaults(&git_repo, &repo_ref, &user_ref, &mut client, &signer)
            .await?;
    } else {
        // User specified grasp servers explicitly — use them
        accept_with_grasp_servers(
            &git_repo,
            &repo_ref,
            &signer,
            &user_ref,
            &mut client,
            &args.grasp_server,
        )
        .await?;
    }

    println!("co-maintainership accepted.");
    println!("your announcement has been published to nostr. you can now push updates.");
    println!("run `ngit repo edit` at any time to update your announcement.");

    Ok(())
}

/// Accept co-maintainership with explicitly specified grasp servers.
#[allow(clippy::too_many_lines)]
async fn accept_with_grasp_servers(
    git_repo: &Repo,
    repo_ref: &RepoRef,
    signer: &Arc<dyn NostrSigner>,
    user_ref: &ngit::login::user::UserRef,
    client: &mut Client,
    grasp_servers: &[String],
) -> Result<()> {
    let my_pubkey = &user_ref.public_key;
    let identifier = &repo_ref.identifier;

    let mut git_servers: Vec<String> = vec![];
    let mut relay_strings: Vec<String> = vec![];

    apply_grasp_infrastructure(
        grasp_servers,
        &mut git_servers,
        &mut relay_strings,
        my_pubkey,
        identifier,
    )?;

    let relays: Vec<RelayUrl> = relay_strings
        .iter()
        .filter_map(|r| RelayUrl::parse(r).ok())
        .collect();

    let latest = latest_event_repo_ref(repo_ref);
    let name = latest
        .as_ref()
        .map_or_else(|| identifier.clone(), |lr| lr.name.clone());
    let description = latest
        .as_ref()
        .map(|lr| lr.description.clone())
        .unwrap_or_default();
    let web = latest.as_ref().map(|lr| lr.web.clone()).unwrap_or_default();
    let hashtags = latest
        .as_ref()
        .map(|lr| lr.hashtags.clone())
        .unwrap_or_default();
    let blossoms = latest
        .as_ref()
        .map(|lr| lr.blossoms.clone())
        .unwrap_or_default();
    let root_commit = latest
        .as_ref()
        .map(|lr| lr.root_commit.clone())
        .filter(|c| !c.is_empty())
        .unwrap_or_else(|| repo_ref.root_commit.clone());

    let mut maintainers = vec![*my_pubkey];
    if repo_ref.trusted_maintainer != *my_pubkey {
        maintainers.push(repo_ref.trusted_maintainer);
    }

    let my_repo_ref = RepoRef {
        identifier: identifier.clone(),
        name,
        description,
        root_commit,
        git_server: git_servers,
        web,
        relays: relays.clone(),
        blossoms,
        hashtags,
        trusted_maintainer: *my_pubkey,
        maintainers_without_annoucnement: None,
        maintainers,
        events: std::collections::HashMap::new(),
        nostr_git_url: None,
    };

    let repo_event = my_repo_ref.to_event(signer).await?;

    client.set_signer(signer.clone()).await;

    send_events(
        client,
        Some(git_repo.get_path()?),
        vec![repo_event],
        user_ref.relays.write(),
        relays.clone(),
        true,
        false,
    )
    .await
    .context("failed to publish co-maintainer announcement")?;

    if !grasp_servers.is_empty() {
        wait_for_grasp_servers(git_repo, grasp_servers, my_pubkey, identifier).await?;
    }

    // Update nostr.repo git config
    git_repo
        .save_git_config_item(
            "nostr.repo",
            &Nip19Coordinate {
                coordinate: Coordinate {
                    kind: Kind::GitRepoAnnouncement,
                    public_key: *my_pubkey,
                    identifier: identifier.clone(),
                },
                relays: vec![],
            }
            .to_bech32()?,
            false,
        )
        .context("failed to update nostr.repo git config")?;

    // Update origin remote
    let nostr_url = my_repo_ref.to_nostr_git_url(&Some(git_repo)).to_string();
    if git_repo.git_repo.find_remote("origin").is_ok() {
        git_repo
            .git_repo
            .remote_set_url("origin", &nostr_url)
            .context("failed to update origin remote")?;
    } else {
        git_repo
            .git_repo
            .remote("origin", &nostr_url)
            .context("failed to set origin remote")?;
    }

    Ok(())
}