diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-07-22 17:14:21 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-07-22 17:14:21 +0100 |
| commit | f4e1df4c718a3755ffe50e99946996729f3504e9 (patch) | |
| tree | 9d3d83f42d4d9cf7f18c5451d524a7b995d8053b /src | |
| parent | d1283a6b55826175423bd382a859928e0f92ffe7 (diff) | |
feat(pr): generate pr event > oversized patch
but only for new proposals
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/git_remote_nostr/push.rs | 126 | ||||
| -rw-r--r-- | src/lib/client.rs | 26 | ||||
| -rw-r--r-- | src/lib/git_events.rs | 130 |
3 files changed, 247 insertions, 35 deletions
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 9ff8af0..b9e8571 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs | |||
| @@ -12,12 +12,13 @@ use client::{get_events_from_local_cache, get_state_from_cache, send_events, sig | |||
| 12 | use console::Term; | 12 | use console::Term; |
| 13 | use git::{RepoActions, sha1_to_oid}; | 13 | use git::{RepoActions, sha1_to_oid}; |
| 14 | use git_events::{ | 14 | use git_events::{ |
| 15 | generate_cover_letter_and_patch_events, generate_patch_event, get_commit_id_from_patch, | 15 | generate_cover_letter_and_patch_events, generate_patch_event, generate_unsigned_pr_event, |
| 16 | get_commit_id_from_patch, | ||
| 16 | }; | 17 | }; |
| 17 | use git2::{Oid, Repository}; | 18 | use git2::{Oid, Repository}; |
| 18 | use ngit::{ | 19 | use ngit::{ |
| 19 | cli_interactor::count_lines_per_msg_vec, | 20 | cli_interactor::count_lines_per_msg_vec, |
| 20 | client::{self, get_event_from_cache_by_id}, | 21 | client::{self, get_event_from_cache_by_id, sign_draft_event}, |
| 21 | git::{ | 22 | git::{ |
| 22 | self, | 23 | self, |
| 23 | nostr_url::{CloneUrl, NostrUrlDecoded}, | 24 | nostr_url::{CloneUrl, NostrUrlDecoded}, |
| @@ -25,10 +26,10 @@ use ngit::{ | |||
| 25 | }, | 26 | }, |
| 26 | git_events::{self, event_to_cover_letter, get_event_root}, | 27 | git_events::{self, event_to_cover_letter, get_event_root}, |
| 27 | login::{self, user::UserRef}, | 28 | login::{self, user::UserRef}, |
| 28 | repo_ref::{self, get_repo_config_from_yaml, is_grasp_server}, | 29 | repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url}, |
| 29 | repo_state, | 30 | repo_state, |
| 30 | }; | 31 | }; |
| 31 | use nostr::nips::nip10::Marker; | 32 | use nostr::{event::UnsignedEvent, nips::nip10::Marker}; |
| 32 | use nostr_sdk::{ | 33 | use nostr_sdk::{ |
| 33 | Event, EventBuilder, EventId, Kind, NostrSigner, PublicKey, RelayUrl, Tag, TagStandard, | 34 | Event, EventBuilder, EventId, Kind, NostrSigner, PublicKey, RelayUrl, Tag, TagStandard, |
| 34 | hashes::sha1::Hash as Sha1Hash, | 35 | hashes::sha1::Hash as Sha1Hash, |
| @@ -404,18 +405,11 @@ async fn process_proposal_refspecs( | |||
| 404 | let (mut ahead, _) = | 405 | let (mut ahead, _) = |
| 405 | git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; | 406 | git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; |
| 406 | ahead.reverse(); | 407 | ahead.reverse(); |
| 407 | for patch in generate_cover_letter_and_patch_events( | 408 | for event in |
| 408 | None, | 409 | generate_patches_or_pr_event(git_repo, repo_ref, &ahead, user_ref, signer, term) |
| 409 | git_repo, | 410 | .await? |
| 410 | &ahead, | ||
| 411 | signer, | ||
| 412 | repo_ref, | ||
| 413 | &None, | ||
| 414 | &[], | ||
| 415 | ) | ||
| 416 | .await? | ||
| 417 | { | 411 | { |
| 418 | events.push(patch); | 412 | events.push(event); |
| 419 | } | 413 | } |
| 420 | } | 414 | } |
| 421 | } | 415 | } |
| @@ -423,6 +417,108 @@ async fn process_proposal_refspecs( | |||
| 423 | Ok((events, rejected_proposal_refspecs)) | 417 | Ok((events, rejected_proposal_refspecs)) |
| 424 | } | 418 | } |
| 425 | 419 | ||
| 420 | async fn generate_patches_or_pr_event( | ||
| 421 | git_repo: &Repo, | ||
| 422 | repo_ref: &RepoRef, | ||
| 423 | ahead: &[Sha1Hash], | ||
| 424 | user_ref: &UserRef, | ||
| 425 | signer: &Arc<dyn NostrSigner>, | ||
| 426 | term: &Term, | ||
| 427 | ) -> Result<Vec<Event>> { | ||
| 428 | let mut events: Vec<Event> = vec![]; | ||
| 429 | let use_pr = ahead.iter().any(|commit| { | ||
| 430 | if let Ok(patch) = git_repo.make_patch_from_commit(commit, &None) { | ||
| 431 | patch.len() | ||
| 432 | > ((65 // max recomended patch event size specified in nip34 in kb | ||
| 433 | // allownace for nostr event wrapper (id, pubkey, tags, sig) | ||
| 434 | - 1) * 1024) | ||
| 435 | } else { | ||
| 436 | true | ||
| 437 | } | ||
| 438 | }); | ||
| 439 | |||
| 440 | if use_pr { | ||
| 441 | let repo_grasps = repo_ref.grasp_servers(); | ||
| 442 | let repo_grasp_clone_urls = repo_ref | ||
| 443 | .git_server | ||
| 444 | .iter() | ||
| 445 | .filter(|s| is_grasp_server(s, &repo_grasps)); | ||
| 446 | |||
| 447 | let mut unsigned_pr_event: Option<UnsignedEvent> = None; | ||
| 448 | let mut failed_clone_urls = vec![]; | ||
| 449 | for clone_url in repo_grasp_clone_urls { | ||
| 450 | let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { | ||
| 451 | unsigned_pr_event.clone() | ||
| 452 | } else { | ||
| 453 | generate_unsigned_pr_event( | ||
| 454 | git_repo, | ||
| 455 | repo_ref, | ||
| 456 | &user_ref.public_key, | ||
| 457 | ahead.first().context("no commits to push")?, | ||
| 458 | &[clone_url], | ||
| 459 | &[], | ||
| 460 | )? | ||
| 461 | }; | ||
| 462 | |||
| 463 | let refspec = format!( | ||
| 464 | "{}:refs/nostr/{}", | ||
| 465 | ahead.first().unwrap(), | ||
| 466 | draft_pr_event.id() | ||
| 467 | ); | ||
| 468 | |||
| 469 | if let Err(error) = push_to_remote_url(git_repo, clone_url, &[refspec], term) { | ||
| 470 | failed_clone_urls.push(clone_url); | ||
| 471 | term.write_line( | ||
| 472 | format!( | ||
| 473 | "push: error sending commit data to {}: {error}", | ||
| 474 | normalize_grasp_server_url(clone_url)? | ||
| 475 | ) | ||
| 476 | .as_str(), | ||
| 477 | )?; | ||
| 478 | } else { | ||
| 479 | term.write_line( | ||
| 480 | format!( | ||
| 481 | "push: commit data sent to {}", | ||
| 482 | normalize_grasp_server_url(clone_url)? | ||
| 483 | ) | ||
| 484 | .as_str(), | ||
| 485 | )?; | ||
| 486 | unsigned_pr_event = Some(draft_pr_event); | ||
| 487 | } | ||
| 488 | } | ||
| 489 | if unsigned_pr_event.is_none() { | ||
| 490 | // TODO get fallback grasp servers that aren't in repo_grasps cycle | ||
| 491 | // through until one succeeds TODO create personal-fork | ||
| 492 | // announcement with grasp servers and push, after a few seconds | ||
| 493 | // push ref/nostr/eventid. if one success break out of | ||
| 494 | // for loop and continue | ||
| 495 | } | ||
| 496 | if let Some(unsigned_pr_event) = unsigned_pr_event { | ||
| 497 | let pr_event = | ||
| 498 | sign_draft_event(unsigned_pr_event, signer, "Pull Request".to_string()).await?; | ||
| 499 | events.push(pr_event); | ||
| 500 | } else { | ||
| 501 | bail!("could not find a grasp server that accepts the Pull Request refs"); | ||
| 502 | } | ||
| 503 | } else { | ||
| 504 | for patch in generate_cover_letter_and_patch_events( | ||
| 505 | None, | ||
| 506 | git_repo, | ||
| 507 | ahead, | ||
| 508 | signer, | ||
| 509 | repo_ref, | ||
| 510 | &None, | ||
| 511 | &[], | ||
| 512 | ) | ||
| 513 | .await? | ||
| 514 | { | ||
| 515 | events.push(patch); | ||
| 516 | } | ||
| 517 | } | ||
| 518 | |||
| 519 | Ok(events) | ||
| 520 | } | ||
| 521 | |||
| 426 | fn push_to_remote( | 522 | fn push_to_remote( |
| 427 | git_repo: &Repo, | 523 | git_repo: &Repo, |
| 428 | git_server_url: &str, | 524 | git_server_url: &str, |
diff --git a/src/lib/client.rs b/src/lib/client.rs index 091d68d..3fe2b57 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs | |||
| @@ -31,7 +31,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressState, P | |||
| 31 | use mockall::*; | 31 | use mockall::*; |
| 32 | use nostr::{ | 32 | use nostr::{ |
| 33 | Event, | 33 | Event, |
| 34 | event::{TagKind, TagStandard}, | 34 | event::{TagKind, TagStandard, UnsignedEvent}, |
| 35 | filter::Alphabet, | 35 | filter::Alphabet, |
| 36 | nips::{nip01::Coordinate, nip19::Nip19Coordinate}, | 36 | nips::{nip01::Coordinate, nip19::Nip19Coordinate}, |
| 37 | signer::SignerBackend, | 37 | signer::SignerBackend, |
| @@ -814,6 +814,30 @@ pub async fn sign_event( | |||
| 814 | } | 814 | } |
| 815 | } | 815 | } |
| 816 | 816 | ||
| 817 | pub async fn sign_draft_event( | ||
| 818 | draft_event: UnsignedEvent, | ||
| 819 | signer: &Arc<dyn NostrSigner>, | ||
| 820 | description: String, | ||
| 821 | ) -> Result<nostr::Event> { | ||
| 822 | if signer.backend() == SignerBackend::NostrConnect { | ||
| 823 | let term = console::Term::stderr(); | ||
| 824 | term.write_line(&format!( | ||
| 825 | "signing event ({description}) with remote signer..." | ||
| 826 | ))?; | ||
| 827 | let event = signer | ||
| 828 | .sign_event(draft_event) | ||
| 829 | .await | ||
| 830 | .context("failed to sign event")?; | ||
| 831 | term.clear_last_lines(1)?; | ||
| 832 | Ok(event) | ||
| 833 | } else { | ||
| 834 | signer | ||
| 835 | .sign_event(draft_event) | ||
| 836 | .await | ||
| 837 | .context("failed to sign event") | ||
| 838 | } | ||
| 839 | } | ||
| 840 | |||
| 817 | pub async fn fetch_public_key(signer: &Arc<dyn NostrSigner>) -> Result<nostr::PublicKey> { | 841 | pub async fn fetch_public_key(signer: &Arc<dyn NostrSigner>) -> Result<nostr::PublicKey> { |
| 818 | if signer.backend() == SignerBackend::NostrConnect { | 842 | if signer.backend() == SignerBackend::NostrConnect { |
| 819 | let term = console::Term::stderr(); | 843 | let term = console::Term::stderr(); |
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 09ec040..86b9641 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs | |||
| @@ -1,7 +1,10 @@ | |||
| 1 | use std::{str::FromStr, sync::Arc}; | 1 | use std::{str::FromStr, sync::Arc}; |
| 2 | 2 | ||
| 3 | use anyhow::{Context, Result, bail}; | 3 | use anyhow::{Context, Result, bail}; |
| 4 | use nostr::nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}; | 4 | use nostr::{ |
| 5 | event::UnsignedEvent, | ||
| 6 | nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}, | ||
| 7 | }; | ||
| 5 | use nostr_sdk::{ | 8 | use nostr_sdk::{ |
| 6 | Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind, | 9 | Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind, |
| 7 | TagStandard, hashes::sha1::Hash as Sha1Hash, | 10 | TagStandard, hashes::sha1::Hash as Sha1Hash, |
| @@ -347,6 +350,111 @@ pub fn event_tag_from_nip19_or_hex( | |||
| 347 | } | 350 | } |
| 348 | } | 351 | } |
| 349 | 352 | ||
| 353 | pub fn generate_unsigned_pr_event( | ||
| 354 | git_repo: &Repo, | ||
| 355 | repo_ref: &RepoRef, | ||
| 356 | signing_public_key: &PublicKey, | ||
| 357 | commit: &Sha1Hash, | ||
| 358 | clone_url_hint: &[&str], | ||
| 359 | mentions: &[nostr::Tag], | ||
| 360 | ) -> Result<UnsignedEvent> { | ||
| 361 | let title = git_repo.get_commit_message_summary(commit)?; | ||
| 362 | |||
| 363 | let description = { | ||
| 364 | let mut description = git_repo.get_commit_message(commit)?.trim().to_string(); | ||
| 365 | if let Some(remaining_description) = description.strip_prefix(&title) { | ||
| 366 | description = remaining_description.trim().to_string(); | ||
| 367 | } | ||
| 368 | description | ||
| 369 | }; | ||
| 370 | |||
| 371 | let root_commit = git_repo | ||
| 372 | .get_root_commit() | ||
| 373 | .context("failed to get root commit of the repository")?; | ||
| 374 | |||
| 375 | Ok(EventBuilder::new(KIND_PULL_REQUEST, description) | ||
| 376 | .tags( | ||
| 377 | [ | ||
| 378 | repo_ref | ||
| 379 | .maintainers | ||
| 380 | .iter() | ||
| 381 | .map(|m| { | ||
| 382 | Tag::from_standardized(TagStandard::Coordinate { | ||
| 383 | coordinate: Coordinate { | ||
| 384 | kind: nostr::Kind::GitRepoAnnouncement, | ||
| 385 | public_key: *m, | ||
| 386 | identifier: repo_ref.identifier.to_string(), | ||
| 387 | }, | ||
| 388 | relay_url: repo_ref.relays.first().cloned(), | ||
| 389 | uppercase: false, | ||
| 390 | }) | ||
| 391 | }) | ||
| 392 | .collect::<Vec<Tag>>(), | ||
| 393 | mentions.to_vec(), | ||
| 394 | vec![ | ||
| 395 | Tag::from_standardized(TagStandard::Subject(title.clone())), | ||
| 396 | Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), | ||
| 397 | Tag::custom( | ||
| 398 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")), | ||
| 399 | vec![format!("{commit}")], | ||
| 400 | ), | ||
| 401 | Tag::custom( | ||
| 402 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")), | ||
| 403 | clone_url_hint | ||
| 404 | .iter() | ||
| 405 | .map(|s| s.to_string()) | ||
| 406 | .collect::<Vec<String>>(), | ||
| 407 | ), | ||
| 408 | Tag::custom( | ||
| 409 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), | ||
| 410 | vec![format!("git Pull Request: {}", title.clone())], | ||
| 411 | ), | ||
| 412 | ], | ||
| 413 | if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) | ||
| 414 | { | ||
| 415 | vec![branch_name_tag] | ||
| 416 | } else { | ||
| 417 | vec![] | ||
| 418 | }, | ||
| 419 | repo_ref | ||
| 420 | .maintainers | ||
| 421 | .iter() | ||
| 422 | .map(|pk| Tag::public_key(*pk)) | ||
| 423 | .collect(), | ||
| 424 | ] | ||
| 425 | .concat(), | ||
| 426 | ) | ||
| 427 | .build(*signing_public_key)) | ||
| 428 | } | ||
| 429 | |||
| 430 | fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option<Tag> { | ||
| 431 | if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { | ||
| 432 | if !branch_name.eq("main") | ||
| 433 | && !branch_name.eq("master") | ||
| 434 | && !branch_name.eq("origin/main") | ||
| 435 | && !branch_name.eq("origin/master") | ||
| 436 | { | ||
| 437 | Some(Tag::custom( | ||
| 438 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), | ||
| 439 | vec![ | ||
| 440 | if let Some(branch_name) = branch_name.strip_prefix("pr/") { | ||
| 441 | branch_name.to_string() | ||
| 442 | } else { | ||
| 443 | branch_name | ||
| 444 | } | ||
| 445 | .chars() | ||
| 446 | .take(60) | ||
| 447 | .collect::<String>(), | ||
| 448 | ], | ||
| 449 | )) | ||
| 450 | } else { | ||
| 451 | None | ||
| 452 | } | ||
| 453 | } else { | ||
| 454 | None | ||
| 455 | } | ||
| 456 | } | ||
| 457 | |||
| 350 | #[allow(clippy::too_many_lines)] | 458 | #[allow(clippy::too_many_lines)] |
| 351 | pub async fn generate_cover_letter_and_patch_events( | 459 | pub async fn generate_cover_letter_and_patch_events( |
| 352 | cover_letter_title_description: Option<(String, String)>, | 460 | cover_letter_title_description: Option<(String, String)>, |
| @@ -409,24 +517,8 @@ pub async fn generate_cover_letter_and_patch_events( | |||
| 409 | // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding | 517 | // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding |
| 410 | // a change like this, or the removal of this tag will require the actual branch name to be tracked | 518 | // a change like this, or the removal of this tag will require the actual branch name to be tracked |
| 411 | // so pulling and pushing still work | 519 | // so pulling and pushing still work |
| 412 | if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { | 520 | if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) { |
| 413 | if !branch_name.eq("main") | 521 | vec![branch_name_tag] |
| 414 | && !branch_name.eq("master") | ||
| 415 | && !branch_name.eq("origin/main") | ||
| 416 | && !branch_name.eq("origin/master") | ||
| 417 | { | ||
| 418 | vec![ | ||
| 419 | Tag::custom( | ||
| 420 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), | ||
| 421 | vec![if let Some(branch_name) = branch_name.strip_prefix("pr/") { | ||
| 422 | branch_name.to_string() | ||
| 423 | } else { | ||
| 424 | branch_name | ||
| 425 | }.chars().take(60).collect::<String>()], | ||
| 426 | ), | ||
| 427 | ] | ||
| 428 | } | ||
| 429 | else { vec![] } | ||
| 430 | } else { | 522 | } else { |
| 431 | vec![] | 523 | vec![] |
| 432 | }, | 524 | }, |