upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-09-04 12:09:06 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2025-09-04 12:09:06 +0100
commit8527646022abdb290222a45314d090eef0871cae (patch)
tree16b92960553c8da5c30e7c0059d2a82a2a366c68
parent4bd46c3cf7bacb062d45d3c99d3edfadc95cb139 (diff)
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
-rw-r--r--src/bin/git_remote_nostr/push.rs66
-rw-r--r--src/bin/ngit/sub_commands/send.rs309
-rw-r--r--src/lib/push.rs319
3 files changed, 351 insertions, 343 deletions
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::{
19 git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root}, 19 git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root},
20 list::list_from_remotes, 20 list::list_from_remotes,
21 login::{self, user::UserRef}, 21 login::{self, user::UserRef},
22 push::{push_refs_and_generate_pr_or_pr_update_event, push_to_remote}, 22 push::{push_to_remote, select_servers_push_refs_and_generate_pr_or_pr_update_event},
23 repo_ref::{self, get_repo_config_from_yaml, is_grasp_server_in_list}, 23 repo_ref::{self, get_repo_config_from_yaml, is_grasp_server_in_list},
24 repo_state, 24 repo_state,
25 utils::{ 25 utils::{
@@ -173,7 +173,7 @@ async fn create_and_publish_events_and_proposals(
173 existing_state: HashMap<String, String>, 173 existing_state: HashMap<String, String>,
174 term: &Term, 174 term: &Term,
175) -> Result<(Vec<String>, bool)> { 175) -> Result<(Vec<String>, bool)> {
176 let (signer, user_ref, _) = 176 let (signer, mut user_ref, _) =
177 login::login_or_signup(&Some(git_repo), &None, &None, Some(client), true).await?; 177 login::login_or_signup(&Some(git_repo), &None, &None, Some(client), true).await?;
178 178
179 if !repo_ref.maintainers.contains(&user_ref.public_key) { 179 if !repo_ref.maintainers.contains(&user_ref.public_key) {
@@ -249,10 +249,11 @@ async fn create_and_publish_events_and_proposals(
249 } 249 }
250 250
251 let (proposal_events, rejected_proposal_refspecs) = process_proposal_refspecs( 251 let (proposal_events, rejected_proposal_refspecs) = process_proposal_refspecs(
252 client,
252 git_repo, 253 git_repo,
253 repo_ref, 254 repo_ref,
254 proposal_refspecs, 255 proposal_refspecs,
255 &user_ref, 256 &mut user_ref,
256 &signer, 257 &signer,
257 term, 258 term,
258 ) 259 )
@@ -281,10 +282,11 @@ async fn create_and_publish_events_and_proposals(
281 282
282#[allow(clippy::too_many_lines)] 283#[allow(clippy::too_many_lines)]
283async fn process_proposal_refspecs( 284async fn process_proposal_refspecs(
285 client: &Client,
284 git_repo: &Repo, 286 git_repo: &Repo,
285 repo_ref: &RepoRef, 287 repo_ref: &RepoRef,
286 proposal_refspecs: &Vec<String>, 288 proposal_refspecs: &Vec<String>,
287 user_ref: &UserRef, 289 user_ref: &mut UserRef,
288 signer: &Arc<dyn NostrSigner>, 290 signer: &Arc<dyn NostrSigner>,
289 term: &Term, 291 term: &Term,
290) -> Result<(Vec<Event>, Vec<String>)> { 292) -> Result<(Vec<Event>, Vec<String>)> {
@@ -294,7 +296,7 @@ async fn process_proposal_refspecs(
294 return Ok((events, rejected_proposal_refspecs)); 296 return Ok((events, rejected_proposal_refspecs));
295 } 297 }
296 let all_proposals = get_all_proposals(git_repo, repo_ref).await?; 298 let all_proposals = get_all_proposals(git_repo, repo_ref).await?;
297 let current_user = &user_ref.public_key; 299 let current_user = user_ref.public_key;
298 300
299 for refspec in proposal_refspecs { 301 for refspec in proposal_refspecs {
300 let (from, to) = refspec_to_from_to(refspec).unwrap(); 302 let (from, to) = refspec_to_from_to(refspec).unwrap();
@@ -302,7 +304,7 @@ async fn process_proposal_refspecs(
302 304
303 // this failed to find existing PR from user 305 // this failed to find existing PR from user
304 if let Some((_, (proposal, patches))) = 306 if let Some((_, (proposal, patches))) =
305 find_proposal_and_patches_by_branch_name(to, &all_proposals, Some(current_user)) 307 find_proposal_and_patches_by_branch_name(to, &all_proposals, Some(&current_user))
306 { 308 {
307 if [repo_ref.maintainers.clone(), vec![proposal.pubkey]] 309 if [repo_ref.maintainers.clone(), vec![proposal.pubkey]]
308 .concat() 310 .concat()
@@ -320,6 +322,7 @@ async fn process_proposal_refspecs(
320 ); 322 );
321 } 323 }
322 for patch in generate_patches_or_pr_event_or_pr_updates( 324 for patch in generate_patches_or_pr_event_or_pr_updates(
325 client,
323 git_repo, 326 git_repo,
324 repo_ref, 327 repo_ref,
325 &ahead, 328 &ahead,
@@ -359,6 +362,7 @@ async fn process_proposal_refspecs(
359 || git_repo.are_commits_too_big_for_patches(&ahead) 362 || git_repo.are_commits_too_big_for_patches(&ahead)
360 { 363 {
361 for event in generate_patches_or_pr_event_or_pr_updates( 364 for event in generate_patches_or_pr_event_or_pr_updates(
365 client,
362 git_repo, 366 git_repo,
363 repo_ref, 367 repo_ref,
364 &ahead, 368 &ahead,
@@ -428,7 +432,7 @@ async fn process_proposal_refspecs(
428 ); 432 );
429 } 433 }
430 for event in generate_patches_or_pr_event_or_pr_updates( 434 for event in generate_patches_or_pr_event_or_pr_updates(
431 git_repo, repo_ref, &ahead, user_ref, None, signer, term, 435 client, git_repo, repo_ref, &ahead, user_ref, None, signer, term,
432 ) 436 )
433 .await? 437 .await?
434 { 438 {
@@ -441,11 +445,13 @@ async fn process_proposal_refspecs(
441} 445}
442 446
443#[allow(clippy::too_many_lines)] 447#[allow(clippy::too_many_lines)]
448#[allow(clippy::too_many_arguments)]
444async fn generate_patches_or_pr_event_or_pr_updates( 449async fn generate_patches_or_pr_event_or_pr_updates(
450 client: &Client,
445 git_repo: &Repo, 451 git_repo: &Repo,
446 repo_ref: &RepoRef, 452 repo_ref: &RepoRef,
447 ahead: &[Sha1Hash], 453 ahead: &[Sha1Hash],
448 user_ref: &UserRef, 454 user_ref: &mut UserRef,
449 root_proposal: Option<&Event>, 455 root_proposal: Option<&Event>,
450 signer: &Arc<dyn NostrSigner>, 456 signer: &Arc<dyn NostrSigner>,
451 term: &Term, 457 term: &Term,
@@ -454,53 +460,27 @@ async fn generate_patches_or_pr_event_or_pr_updates(
454 let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead); 460 let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead);
455 461
456 if use_pr { 462 if use_pr {
457 let repo_grasps = repo_ref.grasp_servers(); 463 select_servers_push_refs_and_generate_pr_or_pr_update_event(
458 let repo_grasp_clone_urls: Vec<String> = repo_ref 464 client,
459 .git_server
460 .iter()
461 .filter(|s| is_grasp_server_in_list(s, &repo_grasps))
462 .cloned()
463 .collect();
464
465 if repo_grasp_clone_urls.is_empty() {
466 // TODO get grasp_default_set servers that aren't in repo_grasps
467 // cycle through until one succeeds TODO create
468 // personal-fork announcement with grasp servers and
469 // push, after a few seconds push ref/nostr/eventid. if
470 // one success break out of for loop and continue
471
472 bail!(
473 "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."
474 );
475 }
476
477 if let (Some(events), _) = push_refs_and_generate_pr_or_pr_update_event(
478 git_repo, 465 git_repo,
479 repo_ref, 466 repo_ref,
480 ahead.first().context("no commits to push")?, 467 ahead.first().context("no commits to push")?,
481 user_ref, 468 user_ref,
482 root_proposal, 469 root_proposal,
483 &None, 470 &None,
484 &repo_grasp_clone_urls,
485 None,
486 signer, 471 signer,
472 false,
487 term, 473 term,
488 ) 474 )
489 .await.context( 475 .await
476 .context(format!(
477 "{} run `ngit send` for more options.",
490 if parent_is_pr { 478 if parent_is_pr {
491 "couldn't generate PR update event" 479 "couldn't generate PR update event."
492 } else { 480 } else {
493 "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." 481 "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."
494 } 482 },
495 )? { 483 ))
496 Ok(events)
497 } else {
498 bail!(
499 "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"
500 );
501 // TODO suggest `ngit send` where user could specify their own clone
502 // url to push to once that feature is added
503 }
504 } else { 484 } else {
505 generate_cover_letter_and_patch_events( 485 generate_cover_letter_and_patch_events(
506 None, 486 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 @@
1use std::{path::Path, str::FromStr, thread, time::Duration}; 1use std::path::Path;
2 2
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use console::Style; 4use console::Style;
5use ngit::{ 5use ngit::{
6 cli_interactor::{
7 PromptChoiceParms, multi_select_with_custom_value, show_multi_input_prompt_success,
8 },
9 client::{Params, send_events}, 6 client::{Params, send_events},
10 git::nostr_url::CloneUrl, 7 git_events::{EventRefType, KIND_PULL_REQUEST, generate_cover_letter_and_patch_events},
11 git_events::{ 8 push::select_servers_push_refs_and_generate_pr_or_pr_update_event,
12 EventRefType, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE,
13 generate_cover_letter_and_patch_events,
14 },
15 push::push_refs_and_generate_pr_or_pr_update_event,
16 repo_ref::{
17 format_grasp_server_url_as_clone_url, format_grasp_server_url_as_relay_url,
18 is_grasp_server_in_list, normalize_grasp_server_url,
19 },
20 utils::proposal_tip_is_pr_or_pr_update, 9 utils::proposal_tip_is_pr_or_pr_update,
21}; 10};
22use nostr::{ 11use nostr::{ToBech32, event::Event, nips::nip19::Nip19Event};
23 ToBech32,
24 event::{Event, Kind},
25 nips::{
26 nip01::Coordinate,
27 nip19::{Nip19Coordinate, Nip19Event},
28 },
29};
30use nostr_sdk::hashes::sha1::Hash as Sha1Hash; 12use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
31 13
32use crate::{ 14use crate::{
@@ -210,278 +192,19 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re
210 commits.reverse(); 192 commits.reverse();
211 193
212 let events = if as_pr { 194 let events = if as_pr {
213 let mut to_try = vec![]; 195 select_servers_push_refs_and_generate_pr_or_pr_update_event(
214 let mut tried = vec![]; 196 &client,
215 let repo_grasps = repo_ref.grasp_servers(); 197 &git_repo,
216 // if the user already has a fork, or is a maintainer, use those git servers 198 &repo_ref,
217 let mut user_repo_ref = get_repo_ref_from_cache( 199 commits.last().context("no commits")?,
218 Some(git_repo_path), 200 &mut user_ref,
219 &Nip19Coordinate { 201 root_proposal.as_ref(),
220 coordinate: Coordinate { 202 &cover_letter_title_description,
221 kind: nostr::event::Kind::GitRepoAnnouncement, 203 &signer,
222 public_key: user_ref.public_key, 204 true,
223 identifier: repo_ref.identifier.clone(), 205 &console::Term::stdout(),
224 },
225 relays: vec![],
226 },
227 ) 206 )
228 .await 207 .await?
229 .ok();
230 if let Some(user_repo_ref) = &user_repo_ref {
231 for url in &user_repo_ref.git_server {
232 if CloneUrl::from_str(url).is_ok() {
233 to_try.push(url.clone());
234 }
235 }
236 }
237 if !to_try.is_empty() || !repo_grasps.is_empty() {
238 println!(
239 "pushing proposal refs to {}",
240 if repo_ref.maintainers.contains(&user_ref.public_key) {
241 "repository git servers"
242 } else if to_try.is_empty() {
243 "repository grasp servers"
244 } else if repo_grasps.is_empty() {
245 "the git servers listed in your fork"
246 } else {
247 "the git servers listed in your fork and repository grasp servers"
248 }
249 );
250 } else {
251 println!(
252 "The repository doesn't list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request."
253 );
254 }
255 // also use repo grasp servers
256 for url in &repo_ref.git_server {
257 if is_grasp_server_in_list(url, &repo_grasps) && !to_try.contains(url) {
258 to_try.push(url.clone());
259 }
260 }
261
262 let mut git_ref = None;
263 let events = loop {
264 let (events, _server_responses) = push_refs_and_generate_pr_or_pr_update_event(
265 &git_repo,
266 &repo_ref,
267 commits.last().context("no commits")?,
268 &user_ref,
269 root_proposal.as_ref(),
270 &cover_letter_title_description,
271 &to_try,
272 git_ref.clone(),
273 &signer,
274 &console::Term::stdout(),
275 )
276 .await?;
277 for url in to_try {
278 tried.push(url);
279 }
280 to_try = vec![];
281 if let Some(events) = events {
282 break events;
283 }
284 // fallback to creating user personal-fork on their grasp servers
285 let untried_user_grasp_servers: Vec<String> = user_ref
286 .grasp_list
287 .urls
288 .iter()
289 .map(std::string::ToString::to_string)
290 .filter(|g| {
291 // is a grasp server not in list of tried
292 !is_grasp_server_in_list(g, &tried)
293 })
294 .collect();
295
296 if untried_user_grasp_servers.is_empty()
297 && Interactor::default().choice(
298 PromptChoiceParms::default()
299 .with_prompt("choose alternative git server")
300 .dont_report()
301 .with_choices(vec![
302 "choose grasp server(s)".to_string(),
303 "enter a git repo url with write permission".to_string(),
304 ])
305 .with_default(0),
306 )? == 1
307 {
308 loop {
309 let clone_url = Interactor::default()
310 .input(
311 PromptInputParms::default()
312 .with_prompt("git repo url with write permission"),
313 )?
314 .clone();
315 if CloneUrl::from_str(&clone_url).is_ok() {
316 to_try.push(clone_url);
317 let mut git_ref_or_branch_name = Interactor::default()
318 .input(
319 PromptInputParms::default()
320 .with_prompt("ref / branch name")
321 .with_default(
322 git_ref.unwrap_or("refs/nostr/<event-id>".to_string()),
323 ),
324 )?
325 .clone();
326 if !git_ref_or_branch_name.starts_with("refs/") {
327 git_ref_or_branch_name = format!("refs/heads/{git_ref_or_branch_name}");
328 }
329 git_ref = Some(git_ref_or_branch_name);
330 break;
331 }
332 println!("invalid clone url");
333 }
334 continue;
335 }
336
337 let mut new_grasp_server_events: Vec<Event> = vec![];
338
339 let grasp_servers = if untried_user_grasp_servers.is_empty() {
340 let default_choices: Vec<String> = client
341 .get_grasp_default_set()
342 .iter()
343 .filter(|g| !is_grasp_server_in_list(g, &tried))
344 .cloned()
345 .collect();
346 let selections = vec![true; default_choices.len()]; // all selected by default
347 let grasp_servers = multi_select_with_custom_value(
348 "alternative grasp server(s)",
349 "grasp server",
350 default_choices,
351 selections,
352 normalize_grasp_server_url,
353 )?;
354 show_multi_input_prompt_success("alternative grasp server(s)", &grasp_servers);
355 if grasp_servers.is_empty() {
356 // ask again
357 continue;
358 }
359 let normalised_grasp_servers: Vec<String> = grasp_servers
360 .iter()
361 .filter_map(|g| normalize_grasp_server_url(g).ok())
362 .collect();
363 // if any grasp servers not listed in user grasp list prompt to update
364 let grasp_servers_not_in_user_prefs: Vec<String> = normalised_grasp_servers
365 .iter()
366 .filter(|g| {
367 !user_ref.grasp_list.urls.contains(
368 // unwrap is safe as we constructed g
369 &nostr::Url::parse(&format_grasp_server_url_as_relay_url(g).unwrap())
370 .unwrap(),
371 )
372 })
373 .cloned()
374 .collect();
375 if !grasp_servers_not_in_user_prefs.is_empty()
376 && Interactor::default().confirm(
377 PromptConfirmParms::default()
378 .with_prompt(
379 "add these to your list of prefered grasp servers?".to_string(),
380 )
381 .with_default(true),
382 )?
383 {
384 for g in &normalised_grasp_servers {
385 let as_url = nostr::Url::parse(&format_grasp_server_url_as_relay_url(g)?)?;
386 if !user_ref.grasp_list.urls.contains(&as_url) {
387 user_ref.grasp_list.urls.push(as_url);
388 }
389 }
390 new_grasp_server_events.push(user_ref.grasp_list.to_event(&signer).await?);
391 }
392 normalised_grasp_servers
393 } else {
394 untried_user_grasp_servers
395 };
396 println!(
397 "{} personal-fork so we can push commits to your prefered grasp servers",
398 if user_repo_ref.is_some() {
399 "Updating"
400 } else {
401 "Creating a"
402 },
403 );
404
405 let grasp_servers_as_personal_clone_url: Vec<String> = grasp_servers
406 .iter()
407 .filter_map(|g| {
408 format_grasp_server_url_as_clone_url(
409 g,
410 &user_ref.public_key,
411 &repo_ref.identifier,
412 )
413 .ok()
414 })
415 .collect();
416
417 // create personal-fork / update existing user repo and add these grasp servers
418 let updated_user_repo_ref = {
419 if let Some(mut user_repo_ref) = user_repo_ref {
420 for g in &grasp_servers_as_personal_clone_url {
421 user_repo_ref.add_grasp_server(g)?;
422 }
423 user_repo_ref
424 } else {
425 // clone repo_ref and reset as personal-fork
426 let mut user_repo_ref = repo_ref.clone();
427 user_repo_ref.trusted_maintainer = user_ref.public_key;
428 user_repo_ref.maintainers = vec![user_ref.public_key];
429 user_repo_ref.git_server = vec![];
430 user_repo_ref.relays = vec![];
431 if !user_repo_ref
432 .hashtags
433 .contains(&"personal-fork".to_string())
434 {
435 user_repo_ref.hashtags.push("personal-fork".to_string());
436 }
437 user_repo_ref
438 }
439 };
440 // pubish event to my-relays and my-fork-relays
441 new_grasp_server_events.push(updated_user_repo_ref.to_event(&signer).await?);
442 send_events(
443 &client,
444 Some(git_repo_path),
445 new_grasp_server_events,
446 user_ref.relays.write(),
447 updated_user_repo_ref.relays.clone(),
448 !cli_args.disable_cli_spinners,
449 false,
450 )
451 .await?;
452 user_repo_ref = Some(updated_user_repo_ref);
453 // wait a few seconds
454 let countdown_start = 5;
455 let term = console::Term::stdout();
456 for i in (1..=countdown_start).rev() {
457 term.write_line(
458 format!(
459 "waiting {i}s grasp servers to create your repo before we push your data"
460 )
461 .as_str(),
462 )?;
463 thread::sleep(Duration::new(1, 0)); // Sleep for 1 second
464 term.clear_last_lines(1)?;
465 }
466 term.flush().unwrap(); // Ensure the output is flushed to the terminal
467
468 // add grasp servers to to_try
469 for url in grasp_servers_as_personal_clone_url {
470 to_try.push(url);
471 }
472 // the loop with continue with the grasp servers
473 };
474 println!(
475 "posting {}",
476 if events.iter().any(|e| e.kind.eq(&Kind::GitStatusClosed)) {
477 "proposal revision as new PR event, and a close status for the old patch"
478 } else if events.iter().any(|e| e.kind.eq(&KIND_PULL_REQUEST_UPDATE)) {
479 "proposal revision as PR update event"
480 } else {
481 "proposal as PR event"
482 }
483 );
484 events
485 } else { 208 } else {
486 let events = generate_cover_letter_and_patch_events( 209 let events = generate_cover_letter_and_patch_events(
487 cover_letter_title_description.clone(), 210 cover_letter_title_description.clone(),
diff --git a/src/lib/push.rs b/src/lib/push.rs
index 8cb0212..28692f3 100644
--- a/src/lib/push.rs
+++ b/src/lib/push.rs
@@ -1,31 +1,39 @@
1use std::{ 1use std::{
2 collections::{HashMap, HashSet}, 2 collections::{HashMap, HashSet},
3 str::FromStr,
3 sync::{Arc, Mutex}, 4 sync::{Arc, Mutex},
4 time::Instant, 5 thread,
6 time::{Duration, Instant},
5}; 7};
6 8
7use anyhow::{Context, Result, anyhow}; 9use anyhow::{Context, Result, anyhow, bail};
8use auth_git2::GitAuthenticator; 10use auth_git2::GitAuthenticator;
9use console::Term; 11use console::Term;
10use nostr::{ 12use nostr::{
11 event::{Event, EventBuilder, Kind, Tag, TagStandard, UnsignedEvent}, 13 event::{Event, EventBuilder, Kind, Tag, TagStandard, UnsignedEvent},
12 hashes::sha1::Hash as Sha1Hash, 14 hashes::sha1::Hash as Sha1Hash,
13 key::PublicKey, 15 key::PublicKey,
14 nips::nip10::Marker, 16 nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19Coordinate},
15 signer::NostrSigner, 17 signer::NostrSigner,
16}; 18};
17 19
18use crate::{ 20use crate::{
19 cli_interactor::count_lines_per_msg_vec, 21 cli_interactor::{
20 client::{sign_draft_event, sign_event}, 22 Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms, PromptInputParms,
23 count_lines_per_msg_vec, multi_select_with_custom_value, show_multi_input_prompt_success,
24 },
25 client::{Connect, get_repo_ref_from_cache, send_events, sign_draft_event, sign_event},
21 git::{ 26 git::{
22 Repo, RepoActions, 27 Repo, RepoActions,
23 nostr_url::{CloneUrl, NostrUrlDecoded}, 28 nostr_url::{CloneUrl, NostrUrlDecoded},
24 oid_to_shorthand_string, 29 oid_to_shorthand_string,
25 }, 30 },
26 git_events::generate_unsigned_pr_or_update_event, 31 git_events::{KIND_PULL_REQUEST_UPDATE, generate_unsigned_pr_or_update_event},
27 login::user::UserRef, 32 login::user::UserRef,
28 repo_ref::{RepoRef, is_grasp_server_clone_url, normalize_grasp_server_url}, 33 repo_ref::{
34 RepoRef, format_grasp_server_url_as_clone_url, format_grasp_server_url_as_relay_url,
35 is_grasp_server_clone_url, is_grasp_server_in_list, normalize_grasp_server_url,
36 },
29 utils::{ 37 utils::{
30 Direction, get_short_git_server_name, get_write_protocols_to_try, join_with_and, 38 Direction, get_short_git_server_name, get_write_protocols_to_try, join_with_and,
31 set_protocol_preference, 39 set_protocol_preference,
@@ -364,6 +372,303 @@ impl<'a> PushReporter<'a> {
364} 372}
365 373
366#[allow(clippy::too_many_arguments)] 374#[allow(clippy::too_many_arguments)]
375pub async fn select_servers_push_refs_and_generate_pr_or_pr_update_event(
376 #[cfg(test)] client: &crate::client::MockConnect,
377 #[cfg(not(test))] client: &crate::client::Client,
378 git_repo: &Repo,
379 repo_ref: &RepoRef,
380 tip: &Sha1Hash,
381 user_ref: &mut UserRef,
382 root_proposal: Option<&Event>,
383 title_description_overide: &Option<(String, String)>,
384 signer: &Arc<dyn NostrSigner>,
385 interactive: bool,
386 term: &Term,
387) -> Result<Vec<Event>> {
388 let git_repo_path = git_repo.get_path()?;
389 let mut to_try = vec![];
390 let mut tried = vec![];
391 let repo_grasps = repo_ref.grasp_servers();
392 // if the user already has a fork, or is a maintainer, use those git servers
393 let mut user_repo_ref = get_repo_ref_from_cache(
394 Some(git_repo_path),
395 &Nip19Coordinate {
396 coordinate: Coordinate {
397 kind: nostr::event::Kind::GitRepoAnnouncement,
398 public_key: user_ref.public_key,
399 identifier: repo_ref.identifier.clone(),
400 },
401 relays: vec![],
402 },
403 )
404 .await
405 .ok();
406 if let Some(user_repo_ref) = &user_repo_ref {
407 for url in &user_repo_ref.git_server {
408 if CloneUrl::from_str(url).is_ok() {
409 to_try.push(url.clone());
410 }
411 }
412 }
413 if !to_try.is_empty() || !repo_grasps.is_empty() {
414 println!(
415 "pushing proposal refs to {}",
416 if repo_ref.maintainers.contains(&user_ref.public_key) {
417 "repository git servers"
418 } else if to_try.is_empty() {
419 "repository grasp servers"
420 } else if repo_grasps.is_empty() {
421 "the git servers listed in your fork"
422 } else {
423 "the git servers listed in your fork and repository grasp servers"
424 }
425 );
426 } else {
427 println!(
428 "The repository doesn't list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request."
429 );
430 }
431 // also use repo grasp servers
432 for url in &repo_ref.git_server {
433 if is_grasp_server_in_list(url, &repo_grasps) && !to_try.contains(url) {
434 to_try.push(url.clone());
435 }
436 }
437
438 let mut git_ref = None;
439 let events = loop {
440 let (events, _server_responses) = push_refs_and_generate_pr_or_pr_update_event(
441 git_repo,
442 repo_ref,
443 tip,
444 user_ref,
445 root_proposal,
446 title_description_overide,
447 &to_try,
448 git_ref.clone(),
449 signer,
450 term,
451 )
452 .await?;
453 for url in to_try {
454 tried.push(url);
455 }
456 to_try = vec![];
457 if let Some(events) = events {
458 break events;
459 }
460 // fallback to creating user personal-fork on their grasp servers
461 let untried_user_grasp_servers: Vec<String> = user_ref
462 .grasp_list
463 .urls
464 .iter()
465 .map(std::string::ToString::to_string)
466 .filter(|g| {
467 // is a grasp server not in list of tried
468 !is_grasp_server_in_list(g, &tried)
469 })
470 .collect();
471
472 if untried_user_grasp_servers.is_empty() {
473 if !interactive {
474 if repo_grasps.is_empty() {
475 bail!(
476 "failed to write PR data. nostr repo doesnt lists any grasp servers which allow you to write PR branches. run `ngit send` to select an alternative git server to host your PR diff."
477 )
478 }
479 bail!(
480 "failed to write PR data to git servers associated with this nostr repo. run `ngit send` to select an alternative git server to host your PR diff."
481 )
482 }
483 if Interactor::default().choice(
484 PromptChoiceParms::default()
485 .with_prompt("choose alternative git server")
486 .dont_report()
487 .with_choices(vec![
488 "choose grasp server(s)".to_string(),
489 "enter a git repo url with write permission".to_string(),
490 ])
491 .with_default(0),
492 )? == 1
493 {
494 loop {
495 let clone_url = Interactor::default()
496 .input(
497 PromptInputParms::default()
498 .with_prompt("git repo url with write permission"),
499 )?
500 .clone();
501 if CloneUrl::from_str(&clone_url).is_ok() {
502 to_try.push(clone_url);
503 let mut git_ref_or_branch_name = Interactor::default()
504 .input(
505 PromptInputParms::default()
506 .with_prompt("ref / branch name")
507 .with_default(
508 git_ref.unwrap_or("refs/nostr/<event-id>".to_string()),
509 ),
510 )?
511 .clone();
512 if !git_ref_or_branch_name.starts_with("refs/") {
513 git_ref_or_branch_name = format!("refs/heads/{git_ref_or_branch_name}");
514 }
515 git_ref = Some(git_ref_or_branch_name);
516 break;
517 }
518 println!("invalid clone url");
519 }
520 continue;
521 }
522 }
523
524 let mut new_grasp_server_events: Vec<Event> = vec![];
525
526 let grasp_servers = if untried_user_grasp_servers.is_empty() {
527 let default_choices: Vec<String> = client
528 .get_grasp_default_set()
529 .iter()
530 .filter(|g| !is_grasp_server_in_list(g, &tried))
531 .cloned()
532 .collect();
533 let selections = vec![true; default_choices.len()]; // all selected by default
534 let grasp_servers = multi_select_with_custom_value(
535 "alternative grasp server(s)",
536 "grasp server",
537 default_choices,
538 selections,
539 normalize_grasp_server_url,
540 )?;
541 show_multi_input_prompt_success("alternative grasp server(s)", &grasp_servers);
542 if grasp_servers.is_empty() {
543 // ask again
544 continue;
545 }
546 let normalised_grasp_servers: Vec<String> = grasp_servers
547 .iter()
548 .filter_map(|g| normalize_grasp_server_url(g).ok())
549 .collect();
550 // if any grasp servers not listed in user grasp list prompt to update
551 let grasp_servers_not_in_user_prefs: Vec<String> = normalised_grasp_servers
552 .iter()
553 .filter(|g| {
554 !user_ref.grasp_list.urls.contains(
555 // unwrap is safe as we constructed g
556 &nostr::Url::parse(&format_grasp_server_url_as_relay_url(g).unwrap())
557 .unwrap(),
558 )
559 })
560 .cloned()
561 .collect();
562 if !grasp_servers_not_in_user_prefs.is_empty()
563 && Interactor::default().confirm(
564 PromptConfirmParms::default()
565 .with_prompt(
566 "add these to your list of prefered grasp servers?".to_string(),
567 )
568 .with_default(true),
569 )?
570 {
571 for g in &normalised_grasp_servers {
572 let as_url = nostr::Url::parse(&format_grasp_server_url_as_relay_url(g)?)?;
573 if !user_ref.grasp_list.urls.contains(&as_url) {
574 user_ref.grasp_list.urls.push(as_url);
575 }
576 }
577 new_grasp_server_events.push(user_ref.grasp_list.to_event(signer).await?);
578 }
579 normalised_grasp_servers
580 } else {
581 untried_user_grasp_servers
582 };
583 println!(
584 "{} personal-fork so we can push commits to your prefered grasp servers",
585 if user_repo_ref.is_some() {
586 "Updating"
587 } else {
588 "Creating a"
589 },
590 );
591
592 let grasp_servers_as_personal_clone_url: Vec<String> = grasp_servers
593 .iter()
594 .filter_map(|g| {
595 format_grasp_server_url_as_clone_url(g, &user_ref.public_key, &repo_ref.identifier)
596 .ok()
597 })
598 .collect();
599
600 // create personal-fork / update existing user repo and add these grasp servers
601 let updated_user_repo_ref = {
602 if let Some(mut user_repo_ref) = user_repo_ref {
603 for g in &grasp_servers_as_personal_clone_url {
604 user_repo_ref.add_grasp_server(g)?;
605 }
606 user_repo_ref
607 } else {
608 // clone repo_ref and reset as personal-fork
609 let mut user_repo_ref = repo_ref.clone();
610 user_repo_ref.trusted_maintainer = user_ref.public_key;
611 user_repo_ref.maintainers = vec![user_ref.public_key];
612 user_repo_ref.git_server = vec![];
613 user_repo_ref.relays = vec![];
614 if !user_repo_ref
615 .hashtags
616 .contains(&"personal-fork".to_string())
617 {
618 user_repo_ref.hashtags.push("personal-fork".to_string());
619 }
620 user_repo_ref
621 }
622 };
623 // pubish event to my-relays and my-fork-relays
624 new_grasp_server_events.push(updated_user_repo_ref.to_event(signer).await?);
625 send_events(
626 client,
627 Some(git_repo_path),
628 new_grasp_server_events,
629 user_ref.relays.write(),
630 updated_user_repo_ref.relays.clone(),
631 #[cfg(test)]
632 true,
633 #[cfg(not(test))]
634 false,
635 false,
636 )
637 .await?;
638 user_repo_ref = Some(updated_user_repo_ref);
639 // wait a few seconds
640 let countdown_start = 5;
641 let term = console::Term::stdout();
642 for i in (1..=countdown_start).rev() {
643 term.write_line(
644 format!("waiting {i}s grasp servers to create your repo before we push your data")
645 .as_str(),
646 )?;
647 thread::sleep(Duration::new(1, 0)); // Sleep for 1 second
648 term.clear_last_lines(1)?;
649 }
650 term.flush().unwrap(); // Ensure the output is flushed to the terminal
651
652 // add grasp servers to to_try
653 for url in grasp_servers_as_personal_clone_url {
654 to_try.push(url);
655 }
656 // the loop with continue with the grasp servers
657 };
658 println!(
659 "posting {}",
660 if events.iter().any(|e| e.kind.eq(&Kind::GitStatusClosed)) {
661 "proposal revision as new PR event, and a close status for the old patch"
662 } else if events.iter().any(|e| e.kind.eq(&KIND_PULL_REQUEST_UPDATE)) {
663 "proposal revision as PR update event"
664 } else {
665 "proposal as PR event"
666 }
667 );
668 Ok(events)
669}
670
671#[allow(clippy::too_many_arguments)]
367pub async fn push_refs_and_generate_pr_or_pr_update_event( 672pub async fn push_refs_and_generate_pr_or_pr_update_event(
368 git_repo: &Repo, 673 git_repo: &Repo,
369 repo_ref: &RepoRef, 674 repo_ref: &RepoRef,