From 8527646022abdb290222a45314d090eef0871cae Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 4 Sep 2025 12:09:06 +0100 Subject: feat(remote): use push PR non-interactive fallback move the PR push code in 'ngit send' into lib. reuse the non-interactive fallbacks in git-remote-nostr --- src/bin/git_remote_nostr/push.rs | 66 +++----- src/bin/ngit/sub_commands/send.rs | 309 ++------------------------------------ 2 files changed, 39 insertions(+), 336 deletions(-) (limited to 'src/bin') diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index df895b1..8c102ee 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -19,7 +19,7 @@ use ngit::{ git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root}, list::list_from_remotes, login::{self, user::UserRef}, - push::{push_refs_and_generate_pr_or_pr_update_event, push_to_remote}, + push::{push_to_remote, select_servers_push_refs_and_generate_pr_or_pr_update_event}, repo_ref::{self, get_repo_config_from_yaml, is_grasp_server_in_list}, repo_state, utils::{ @@ -173,7 +173,7 @@ async fn create_and_publish_events_and_proposals( existing_state: HashMap, term: &Term, ) -> Result<(Vec, bool)> { - let (signer, user_ref, _) = + let (signer, mut user_ref, _) = login::login_or_signup(&Some(git_repo), &None, &None, Some(client), true).await?; if !repo_ref.maintainers.contains(&user_ref.public_key) { @@ -249,10 +249,11 @@ async fn create_and_publish_events_and_proposals( } let (proposal_events, rejected_proposal_refspecs) = process_proposal_refspecs( + client, git_repo, repo_ref, proposal_refspecs, - &user_ref, + &mut user_ref, &signer, term, ) @@ -281,10 +282,11 @@ async fn create_and_publish_events_and_proposals( #[allow(clippy::too_many_lines)] async fn process_proposal_refspecs( + client: &Client, git_repo: &Repo, repo_ref: &RepoRef, proposal_refspecs: &Vec, - user_ref: &UserRef, + user_ref: &mut UserRef, signer: &Arc, term: &Term, ) -> Result<(Vec, Vec)> { @@ -294,7 +296,7 @@ async fn process_proposal_refspecs( return Ok((events, rejected_proposal_refspecs)); } let all_proposals = get_all_proposals(git_repo, repo_ref).await?; - let current_user = &user_ref.public_key; + let current_user = user_ref.public_key; for refspec in proposal_refspecs { let (from, to) = refspec_to_from_to(refspec).unwrap(); @@ -302,7 +304,7 @@ async fn process_proposal_refspecs( // this failed to find existing PR from user if let Some((_, (proposal, patches))) = - find_proposal_and_patches_by_branch_name(to, &all_proposals, Some(current_user)) + find_proposal_and_patches_by_branch_name(to, &all_proposals, Some(¤t_user)) { if [repo_ref.maintainers.clone(), vec![proposal.pubkey]] .concat() @@ -320,6 +322,7 @@ async fn process_proposal_refspecs( ); } for patch in generate_patches_or_pr_event_or_pr_updates( + client, git_repo, repo_ref, &ahead, @@ -359,6 +362,7 @@ async fn process_proposal_refspecs( || git_repo.are_commits_too_big_for_patches(&ahead) { for event in generate_patches_or_pr_event_or_pr_updates( + client, git_repo, repo_ref, &ahead, @@ -428,7 +432,7 @@ async fn process_proposal_refspecs( ); } for event in generate_patches_or_pr_event_or_pr_updates( - git_repo, repo_ref, &ahead, user_ref, None, signer, term, + client, git_repo, repo_ref, &ahead, user_ref, None, signer, term, ) .await? { @@ -441,11 +445,13 @@ async fn process_proposal_refspecs( } #[allow(clippy::too_many_lines)] +#[allow(clippy::too_many_arguments)] async fn generate_patches_or_pr_event_or_pr_updates( + client: &Client, git_repo: &Repo, repo_ref: &RepoRef, ahead: &[Sha1Hash], - user_ref: &UserRef, + user_ref: &mut UserRef, root_proposal: Option<&Event>, signer: &Arc, term: &Term, @@ -454,53 +460,27 @@ async fn generate_patches_or_pr_event_or_pr_updates( let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead); if use_pr { - let repo_grasps = repo_ref.grasp_servers(); - let repo_grasp_clone_urls: Vec = repo_ref - .git_server - .iter() - .filter(|s| is_grasp_server_in_list(s, &repo_grasps)) - .cloned() - .collect(); - - if repo_grasp_clone_urls.is_empty() { - // TODO get grasp_default_set servers that aren't in repo_grasps - // cycle through until one succeeds TODO create - // personal-fork announcement with grasp servers and - // push, after a few seconds push ref/nostr/eventid. if - // one success break out of for loop and continue - - bail!( - "The repository doesnt list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request. Soon ngit will support pushing your changes to a different git / grasp git server." - ); - } - - if let (Some(events), _) = push_refs_and_generate_pr_or_pr_update_event( + select_servers_push_refs_and_generate_pr_or_pr_update_event( + client, git_repo, repo_ref, ahead.first().context("no commits to push")?, user_ref, root_proposal, &None, - &repo_grasp_clone_urls, - None, signer, + false, term, ) - .await.context( + .await + .context(format!( + "{} run `ngit send` for more options.", if parent_is_pr { - "couldn't generate PR update event" + "couldn't generate PR update event." } else { "a commit in your proposal is too big for a nostr patch so we tried to create it as a nostr PR instead. Unfortunately this failed." - } - )? { - Ok(events) - } else { - bail!( - "a commit in your proposal is too big for a nostr patch. tried to use submit as a nostr Pull Request but could not find a grasp server that would accept your changes" - ); - // TODO suggest `ngit send` where user could specify their own clone - // url to push to once that feature is added - } + }, + )) } else { generate_cover_letter_and_patch_events( None, diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index 3ae941f..ba64f64 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs @@ -1,32 +1,14 @@ -use std::{path::Path, str::FromStr, thread, time::Duration}; +use std::path::Path; use anyhow::{Context, Result, bail}; use console::Style; use ngit::{ - cli_interactor::{ - PromptChoiceParms, multi_select_with_custom_value, show_multi_input_prompt_success, - }, client::{Params, send_events}, - git::nostr_url::CloneUrl, - git_events::{ - EventRefType, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, - generate_cover_letter_and_patch_events, - }, - push::push_refs_and_generate_pr_or_pr_update_event, - repo_ref::{ - format_grasp_server_url_as_clone_url, format_grasp_server_url_as_relay_url, - is_grasp_server_in_list, normalize_grasp_server_url, - }, + git_events::{EventRefType, KIND_PULL_REQUEST, generate_cover_letter_and_patch_events}, + push::select_servers_push_refs_and_generate_pr_or_pr_update_event, utils::proposal_tip_is_pr_or_pr_update, }; -use nostr::{ - ToBech32, - event::{Event, Kind}, - nips::{ - nip01::Coordinate, - nip19::{Nip19Coordinate, Nip19Event}, - }, -}; +use nostr::{ToBech32, event::Event, nips::nip19::Nip19Event}; use nostr_sdk::hashes::sha1::Hash as Sha1Hash; use crate::{ @@ -210,278 +192,19 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re commits.reverse(); let events = if as_pr { - let mut to_try = vec![]; - let mut tried = vec![]; - let repo_grasps = repo_ref.grasp_servers(); - // if the user already has a fork, or is a maintainer, use those git servers - let mut user_repo_ref = get_repo_ref_from_cache( - Some(git_repo_path), - &Nip19Coordinate { - coordinate: Coordinate { - kind: nostr::event::Kind::GitRepoAnnouncement, - public_key: user_ref.public_key, - identifier: repo_ref.identifier.clone(), - }, - relays: vec![], - }, + select_servers_push_refs_and_generate_pr_or_pr_update_event( + &client, + &git_repo, + &repo_ref, + commits.last().context("no commits")?, + &mut user_ref, + root_proposal.as_ref(), + &cover_letter_title_description, + &signer, + true, + &console::Term::stdout(), ) - .await - .ok(); - if let Some(user_repo_ref) = &user_repo_ref { - for url in &user_repo_ref.git_server { - if CloneUrl::from_str(url).is_ok() { - to_try.push(url.clone()); - } - } - } - if !to_try.is_empty() || !repo_grasps.is_empty() { - println!( - "pushing proposal refs to {}", - if repo_ref.maintainers.contains(&user_ref.public_key) { - "repository git servers" - } else if to_try.is_empty() { - "repository grasp servers" - } else if repo_grasps.is_empty() { - "the git servers listed in your fork" - } else { - "the git servers listed in your fork and repository grasp servers" - } - ); - } else { - println!( - "The repository doesn't list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request." - ); - } - // also use repo grasp servers - for url in &repo_ref.git_server { - if is_grasp_server_in_list(url, &repo_grasps) && !to_try.contains(url) { - to_try.push(url.clone()); - } - } - - let mut git_ref = None; - let events = loop { - let (events, _server_responses) = push_refs_and_generate_pr_or_pr_update_event( - &git_repo, - &repo_ref, - commits.last().context("no commits")?, - &user_ref, - root_proposal.as_ref(), - &cover_letter_title_description, - &to_try, - git_ref.clone(), - &signer, - &console::Term::stdout(), - ) - .await?; - for url in to_try { - tried.push(url); - } - to_try = vec![]; - if let Some(events) = events { - break events; - } - // fallback to creating user personal-fork on their grasp servers - let untried_user_grasp_servers: Vec = user_ref - .grasp_list - .urls - .iter() - .map(std::string::ToString::to_string) - .filter(|g| { - // is a grasp server not in list of tried - !is_grasp_server_in_list(g, &tried) - }) - .collect(); - - if untried_user_grasp_servers.is_empty() - && Interactor::default().choice( - PromptChoiceParms::default() - .with_prompt("choose alternative git server") - .dont_report() - .with_choices(vec![ - "choose grasp server(s)".to_string(), - "enter a git repo url with write permission".to_string(), - ]) - .with_default(0), - )? == 1 - { - loop { - let clone_url = Interactor::default() - .input( - PromptInputParms::default() - .with_prompt("git repo url with write permission"), - )? - .clone(); - if CloneUrl::from_str(&clone_url).is_ok() { - to_try.push(clone_url); - let mut git_ref_or_branch_name = Interactor::default() - .input( - PromptInputParms::default() - .with_prompt("ref / branch name") - .with_default( - git_ref.unwrap_or("refs/nostr/".to_string()), - ), - )? - .clone(); - if !git_ref_or_branch_name.starts_with("refs/") { - git_ref_or_branch_name = format!("refs/heads/{git_ref_or_branch_name}"); - } - git_ref = Some(git_ref_or_branch_name); - break; - } - println!("invalid clone url"); - } - continue; - } - - let mut new_grasp_server_events: Vec = vec![]; - - let grasp_servers = if untried_user_grasp_servers.is_empty() { - let default_choices: Vec = client - .get_grasp_default_set() - .iter() - .filter(|g| !is_grasp_server_in_list(g, &tried)) - .cloned() - .collect(); - let selections = vec![true; default_choices.len()]; // all selected by default - let grasp_servers = multi_select_with_custom_value( - "alternative grasp server(s)", - "grasp server", - default_choices, - selections, - normalize_grasp_server_url, - )?; - show_multi_input_prompt_success("alternative grasp server(s)", &grasp_servers); - if grasp_servers.is_empty() { - // ask again - continue; - } - let normalised_grasp_servers: Vec = grasp_servers - .iter() - .filter_map(|g| normalize_grasp_server_url(g).ok()) - .collect(); - // if any grasp servers not listed in user grasp list prompt to update - let grasp_servers_not_in_user_prefs: Vec = normalised_grasp_servers - .iter() - .filter(|g| { - !user_ref.grasp_list.urls.contains( - // unwrap is safe as we constructed g - &nostr::Url::parse(&format_grasp_server_url_as_relay_url(g).unwrap()) - .unwrap(), - ) - }) - .cloned() - .collect(); - if !grasp_servers_not_in_user_prefs.is_empty() - && Interactor::default().confirm( - PromptConfirmParms::default() - .with_prompt( - "add these to your list of prefered grasp servers?".to_string(), - ) - .with_default(true), - )? - { - for g in &normalised_grasp_servers { - let as_url = nostr::Url::parse(&format_grasp_server_url_as_relay_url(g)?)?; - if !user_ref.grasp_list.urls.contains(&as_url) { - user_ref.grasp_list.urls.push(as_url); - } - } - new_grasp_server_events.push(user_ref.grasp_list.to_event(&signer).await?); - } - normalised_grasp_servers - } else { - untried_user_grasp_servers - }; - println!( - "{} personal-fork so we can push commits to your prefered grasp servers", - if user_repo_ref.is_some() { - "Updating" - } else { - "Creating a" - }, - ); - - let grasp_servers_as_personal_clone_url: Vec = grasp_servers - .iter() - .filter_map(|g| { - format_grasp_server_url_as_clone_url( - g, - &user_ref.public_key, - &repo_ref.identifier, - ) - .ok() - }) - .collect(); - - // create personal-fork / update existing user repo and add these grasp servers - let updated_user_repo_ref = { - if let Some(mut user_repo_ref) = user_repo_ref { - for g in &grasp_servers_as_personal_clone_url { - user_repo_ref.add_grasp_server(g)?; - } - user_repo_ref - } else { - // clone repo_ref and reset as personal-fork - let mut user_repo_ref = repo_ref.clone(); - user_repo_ref.trusted_maintainer = user_ref.public_key; - user_repo_ref.maintainers = vec![user_ref.public_key]; - user_repo_ref.git_server = vec![]; - user_repo_ref.relays = vec![]; - if !user_repo_ref - .hashtags - .contains(&"personal-fork".to_string()) - { - user_repo_ref.hashtags.push("personal-fork".to_string()); - } - user_repo_ref - } - }; - // pubish event to my-relays and my-fork-relays - new_grasp_server_events.push(updated_user_repo_ref.to_event(&signer).await?); - send_events( - &client, - Some(git_repo_path), - new_grasp_server_events, - user_ref.relays.write(), - updated_user_repo_ref.relays.clone(), - !cli_args.disable_cli_spinners, - false, - ) - .await?; - user_repo_ref = Some(updated_user_repo_ref); - // wait a few seconds - let countdown_start = 5; - let term = console::Term::stdout(); - for i in (1..=countdown_start).rev() { - term.write_line( - format!( - "waiting {i}s grasp servers to create your repo before we push your data" - ) - .as_str(), - )?; - thread::sleep(Duration::new(1, 0)); // Sleep for 1 second - term.clear_last_lines(1)?; - } - term.flush().unwrap(); // Ensure the output is flushed to the terminal - - // add grasp servers to to_try - for url in grasp_servers_as_personal_clone_url { - to_try.push(url); - } - // the loop with continue with the grasp servers - }; - println!( - "posting {}", - if events.iter().any(|e| e.kind.eq(&Kind::GitStatusClosed)) { - "proposal revision as new PR event, and a close status for the old patch" - } else if events.iter().any(|e| e.kind.eq(&KIND_PULL_REQUEST_UPDATE)) { - "proposal revision as PR update event" - } else { - "proposal as PR event" - } - ); - events + .await? } else { let events = generate_cover_letter_and_patch_events( cover_letter_title_description.clone(), -- cgit v1.2.3