diff options
Diffstat (limited to 'src/lib/push.rs')
| -rw-r--r-- | src/lib/push.rs | 319 |
1 files changed, 312 insertions, 7 deletions
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 @@ | |||
| 1 | use std::{ | 1 | use 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 | ||
| 7 | use anyhow::{Context, Result, anyhow}; | 9 | use anyhow::{Context, Result, anyhow, bail}; |
| 8 | use auth_git2::GitAuthenticator; | 10 | use auth_git2::GitAuthenticator; |
| 9 | use console::Term; | 11 | use console::Term; |
| 10 | use nostr::{ | 12 | use 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 | ||
| 18 | use crate::{ | 20 | use 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)] |
| 375 | pub 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)] | ||
| 367 | pub async fn push_refs_and_generate_pr_or_pr_update_event( | 672 | pub 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, |