diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-07-23 08:51:21 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-07-23 08:51:33 +0100 |
| commit | ecfb54e1c89455590f816152b9efb722f0115bf1 (patch) | |
| tree | c1427a5e6c57302e795519c4de92c3e3d722b595 | |
| parent | 698b05be2e48b38a4f268bfabc3562e83e0c1363 (diff) | |
feat(pr): updates and pr as patch revision
issue a pull request update if pushing or force pushing
a pull request
issue a pull request with an e tag for original patch and close status
for the original patch when pushing or force pushing against a patch
when the new commits are too big to be iussed as patches
| -rw-r--r-- | src/bin/git_remote_nostr/push.rs | 155 | ||||
| -rw-r--r-- | src/lib/git_events.rs | 143 |
2 files changed, 220 insertions, 78 deletions
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 61f4f92..596cd68 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs | |||
| @@ -12,8 +12,8 @@ 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, generate_unsigned_pr_event, | 15 | generate_cover_letter_and_patch_events, generate_patch_event, |
| 16 | get_commit_id_from_patch, | 16 | generate_unsigned_pr_or_update_event, get_commit_id_from_patch, |
| 17 | }; | 17 | }; |
| 18 | use git2::{Oid, Repository}; | 18 | use git2::{Oid, Repository}; |
| 19 | use ngit::{ | 19 | use ngit::{ |
| @@ -24,7 +24,7 @@ use ngit::{ | |||
| 24 | nostr_url::{CloneUrl, NostrUrlDecoded}, | 24 | nostr_url::{CloneUrl, NostrUrlDecoded}, |
| 25 | oid_to_shorthand_string, | 25 | oid_to_shorthand_string, |
| 26 | }, | 26 | }, |
| 27 | git_events::{self, event_to_cover_letter, get_event_root}, | 27 | git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root}, |
| 28 | login::{self, user::UserRef}, | 28 | login::{self, user::UserRef}, |
| 29 | repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url}, | 29 | repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url}, |
| 30 | repo_state, | 30 | repo_state, |
| @@ -325,14 +325,14 @@ async fn process_proposal_refspecs( | |||
| 325 | let (mut ahead, _) = | 325 | let (mut ahead, _) = |
| 326 | git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; | 326 | git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; |
| 327 | ahead.reverse(); | 327 | ahead.reverse(); |
| 328 | for patch in generate_cover_letter_and_patch_events( | 328 | for patch in generate_patches_or_pr_event_or_pr_updates( |
| 329 | None, | ||
| 330 | git_repo, | 329 | git_repo, |
| 330 | repo_ref, | ||
| 331 | &ahead, | 331 | &ahead, |
| 332 | user_ref, | ||
| 333 | Some(proposal), | ||
| 332 | signer, | 334 | signer, |
| 333 | repo_ref, | 335 | term, |
| 334 | &Some(proposal.id.to_string()), | ||
| 335 | &[], | ||
| 336 | ) | 336 | ) |
| 337 | .await? | 337 | .await? |
| 338 | { | 338 | { |
| @@ -356,6 +356,23 @@ async fn process_proposal_refspecs( | |||
| 356 | }; | 356 | }; |
| 357 | let mut parent_patch = tip_patch.clone(); | 357 | let mut parent_patch = tip_patch.clone(); |
| 358 | ahead.reverse(); | 358 | ahead.reverse(); |
| 359 | if proposal.kind.eq(&KIND_PULL_REQUEST) | ||
| 360 | || are_commits_too_big_for_patches(git_repo, &ahead) | ||
| 361 | { | ||
| 362 | for event in generate_patches_or_pr_event_or_pr_updates( | ||
| 363 | git_repo, | ||
| 364 | repo_ref, | ||
| 365 | &ahead, | ||
| 366 | user_ref, | ||
| 367 | Some(proposal), | ||
| 368 | signer, | ||
| 369 | term, | ||
| 370 | ) | ||
| 371 | .await? | ||
| 372 | { | ||
| 373 | events.push(event); | ||
| 374 | } | ||
| 375 | } | ||
| 359 | for (i, commit) in ahead.iter().enumerate() { | 376 | for (i, commit) in ahead.iter().enumerate() { |
| 360 | let new_patch = generate_patch_event( | 377 | let new_patch = generate_patch_event( |
| 361 | git_repo, | 378 | git_repo, |
| @@ -405,9 +422,10 @@ async fn process_proposal_refspecs( | |||
| 405 | let (mut ahead, _) = | 422 | let (mut ahead, _) = |
| 406 | git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; | 423 | git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; |
| 407 | ahead.reverse(); | 424 | ahead.reverse(); |
| 408 | for event in | 425 | for event in generate_patches_or_pr_event_or_pr_updates( |
| 409 | generate_patches_or_pr_event(git_repo, repo_ref, &ahead, user_ref, signer, term) | 426 | git_repo, repo_ref, &ahead, user_ref, None, signer, term, |
| 410 | .await? | 427 | ) |
| 428 | .await? | ||
| 411 | { | 429 | { |
| 412 | events.push(event); | 430 | events.push(event); |
| 413 | } | 431 | } |
| @@ -417,25 +435,32 @@ async fn process_proposal_refspecs( | |||
| 417 | Ok((events, rejected_proposal_refspecs)) | 435 | Ok((events, rejected_proposal_refspecs)) |
| 418 | } | 436 | } |
| 419 | 437 | ||
| 420 | async fn generate_patches_or_pr_event( | 438 | fn are_commits_too_big_for_patches(git_repo: &Repo, commits: &[Sha1Hash]) -> bool { |
| 439 | commits.iter().any(|commit| { | ||
| 440 | if let Ok(patch) = git_repo.make_patch_from_commit(commit, &None) { | ||
| 441 | patch.len() | ||
| 442 | > ((65 // max recomended patch event size specified in nip34 in kb | ||
| 443 | // allownace for nostr event wrapper (id, pubkey, tags, sig) | ||
| 444 | - 1) * 1024) | ||
| 445 | } else { | ||
| 446 | true | ||
| 447 | } | ||
| 448 | }) | ||
| 449 | } | ||
| 450 | |||
| 451 | #[allow(clippy::too_many_lines)] | ||
| 452 | async fn generate_patches_or_pr_event_or_pr_updates( | ||
| 421 | git_repo: &Repo, | 453 | git_repo: &Repo, |
| 422 | repo_ref: &RepoRef, | 454 | repo_ref: &RepoRef, |
| 423 | ahead: &[Sha1Hash], | 455 | ahead: &[Sha1Hash], |
| 424 | user_ref: &UserRef, | 456 | user_ref: &UserRef, |
| 457 | root_proposal: Option<&Event>, | ||
| 425 | signer: &Arc<dyn NostrSigner>, | 458 | signer: &Arc<dyn NostrSigner>, |
| 426 | term: &Term, | 459 | term: &Term, |
| 427 | ) -> Result<Vec<Event>> { | 460 | ) -> Result<Vec<Event>> { |
| 428 | let mut events: Vec<Event> = vec![]; | 461 | let mut events: Vec<Event> = vec![]; |
| 429 | let use_pr = ahead.iter().any(|commit| { | 462 | let use_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)) |
| 430 | if let Ok(patch) = git_repo.make_patch_from_commit(commit, &None) { | 463 | || are_commits_too_big_for_patches(git_repo, ahead); |
| 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 | 464 | ||
| 440 | if use_pr { | 465 | if use_pr { |
| 441 | let repo_grasps = repo_ref.grasp_servers(); | 466 | let repo_grasps = repo_ref.grasp_servers(); |
| @@ -450,10 +475,11 @@ async fn generate_patches_or_pr_event( | |||
| 450 | let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { | 475 | let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { |
| 451 | unsigned_pr_event.clone() | 476 | unsigned_pr_event.clone() |
| 452 | } else { | 477 | } else { |
| 453 | generate_unsigned_pr_event( | 478 | generate_unsigned_pr_or_update_event( |
| 454 | git_repo, | 479 | git_repo, |
| 455 | repo_ref, | 480 | repo_ref, |
| 456 | &user_ref.public_key, | 481 | &user_ref.public_key, |
| 482 | root_proposal, | ||
| 457 | ahead.first().context("no commits to push")?, | 483 | ahead.first().context("no commits to push")?, |
| 458 | &[clone_url], | 484 | &[clone_url], |
| 459 | &[], | 485 | &[], |
| @@ -494,9 +520,30 @@ async fn generate_patches_or_pr_event( | |||
| 494 | // for loop and continue | 520 | // for loop and continue |
| 495 | } | 521 | } |
| 496 | if let Some(unsigned_pr_event) = unsigned_pr_event { | 522 | if let Some(unsigned_pr_event) = unsigned_pr_event { |
| 497 | let pr_event = | 523 | let pr_event = sign_draft_event( |
| 498 | sign_draft_event(unsigned_pr_event, signer, "Pull Request".to_string()).await?; | 524 | unsigned_pr_event, |
| 525 | signer, | ||
| 526 | if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { | ||
| 527 | "Pull Request Replacing Original Patch" | ||
| 528 | } else if root_proposal.is_some() { | ||
| 529 | "Pull Request Update" | ||
| 530 | } else { | ||
| 531 | "Pull Request" | ||
| 532 | } | ||
| 533 | .to_string(), | ||
| 534 | ) | ||
| 535 | .await?; | ||
| 499 | events.push(pr_event); | 536 | events.push(pr_event); |
| 537 | if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { | ||
| 538 | events.push( | ||
| 539 | create_close_status_for_original_patch( | ||
| 540 | signer, | ||
| 541 | repo_ref, | ||
| 542 | root_proposal.unwrap(), | ||
| 543 | ) | ||
| 544 | .await?, | ||
| 545 | ); | ||
| 546 | } | ||
| 500 | } else { | 547 | } else { |
| 501 | bail!("could not find a grasp server that accepts the Pull Request refs"); | 548 | bail!("could not find a grasp server that accepts the Pull Request refs"); |
| 502 | } | 549 | } |
| @@ -507,7 +554,7 @@ async fn generate_patches_or_pr_event( | |||
| 507 | ahead, | 554 | ahead, |
| 508 | signer, | 555 | signer, |
| 509 | repo_ref, | 556 | repo_ref, |
| 510 | &None, | 557 | &root_proposal.map(|proposal| proposal.id.to_string()), |
| 511 | &[], | 558 | &[], |
| 512 | ) | 559 | ) |
| 513 | .await? | 560 | .await? |
| @@ -1487,6 +1534,62 @@ async fn create_merge_status( | |||
| 1487 | .await | 1534 | .await |
| 1488 | } | 1535 | } |
| 1489 | 1536 | ||
| 1537 | async fn create_close_status_for_original_patch( | ||
| 1538 | signer: &Arc<dyn NostrSigner>, | ||
| 1539 | repo_ref: &RepoRef, | ||
| 1540 | proposal: &Event, | ||
| 1541 | ) -> Result<Event> { | ||
| 1542 | let mut public_keys = repo_ref | ||
| 1543 | .maintainers | ||
| 1544 | .iter() | ||
| 1545 | .copied() | ||
| 1546 | .collect::<HashSet<PublicKey>>(); | ||
| 1547 | public_keys.insert(proposal.pubkey); | ||
| 1548 | |||
| 1549 | sign_event( | ||
| 1550 | EventBuilder::new(nostr::event::Kind::GitStatusClosed, String::new()).tags( | ||
| 1551 | [ | ||
| 1552 | vec![ | ||
| 1553 | Tag::custom( | ||
| 1554 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), | ||
| 1555 | vec![ | ||
| 1556 | "Git patch closed as forthcoming update is too large. Replacing with Pull Request" | ||
| 1557 | .to_string(), | ||
| 1558 | ], | ||
| 1559 | ), | ||
| 1560 | Tag::from_standardized(nostr::TagStandard::Event { | ||
| 1561 | event_id: proposal.id, | ||
| 1562 | relay_url: repo_ref.relays.first().cloned(), | ||
| 1563 | marker: Some(Marker::Root), | ||
| 1564 | public_key: None, | ||
| 1565 | uppercase: false, | ||
| 1566 | }), | ||
| 1567 | ], | ||
| 1568 | public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(), | ||
| 1569 | repo_ref | ||
| 1570 | .coordinates() | ||
| 1571 | .iter() | ||
| 1572 | .map(|c| { | ||
| 1573 | Tag::from_standardized(TagStandard::Coordinate { | ||
| 1574 | coordinate: c.coordinate.clone(), | ||
| 1575 | relay_url: c.relays.first().cloned(), | ||
| 1576 | uppercase: false, | ||
| 1577 | }) | ||
| 1578 | }) | ||
| 1579 | .collect::<Vec<Tag>>(), | ||
| 1580 | vec![ | ||
| 1581 | Tag::from_standardized(nostr::TagStandard::Reference( | ||
| 1582 | repo_ref.root_commit.to_string(), | ||
| 1583 | )), | ||
| 1584 | ], | ||
| 1585 | ] | ||
| 1586 | .concat(), | ||
| 1587 | ), | ||
| 1588 | signer, | ||
| 1589 | "close status for original patch".to_string(), | ||
| 1590 | ) | ||
| 1591 | .await | ||
| 1592 | } | ||
| 1490 | async fn get_proposal_and_revision_root_from_patch( | 1593 | async fn get_proposal_and_revision_root_from_patch( |
| 1491 | git_repo: &Repo, | 1594 | git_repo: &Repo, |
| 1492 | patch: &Event, | 1595 | patch: &Event, |
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 86b9641..7bca63b 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs | |||
| @@ -350,10 +350,11 @@ pub fn event_tag_from_nip19_or_hex( | |||
| 350 | } | 350 | } |
| 351 | } | 351 | } |
| 352 | 352 | ||
| 353 | pub fn generate_unsigned_pr_event( | 353 | pub fn generate_unsigned_pr_or_update_event( |
| 354 | git_repo: &Repo, | 354 | git_repo: &Repo, |
| 355 | repo_ref: &RepoRef, | 355 | repo_ref: &RepoRef, |
| 356 | signing_public_key: &PublicKey, | 356 | signing_public_key: &PublicKey, |
| 357 | root_proposal: Option<&Event>, | ||
| 357 | commit: &Sha1Hash, | 358 | commit: &Sha1Hash, |
| 358 | clone_url_hint: &[&str], | 359 | clone_url_hint: &[&str], |
| 359 | mentions: &[nostr::Tag], | 360 | mentions: &[nostr::Tag], |
| @@ -372,59 +373,97 @@ pub fn generate_unsigned_pr_event( | |||
| 372 | .get_root_commit() | 373 | .get_root_commit() |
| 373 | .context("failed to get root commit of the repository")?; | 374 | .context("failed to get root commit of the repository")?; |
| 374 | 375 | ||
| 375 | Ok(EventBuilder::new(KIND_PULL_REQUEST, description) | 376 | Ok(if root_proposal.is_some() { |
| 376 | .tags( | 377 | EventBuilder::new(KIND_PULL_REQUEST_UPDATE, "") |
| 377 | [ | 378 | } else { |
| 378 | repo_ref | 379 | EventBuilder::new(KIND_PULL_REQUEST, description) |
| 379 | .maintainers | 380 | } |
| 380 | .iter() | 381 | .tags( |
| 381 | .map(|m| { | 382 | [ |
| 382 | Tag::from_standardized(TagStandard::Coordinate { | 383 | repo_ref |
| 383 | coordinate: Coordinate { | 384 | .maintainers |
| 384 | kind: nostr::Kind::GitRepoAnnouncement, | 385 | .iter() |
| 385 | public_key: *m, | 386 | .map(|m| { |
| 386 | identifier: repo_ref.identifier.to_string(), | 387 | Tag::from_standardized(TagStandard::Coordinate { |
| 387 | }, | 388 | coordinate: Coordinate { |
| 388 | relay_url: repo_ref.relays.first().cloned(), | 389 | kind: nostr::Kind::GitRepoAnnouncement, |
| 389 | uppercase: false, | 390 | public_key: *m, |
| 390 | }) | 391 | identifier: repo_ref.identifier.to_string(), |
| 392 | }, | ||
| 393 | relay_url: repo_ref.relays.first().cloned(), | ||
| 394 | uppercase: false, | ||
| 391 | }) | 395 | }) |
| 392 | .collect::<Vec<Tag>>(), | 396 | }) |
| 393 | mentions.to_vec(), | 397 | .collect::<Vec<Tag>>(), |
| 394 | vec![ | 398 | mentions.to_vec(), |
| 395 | Tag::from_standardized(TagStandard::Subject(title.clone())), | 399 | if let Some(root_proposal) = root_proposal { |
| 396 | Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), | 400 | [ |
| 397 | Tag::custom( | 401 | vec![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")), | 402 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), |
| 410 | vec![format!("git Pull Request: {}", title.clone())], | 403 | vec![format!("git Pull Request Update")], |
| 411 | ), | 404 | )], |
| 412 | ], | 405 | if root_proposal.kind.eq(&KIND_PULL_REQUEST) { |
| 413 | if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) | 406 | vec![ |
| 414 | { | 407 | Tag::custom( |
| 415 | vec![branch_name_tag] | 408 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("E")), |
| 416 | } else { | 409 | vec![root_proposal.id], |
| 417 | vec![] | 410 | ), |
| 418 | }, | 411 | Tag::custom( |
| 419 | repo_ref | 412 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("P")), |
| 420 | .maintainers | 413 | vec![root_proposal.pubkey], |
| 421 | .iter() | 414 | ), |
| 422 | .map(|pk| Tag::public_key(*pk)) | 415 | ] |
| 423 | .collect(), | 416 | } else { |
| 424 | ] | 417 | // root proposal is a Patch - so use e tag per nip34 spec |
| 425 | .concat(), | 418 | vec![Tag::custom( |
| 426 | ) | 419 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("e")), |
| 427 | .build(*signing_public_key)) | 420 | vec![root_proposal.id], |
| 421 | )] | ||
| 422 | }, | ||
| 423 | ] | ||
| 424 | .concat() | ||
| 425 | } else { | ||
| 426 | [ | ||
| 427 | vec![ | ||
| 428 | Tag::from_standardized(TagStandard::Subject(title.clone())), | ||
| 429 | Tag::custom( | ||
| 430 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), | ||
| 431 | vec![format!("git Pull Request: {}", title.clone())], | ||
| 432 | ), | ||
| 433 | ], | ||
| 434 | if let Some(branch_name_tag) = | ||
| 435 | make_branch_name_tag_from_check_out_branch(git_repo) | ||
| 436 | { | ||
| 437 | vec![branch_name_tag] | ||
| 438 | } else { | ||
| 439 | vec![] | ||
| 440 | }, | ||
| 441 | ] | ||
| 442 | .concat() | ||
| 443 | }, | ||
| 444 | vec![ | ||
| 445 | Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), | ||
| 446 | Tag::custom( | ||
| 447 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")), | ||
| 448 | vec![format!("{commit}")], | ||
| 449 | ), | ||
| 450 | Tag::custom( | ||
| 451 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")), | ||
| 452 | clone_url_hint | ||
| 453 | .iter() | ||
| 454 | .map(|s| s.to_string()) | ||
| 455 | .collect::<Vec<String>>(), | ||
| 456 | ), | ||
| 457 | ], | ||
| 458 | repo_ref | ||
| 459 | .maintainers | ||
| 460 | .iter() | ||
| 461 | .map(|pk| Tag::public_key(*pk)) | ||
| 462 | .collect(), | ||
| 463 | ] | ||
| 464 | .concat(), | ||
| 465 | ) | ||
| 466 | .build(*signing_public_key)) | ||
| 428 | } | 467 | } |
| 429 | 468 | ||
| 430 | fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option<Tag> { | 469 | fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option<Tag> { |