upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bin/git_remote_nostr/push.rs126
-rw-r--r--src/lib/client.rs26
-rw-r--r--src/lib/git_events.rs130
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
12use console::Term; 12use console::Term;
13use git::{RepoActions, sha1_to_oid}; 13use git::{RepoActions, sha1_to_oid};
14use git_events::{ 14use 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};
17use git2::{Oid, Repository}; 18use git2::{Oid, Repository};
18use ngit::{ 19use 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};
31use nostr::nips::nip10::Marker; 32use nostr::{event::UnsignedEvent, nips::nip10::Marker};
32use nostr_sdk::{ 33use 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
420async 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
426fn push_to_remote( 522fn 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
31use mockall::*; 31use mockall::*;
32use nostr::{ 32use 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
817pub 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
817pub async fn fetch_public_key(signer: &Arc<dyn NostrSigner>) -> Result<nostr::PublicKey> { 841pub 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 @@
1use std::{str::FromStr, sync::Arc}; 1use std::{str::FromStr, sync::Arc};
2 2
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use nostr::nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}; 4use nostr::{
5 event::UnsignedEvent,
6 nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19},
7};
5use nostr_sdk::{ 8use 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
353pub 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
430fn 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)]
351pub async fn generate_cover_letter_and_patch_events( 459pub 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 },