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(Clone, Debug, clap::Args)]
pub struct SubCommandArgs {
#[arg(short, long, num_args = 1..)]
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"],
));
};
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",
],
));
};
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"],
));
}
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() {
accept_maintainership_with_defaults(&git_repo, &repo_ref, &user_ref, &mut client, &signer)
.await?;
} else {
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(())
}
#[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;
let _ = 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?;
}
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")?;
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(())
}