From a625be66cfbced5f96cb0123a286937543e7273c Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 1 Aug 2025 09:39:36 +0100 Subject: refactor: move patch size evaluation fn to lib so we can use it in ngit as well as remote helper --- src/bin/git_remote_nostr/push.rs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) (limited to 'src/bin/git_remote_nostr') diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 9ba7c30..38b6fc4 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -357,7 +357,7 @@ async fn process_proposal_refspecs( ); } if proposal.kind.eq(&KIND_PULL_REQUEST) - || are_commits_too_big_for_patches(git_repo, &ahead) + || git_repo.are_commits_too_big_for_patches(&ahead) { for event in generate_patches_or_pr_event_or_pr_updates( git_repo, @@ -441,19 +441,6 @@ async fn process_proposal_refspecs( Ok((events, rejected_proposal_refspecs)) } -fn are_commits_too_big_for_patches(git_repo: &Repo, commits: &[Sha1Hash]) -> bool { - commits.iter().any(|commit| { - if let Ok(patch) = git_repo.make_patch_from_commit(commit, &None) { - patch.len() - > ((65 // max recomended patch event size specified in nip34 in kb - // allownace for nostr event wrapper (id, pubkey, tags, sig) - - 1) * 1024) - } else { - true - } - }) -} - #[allow(clippy::too_many_lines)] async fn generate_patches_or_pr_event_or_pr_updates( git_repo: &Repo, @@ -466,7 +453,7 @@ async fn generate_patches_or_pr_event_or_pr_updates( ) -> Result> { let mut events: Vec = vec![]; let use_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)) - || are_commits_too_big_for_patches(git_repo, ahead); + || git_repo.are_commits_too_big_for_patches(ahead); if use_pr { let repo_grasps = repo_ref.grasp_servers(); -- cgit v1.2.3 From 3d04fb224b68187a67b9db0a37f662b5c5382f1e Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 4 Aug 2025 08:12:51 +0100 Subject: refactor: abstract pr event generation & ref push so that we can use it in `ngit send` --- src/bin/git_remote_nostr/push.rs | 206 ++++++++++++++++++++++----------------- 1 file changed, 115 insertions(+), 91 deletions(-) (limited to 'src/bin/git_remote_nostr') diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 38b6fc4..3478608 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -452,101 +452,28 @@ async fn generate_patches_or_pr_event_or_pr_updates( term: &Term, ) -> Result> { let mut events: Vec = vec![]; - let use_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)) - || git_repo.are_commits_too_big_for_patches(ahead); + let parent_is_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)); + let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead); if use_pr { - let repo_grasps = repo_ref.grasp_servers(); - let repo_grasp_clone_urls = repo_ref - .git_server - .iter() - .filter(|s| is_grasp_server(s, &repo_grasps)); - - let mut unsigned_pr_event: Option = None; - let mut failed_clone_urls = vec![]; - for clone_url in repo_grasp_clone_urls { - let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { - unsigned_pr_event.clone() - } else { - generate_unsigned_pr_or_update_event( - git_repo, - repo_ref, - &user_ref.public_key, - root_proposal, - ahead.first().context("no commits to push")?, - &[clone_url], - &[], - )? - }; - - let refspec = format!( - "{}:refs/nostr/{}", - ahead.first().unwrap(), - draft_pr_event.id() - ); - - if let Err(error) = push_to_remote_url(git_repo, clone_url, &[refspec], term) { - failed_clone_urls.push(clone_url); - term.write_line( - format!( - "push: error sending commit data to {}: {error}", - normalize_grasp_server_url(clone_url)? - ) - .as_str(), - )?; + for event in push_refs_and_generate_pr_or_pr_update_event( + git_repo, + repo_ref, + ahead.first().context("no commits to push")?, + user_ref, + root_proposal, + signer, + term, + ) + .await.context( + if parent_is_pr { + "couldn't generate PR update event" } else { - term.write_line( - format!( - "push: commit data sent to {}", - normalize_grasp_server_url(clone_url)? - ) - .as_str(), - )?; - unsigned_pr_event = Some(draft_pr_event); + "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." } - } - if unsigned_pr_event.is_none() { - bail!( - "a commit in your proposal is too big for a nostr patch. 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." - ); - - // TODO get grasp_default_set servers that aren't in repo_grasps - // cycle through until one succeeds TODO create - // personal-fork announcement with grasp servers and - // push, after a few seconds push ref/nostr/eventid. if - // one success break out of for loop and continue - } - if let Some(unsigned_pr_event) = unsigned_pr_event { - let pr_event = sign_draft_event( - unsigned_pr_event, - signer, - if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { - "Pull Request Replacing Original Patch" - } else if root_proposal.is_some() { - "Pull Request Update" - } else { - "Pull Request" - } - .to_string(), - ) - .await?; - events.push(pr_event); - if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { - events.push( - create_close_status_for_original_patch( - signer, - repo_ref, - root_proposal.unwrap(), - ) - .await?, - ); - } - } else { - bail!( - "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" - ); - // TODO suggest `ngit send` where user could specify their own clone - // url to push to once that feature is added + )? + { + events.push(event); } } else { for patch in generate_cover_letter_and_patch_events( @@ -567,6 +494,103 @@ async fn generate_patches_or_pr_event_or_pr_updates( Ok(events) } +async fn push_refs_and_generate_pr_or_pr_update_event( + git_repo: &Repo, + repo_ref: &RepoRef, + tip: &Sha1Hash, + user_ref: &UserRef, + root_proposal: Option<&Event>, + signer: &Arc, + term: &Term, +) -> Result> { + let mut events: Vec = vec![]; + let repo_grasps = repo_ref.grasp_servers(); + let repo_grasp_clone_urls = repo_ref + .git_server + .iter() + .filter(|s| is_grasp_server(s, &repo_grasps)); + + let mut unsigned_pr_event: Option = None; + let mut failed_clone_urls = vec![]; + for clone_url in repo_grasp_clone_urls { + let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { + unsigned_pr_event.clone() + } else { + generate_unsigned_pr_or_update_event( + git_repo, + repo_ref, + &user_ref.public_key, + root_proposal, + tip, + &[clone_url], + &[], + )? + }; + + let refspec = format!("{}:refs/nostr/{}", tip, draft_pr_event.id()); + + if let Err(error) = push_to_remote_url(git_repo, clone_url, &[refspec], term) { + failed_clone_urls.push(clone_url); + term.write_line( + format!( + "push: error sending commit data to {}: {error}", + normalize_grasp_server_url(clone_url)? + ) + .as_str(), + )?; + } else { + term.write_line( + format!( + "push: commit data sent to {}", + normalize_grasp_server_url(clone_url)? + ) + .as_str(), + )?; + unsigned_pr_event = Some(draft_pr_event); + } + } + if unsigned_pr_event.is_none() { + bail!( + "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." + ); + + // TODO get grasp_default_set servers that aren't in repo_grasps + // cycle through until one succeeds TODO create + // personal-fork announcement with grasp servers and + // push, after a few seconds push ref/nostr/eventid. if + // one success break out of for loop and continue + } + if let Some(unsigned_pr_event) = unsigned_pr_event { + let pr_event = sign_draft_event( + unsigned_pr_event, + signer, + if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { + "Pull Request Replacing Original Patch" + } else if root_proposal.is_some() { + "Pull Request Update" + } else { + "Pull Request" + } + .to_string(), + ) + .await?; + events.push(pr_event); + if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { + events.push( + create_close_status_for_original_patch(signer, repo_ref, root_proposal.unwrap()) + .await?, + ); + } + } else { + bail!( + "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" + ); + // TODO suggest `ngit send` where user could specify their own clone + // url to push to once that feature is added + } + Ok(events) +} + type HashMapUrlRefspecs = HashMap>; #[allow(clippy::too_many_lines)] -- cgit v1.2.3 From f76fe63da5f2c2f85215e86c8ecc63eda7c93902 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 4 Aug 2025 08:50:54 +0100 Subject: refactor: move generate pr event fn into lib for future use in `ngit send` --- src/bin/git_remote_nostr/push.rs | 164 ++------------------------------------ src/lib/push.rs | 168 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 172 insertions(+), 160 deletions(-) (limited to 'src/bin/git_remote_nostr') diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 3478608..e588a5a 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -10,25 +10,24 @@ use client::{get_events_from_local_cache, get_state_from_cache, send_events, sig use console::Term; use git::{RepoActions, sha1_to_oid}; use git_events::{ - generate_cover_letter_and_patch_events, generate_patch_event, - generate_unsigned_pr_or_update_event, get_commit_id_from_patch, + generate_cover_letter_and_patch_events, generate_patch_event, get_commit_id_from_patch, }; use git2::{Oid, Repository}; use ngit::{ - client::{self, get_event_from_cache_by_id, sign_draft_event}, + client::{self, get_event_from_cache_by_id}, git::{self, nostr_url::NostrUrlDecoded}, git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root}, list::list_from_remotes, login::{self, user::UserRef}, - push::{push_to_remote, push_to_remote_url}, - repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url}, + push::{push_refs_and_generate_pr_or_pr_update_event, push_to_remote}, + repo_ref::{self, get_repo_config_from_yaml, is_grasp_server}, repo_state, utils::{ find_proposal_and_patches_by_branch_name, get_all_proposals, get_remote_name_by_url, get_short_git_server_name, read_line, }, }; -use nostr::{event::UnsignedEvent, nips::nip10::Marker}; +use nostr::nips::nip10::Marker; use nostr_sdk::{ Event, EventBuilder, EventId, Kind, NostrSigner, PublicKey, RelayUrl, Tag, TagStandard, hashes::sha1::Hash as Sha1Hash, @@ -494,103 +493,6 @@ async fn generate_patches_or_pr_event_or_pr_updates( Ok(events) } -async fn push_refs_and_generate_pr_or_pr_update_event( - git_repo: &Repo, - repo_ref: &RepoRef, - tip: &Sha1Hash, - user_ref: &UserRef, - root_proposal: Option<&Event>, - signer: &Arc, - term: &Term, -) -> Result> { - let mut events: Vec = vec![]; - let repo_grasps = repo_ref.grasp_servers(); - let repo_grasp_clone_urls = repo_ref - .git_server - .iter() - .filter(|s| is_grasp_server(s, &repo_grasps)); - - let mut unsigned_pr_event: Option = None; - let mut failed_clone_urls = vec![]; - for clone_url in repo_grasp_clone_urls { - let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { - unsigned_pr_event.clone() - } else { - generate_unsigned_pr_or_update_event( - git_repo, - repo_ref, - &user_ref.public_key, - root_proposal, - tip, - &[clone_url], - &[], - )? - }; - - let refspec = format!("{}:refs/nostr/{}", tip, draft_pr_event.id()); - - if let Err(error) = push_to_remote_url(git_repo, clone_url, &[refspec], term) { - failed_clone_urls.push(clone_url); - term.write_line( - format!( - "push: error sending commit data to {}: {error}", - normalize_grasp_server_url(clone_url)? - ) - .as_str(), - )?; - } else { - term.write_line( - format!( - "push: commit data sent to {}", - normalize_grasp_server_url(clone_url)? - ) - .as_str(), - )?; - unsigned_pr_event = Some(draft_pr_event); - } - } - if unsigned_pr_event.is_none() { - bail!( - "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." - ); - - // TODO get grasp_default_set servers that aren't in repo_grasps - // cycle through until one succeeds TODO create - // personal-fork announcement with grasp servers and - // push, after a few seconds push ref/nostr/eventid. if - // one success break out of for loop and continue - } - if let Some(unsigned_pr_event) = unsigned_pr_event { - let pr_event = sign_draft_event( - unsigned_pr_event, - signer, - if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { - "Pull Request Replacing Original Patch" - } else if root_proposal.is_some() { - "Pull Request Update" - } else { - "Pull Request" - } - .to_string(), - ) - .await?; - events.push(pr_event); - if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { - events.push( - create_close_status_for_original_patch(signer, repo_ref, root_proposal.unwrap()) - .await?, - ); - } - } else { - bail!( - "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" - ); - // TODO suggest `ngit send` where user could specify their own clone - // url to push to once that feature is added - } - Ok(events) -} - type HashMapUrlRefspecs = HashMap>; #[allow(clippy::too_many_lines)] @@ -1283,62 +1185,6 @@ async fn create_merge_status( .await } -async fn create_close_status_for_original_patch( - signer: &Arc, - repo_ref: &RepoRef, - proposal: &Event, -) -> Result { - let mut public_keys = repo_ref - .maintainers - .iter() - .copied() - .collect::>(); - public_keys.insert(proposal.pubkey); - - sign_event( - EventBuilder::new(nostr::event::Kind::GitStatusClosed, String::new()).tags( - [ - vec![ - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), - vec![ - "Git patch closed as forthcoming update is too large. Replacing with Pull Request" - .to_string(), - ], - ), - Tag::from_standardized(nostr::TagStandard::Event { - event_id: proposal.id, - relay_url: repo_ref.relays.first().cloned(), - marker: Some(Marker::Root), - public_key: None, - uppercase: false, - }), - ], - public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(), - repo_ref - .coordinates() - .iter() - .map(|c| { - Tag::from_standardized(TagStandard::Coordinate { - coordinate: c.coordinate.clone(), - relay_url: c.relays.first().cloned(), - uppercase: false, - }) - }) - .collect::>(), - vec![ - Tag::from_standardized(nostr::TagStandard::Reference( - repo_ref.root_commit.to_string(), - )), - ], - ] - .concat(), - ), - signer, - "close status for original patch".to_string(), - ) - .await -} async fn get_proposal_and_revision_root_from_patch( git_repo: &Repo, patch: &Event, diff --git a/src/lib/push.rs b/src/lib/push.rs index 0d0ec93..1c09555 100644 --- a/src/lib/push.rs +++ b/src/lib/push.rs @@ -1,19 +1,31 @@ use std::{ + collections::HashSet, sync::{Arc, Mutex}, time::Instant, }; -use anyhow::{Result, anyhow}; +use anyhow::{Result, anyhow, bail}; use auth_git2::GitAuthenticator; use console::Term; +use nostr::{ + event::{Event, EventBuilder, Kind, Tag, TagStandard, UnsignedEvent}, + hashes::sha1::Hash as Sha1Hash, + key::PublicKey, + nips::nip10::Marker, + signer::NostrSigner, +}; use crate::{ cli_interactor::count_lines_per_msg_vec, + client::{sign_draft_event, sign_event}, git::{ Repo, nostr_url::{CloneUrl, NostrUrlDecoded}, oid_to_shorthand_string, }, + git_events::generate_unsigned_pr_or_update_event, + login::user::UserRef, + repo_ref::{RepoRef, is_grasp_server, normalize_grasp_server_url}, utils::{ Direction, get_short_git_server_name, get_write_protocols_to_try, join_with_and, set_protocol_preference, @@ -308,3 +320,157 @@ impl<'a> PushReporter<'a> { } } } + +pub async fn push_refs_and_generate_pr_or_pr_update_event( + git_repo: &Repo, + repo_ref: &RepoRef, + tip: &Sha1Hash, + user_ref: &UserRef, + root_proposal: Option<&Event>, + signer: &Arc, + term: &Term, +) -> Result> { + let mut events: Vec = vec![]; + let repo_grasps = repo_ref.grasp_servers(); + let repo_grasp_clone_urls = repo_ref + .git_server + .iter() + .filter(|s| is_grasp_server(s, &repo_grasps)); + + let mut unsigned_pr_event: Option = None; + let mut failed_clone_urls = vec![]; + for clone_url in repo_grasp_clone_urls { + let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { + unsigned_pr_event.clone() + } else { + generate_unsigned_pr_or_update_event( + git_repo, + repo_ref, + &user_ref.public_key, + root_proposal, + tip, + &[clone_url], + &[], + )? + }; + + let refspec = format!("{}:refs/nostr/{}", tip, draft_pr_event.id()); + + if let Err(error) = push_to_remote_url(git_repo, clone_url, &[refspec], term) { + failed_clone_urls.push(clone_url); + term.write_line( + format!( + "push: error sending commit data to {}: {error}", + normalize_grasp_server_url(clone_url)? + ) + .as_str(), + )?; + } else { + term.write_line( + format!( + "push: commit data sent to {}", + normalize_grasp_server_url(clone_url)? + ) + .as_str(), + )?; + unsigned_pr_event = Some(draft_pr_event); + } + } + if unsigned_pr_event.is_none() { + bail!( + "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." + ); + + // TODO get grasp_default_set servers that aren't in repo_grasps + // cycle through until one succeeds TODO create + // personal-fork announcement with grasp servers and + // push, after a few seconds push ref/nostr/eventid. if + // one success break out of for loop and continue + } + if let Some(unsigned_pr_event) = unsigned_pr_event { + let pr_event = sign_draft_event( + unsigned_pr_event, + signer, + if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { + "Pull Request Replacing Original Patch" + } else if root_proposal.is_some() { + "Pull Request Update" + } else { + "Pull Request" + } + .to_string(), + ) + .await?; + events.push(pr_event); + if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { + events.push( + create_close_status_for_original_patch(signer, repo_ref, root_proposal.unwrap()) + .await?, + ); + } + } else { + bail!( + "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" + ); + // TODO suggest `ngit send` where user could specify their own clone + // url to push to once that feature is added + } + Ok(events) +} + +async fn create_close_status_for_original_patch( + signer: &Arc, + repo_ref: &RepoRef, + proposal: &Event, +) -> Result { + let mut public_keys = repo_ref + .maintainers + .iter() + .copied() + .collect::>(); + public_keys.insert(proposal.pubkey); + + sign_event( + EventBuilder::new(nostr::event::Kind::GitStatusClosed, String::new()).tags( + [ + vec![ + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), + vec![ + "Git patch closed as forthcoming update is too large. Replacing with Pull Request" + .to_string(), + ], + ), + Tag::from_standardized(nostr::TagStandard::Event { + event_id: proposal.id, + relay_url: repo_ref.relays.first().cloned(), + marker: Some(Marker::Root), + public_key: None, + uppercase: false, + }), + ], + public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(), + repo_ref + .coordinates() + .iter() + .map(|c| { + Tag::from_standardized(TagStandard::Coordinate { + coordinate: c.coordinate.clone(), + relay_url: c.relays.first().cloned(), + uppercase: false, + }) + }) + .collect::>(), + vec![ + Tag::from_standardized(nostr::TagStandard::Reference( + repo_ref.root_commit.to_string(), + )), + ], + ] + .concat(), + ), + signer, + "close status for original patch".to_string(), + ) + .await +} -- cgit v1.2.3 From f48677bad3f3dabb80992806e0e4c8ad4d45c716 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 4 Aug 2025 11:50:39 +0100 Subject: feat(send): support PR and PR update events send as a PR if the commit would make patches that are too big for nostr events. send as a PR update if the proposal is PR. send as a PR, revising a patch root, if patches would be too big. in tests `get_pretend_proposal_root_event` has to be a actual proposal with a tip, rather than just a cover letter, so we have replaced it. --- src/bin/git_remote_nostr/push.rs | 1 + src/bin/ngit/sub_commands/send.rs | 137 +++++++++++++++++++++++++------------- src/lib/git_events.rs | 10 ++- src/lib/push.rs | 3 + src/lib/utils.rs | 36 +++++++++- test_utils/src/lib.rs | 2 +- tests/ngit_send.rs | 84 +++++++++++++++++------ 7 files changed, 202 insertions(+), 71 deletions(-) (limited to 'src/bin/git_remote_nostr') diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index e588a5a..3967699 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -461,6 +461,7 @@ async fn generate_patches_or_pr_event_or_pr_updates( ahead.first().context("no commits to push")?, user_ref, root_proposal, + &None, signer, term, ) diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index 9f1857f..0aefb03 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs @@ -4,9 +4,11 @@ use anyhow::{Context, Result, bail}; use console::Style; use ngit::{ client::{Params, send_events}, - git_events::{EventRefType, generate_cover_letter_and_patch_events}, + git_events::{EventRefType, KIND_PULL_REQUEST, generate_cover_letter_and_patch_events}, + push::push_refs_and_generate_pr_or_pr_update_event, + utils::proposal_tip_is_pr_or_pr_update, }; -use nostr::{ToBech32, nips::nip19::Nip19Event}; +use nostr::{ToBech32, event::Event, nips::nip19::Nip19Event}; use nostr_sdk::hashes::sha1::Hash as Sha1Hash; use crate::{ @@ -60,12 +62,14 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; } - let (root_proposal_id, mention_tags) = - get_root_proposal_id_and_mentions_from_in_reply_to(git_repo.get_path()?, &args.in_reply_to) + let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?; + + let (root_proposal, mention_tags) = + get_root_proposal_and_mentions_from_in_reply_to(git_repo.get_path()?, &args.in_reply_to) .await?; if let Some(root_ref) = args.in_reply_to.first() { - if root_proposal_id.is_some() { + if root_proposal.is_some() { println!("creating proposal revision for: {root_ref}"); } } @@ -112,7 +116,30 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re &main_tip, )?; - let title = if args.no_cover_letter { + let as_pr = { + if let Some(root_proposal) = &root_proposal { + proposal_tip_is_pr_or_pr_update(git_repo_path, &repo_ref, &root_proposal.id).await? + } else { + false + } + } || git_repo.are_commits_too_big_for_patches(&commits); + + let title = if as_pr { + match &args.title { + Some(t) => Some(t.clone()), + None => { + if root_proposal.is_none() { + Some( + Interactor::default() + .input(PromptInputParms::default().with_prompt("title"))? + .clone(), + ) + } else { + None + } + } + } + } else if args.no_cover_letter { None } else { match &args.title { @@ -142,7 +169,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re t.clone() } else { Interactor::default() - .input(PromptInputParms::default().with_prompt("cover letter description"))? + .input(PromptInputParms::default().with_prompt("description"))? .clone() }, )) @@ -161,42 +188,58 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re client.set_signer(signer.clone()).await; - let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?; - // oldest first commits.reverse(); - let events = generate_cover_letter_and_patch_events( - cover_letter_title_description.clone(), - &git_repo, - &commits, - &signer, - &repo_ref, - &root_proposal_id, - &mention_tags, - ) - .await?; + let events = if as_pr { + push_refs_and_generate_pr_or_pr_update_event( + &git_repo, + &repo_ref, + commits.last().context("no commits")?, + &user_ref, + root_proposal.as_ref(), + &cover_letter_title_description, + &signer, + &console::Term::stdout(), + ) + .await? - println!( - "posting {} patch{} {} a covering letter...", - if cover_letter_title_description.is_none() { - events.len() - } else { - events.len() - 1 - }, - if cover_letter_title_description.is_none() && events.len().eq(&1) - || cover_letter_title_description.is_some() && events.len().eq(&2) - { - "" - } else { - "es" - }, - if cover_letter_title_description.is_none() { - "without" - } else { - "with" - } - ); + // TODO + // - allow specifying clone url and ref + } else { + let events = generate_cover_letter_and_patch_events( + cover_letter_title_description.clone(), + &git_repo, + &commits, + &signer, + &repo_ref, + &root_proposal.as_ref().map(|e| e.id.to_string()), + &mention_tags, + ) + .await?; + + println!( + "posting {} patch{} {} a covering letter...", + if cover_letter_title_description.is_none() { + events.len() + } else { + events.len() - 1 + }, + if cover_letter_title_description.is_none() && events.len().eq(&1) + || cover_letter_title_description.is_some() && events.len().eq(&2) + { + "" + } else { + "es" + }, + if cover_letter_title_description.is_none() { + "without" + } else { + "with" + } + ); + events + }; send_events( &client, @@ -209,7 +252,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re ) .await?; - if root_proposal_id.is_none() { + if root_proposal.is_none() { if let Some(event) = events.first() { let event_bech32 = if let Some(relay) = repo_ref.relays.first() { Nip19Event { @@ -376,11 +419,11 @@ fn summarise_commit_for_selection(git_repo: &Repo, commit: &Sha1Hash) -> Result< )) } -async fn get_root_proposal_id_and_mentions_from_in_reply_to( +async fn get_root_proposal_and_mentions_from_in_reply_to( git_repo_path: &Path, in_reply_to: &[String], -) -> Result<(Option, Vec)> { - let root_proposal_id = if let Some(first) = in_reply_to.first() { +) -> Result<(Option, Vec)> { + let root_proposal = if let Some(first) = in_reply_to.first() { match event_tag_from_nip19_or_hex(first, "in-reply-to", EventRefType::Root, true, false)? .as_standardized() { @@ -398,8 +441,8 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to( .await?; if let Some(first) = events.iter().find(|e| e.id.eq(event_id)) { - if event_is_patch_set_root(first) { - Some(event_id.to_string()) + if event_is_patch_set_root(first) || first.kind.eq(&KIND_PULL_REQUEST) { + Some(first.clone()) } else { None } @@ -415,7 +458,7 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to( let mut mention_tags = vec![]; for (i, reply_to) in in_reply_to.iter().enumerate() { - if i.ne(&0) || root_proposal_id.is_none() { + if i.ne(&0) || root_proposal.is_none() { mention_tags.push( event_tag_from_nip19_or_hex( reply_to, @@ -431,7 +474,7 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to( } } - Ok((root_proposal_id, mention_tags)) + Ok((root_proposal, mention_tags)) } // TODO diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 79f5772..bbfcbea 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs @@ -376,11 +376,13 @@ pub fn event_tag_from_nip19_or_hex( } } +#[allow(clippy::too_many_arguments)] pub fn generate_unsigned_pr_or_update_event( git_repo: &Repo, repo_ref: &RepoRef, signing_public_key: &PublicKey, root_proposal: Option<&Event>, + title_description_overide: &Option<(String, String)>, commit: &Sha1Hash, clone_url_hint: &[&str], mentions: &[nostr::Tag], @@ -395,13 +397,17 @@ pub fn generate_unsigned_pr_or_update_event( None }; - let title = if let Some(cl) = &root_patch_cover_letter { + let title = if let Some((title, _)) = &title_description_overide { + title.clone() + } else if let Some(cl) = &root_patch_cover_letter { cl.title.clone() } else { git_repo.get_commit_message_summary(commit)? }; - let description = if let Some(cl) = &root_patch_cover_letter { + let description = if let Some((_, description)) = &title_description_overide { + description.clone() + } else if let Some(cl) = &root_patch_cover_letter { cl.description.clone() } else { let mut description = git_repo.get_commit_message(commit)?.trim().to_string(); diff --git a/src/lib/push.rs b/src/lib/push.rs index 1c09555..bcd368b 100644 --- a/src/lib/push.rs +++ b/src/lib/push.rs @@ -321,12 +321,14 @@ impl<'a> PushReporter<'a> { } } +#[allow(clippy::too_many_arguments)] pub async fn push_refs_and_generate_pr_or_pr_update_event( git_repo: &Repo, repo_ref: &RepoRef, tip: &Sha1Hash, user_ref: &UserRef, root_proposal: Option<&Event>, + title_description_overide: &Option<(String, String)>, signer: &Arc, term: &Term, ) -> Result> { @@ -348,6 +350,7 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( repo_ref, &user_ref.public_key, root_proposal, + title_description_overide, tip, &[clone_url], &[], diff --git a/src/lib/utils.rs b/src/lib/utils.rs index 431757f..431a14f 100644 --- a/src/lib/utils.rs +++ b/src/lib/utils.rs @@ -3,11 +3,13 @@ use std::{ collections::HashMap, fmt, io::{self, Stdin}, + path::Path, str::FromStr, }; use anyhow::{Context, Result, bail}; use git2::Repository; +use nostr::nips::nip19::ToBech32; use nostr_sdk::{Event, EventId, Kind, PublicKey, Url}; use crate::{ @@ -20,7 +22,8 @@ use crate::{ nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, }, git_events::{ - event_is_revision_root, get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, + KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_revision_root, + get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, is_event_proposal_root_for_branch, status_kinds, }, repo_ref::RepoRef, @@ -187,6 +190,37 @@ pub async fn get_all_proposals( Ok(all_proposals) } +pub async fn proposal_tip_is_pr_or_pr_update( + git_repo_path: &Path, + repo_ref: &RepoRef, + proposal_id: &EventId, +) -> Result { + let commits_events = + get_all_proposal_patch_pr_pr_update_events_from_cache(git_repo_path, repo_ref, proposal_id) + .await + .context(format!( + "cannot get existing proposal events for {}", + proposal_id.to_bech32()? + ))?; + let most_recent_proposal_patch_chain = get_pr_tip_event_or_most_recent_patch_with_ancestors( + commits_events.clone(), + ) + .context(format!( + "cannot find tip from proposal events for {}", + proposal_id.to_bech32()?, + ))?; + + Ok([KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains( + &most_recent_proposal_patch_chain + .first() + .context(format!( + "cannot find any proposal events for {}", + proposal_id.to_bech32()? + ))? + .kind, + )) +} + pub fn find_proposal_and_patches_by_branch_name<'a>( refstr: &'a str, proposals: &'a HashMap)>, diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 3ae004f..12cac76 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -210,7 +210,7 @@ pub fn generate_repo_ref_event_with_git_server_with_keys( } /// enough to fool event_is_patch_set_root pub fn get_pretend_proposal_root_event() -> nostr::Event { - serde_json::from_str(r#"{"id":"431e58eb8e1b4e20292d1d5bbe81d5cfb042e1bc165de32eddfdd52245a4cce4","pubkey":"f53e4bcd7a9cdef049cf6467d638a1321958acd3b71eb09823fd6fadb023d768","created_at":1721404213,"kind":1617,"tags":[["a","30617:ba882566eff14f3baa976103998c452d27fe95b65a796a6a9f92628bced76fe5:9ee507fc4357d7ee16a5d8901bedcd103f23c17d-consider-it-random"],["a","30617:f53e4bcd7a9cdef049cf6467d638a1321958acd3b71eb09823fd6fadb023d768:9ee507fc4357d7ee16a5d8901bedcd103f23c17d-consider-it-random"],["r","9ee507fc4357d7ee16a5d8901bedcd103f23c17d"],["t","cover-letter"],["alt","git patch cover letter: exampletitle"],["t","root"],["e","8cb75aa4cda10a3a0f3242dc49d36159d30b3185bf63414cf6ce17f5c14a73b1","","mention"],["branch-name","feature"],["p","ba882566eff14f3baa976103998c452d27fe95b65a796a6a9f92628bced76fe5"],["p","f53e4bcd7a9cdef049cf6467d638a1321958acd3b71eb09823fd6fadb023d768"]],"content":"From fe973a840fba2a8ab37dd505c154854a69a6505c Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/2] exampletitle\n\nexampledescription","sig":"37d5b2338bf9fd9d598e6494ae88af9a8dbd52330cfe9d025ee55e35e2f3f55e931ba039d9f7fed8e6fc40206e47619a24f730f8eddc2a07ccfb3988a5005170"}"#).unwrap() + serde_json::from_str(r#"{"id":"000c104861e34a453481ab23e7de21a6baf475b394479705363b035936732528","pubkey":"f53e4bcd7a9cdef049cf6467d638a1321958acd3b71eb09823fd6fadb023d768","created_at":1754322009,"kind":1617,"tags":[["a","30617:f53e4bcd7a9cdef049cf6467d638a1321958acd3b71eb09823fd6fadb023d768:9ee507fc4357d7ee16a5d8901bedcd103f23c17d-consider-it-random","ws://localhost:8055"],["a","30617:ba882566eff14f3baa976103998c452d27fe95b65a796a6a9f92628bced76fe5:9ee507fc4357d7ee16a5d8901bedcd103f23c17d-consider-it-random","ws://localhost:8055"],["r","9ee507fc4357d7ee16a5d8901bedcd103f23c17d"],["r","232efb37ebc67692c9e9ff58b83c0d3d63971a0a"],["alt","git patch: add t3.md"],["t","root"],["branch-name","feature"],["p","ba882566eff14f3baa976103998c452d27fe95b65a796a6a9f92628bced76fe5"],["commit","232efb37ebc67692c9e9ff58b83c0d3d63971a0a"],["parent-commit","431b84edc0d2fa118d63faa3c2db9c73d630a5ae"],["commit-pgp-sig",""],["description","add t3.md"],["author","Joe Bloggs","joe.bloggs@pm.me","0","0"],["committer","Joe Bloggs","joe.bloggs@pm.me","0","0"]],"content":"From 232efb37ebc67692c9e9ff58b83c0d3d63971a0a Mon Sep 17 00:00:00 2001\nFrom: Joe Bloggs \nDate: Thu, 1 Jan 1970 00:00:00 +0000\nSubject: [PATCH 1/2] add t3.md\n\n---\n t3.md | 1 +\n 1 file changed, 1 insertion(+)\n create mode 100644 t3.md\n\ndiff --git a/t3.md b/t3.md\nnew file mode 100644\nindex 0000000..f0eec86\n--- /dev/null\n+++ b/t3.md\n@@ -0,0 +1 @@\n+some content\n\\ No newline at end of file\n--\nlibgit2 1.9.1\n\n","sig":"65577fea803ea464bb073273a3fbfbdb5bfdaa64fb3b1d029ee8f3729fde051ad90610d08e441335f365b6c1d6f2270909bc37d12433ca82f0b2928b7a503e31"}"#).unwrap() } /// wrapper for a cli testing tool - currently wraps rexpect and dialoguer diff --git a/tests/ngit_send.rs b/tests/ngit_send.rs index ec72667..e128bd9 100644 --- a/tests/ngit_send.rs +++ b/tests/ngit_send.rs @@ -37,6 +37,25 @@ mod when_commits_behind_ask_to_proceed { Ok(test_repo) } + fn create_relay_51() -> Result> { + Ok(Relay::new( + 8051, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + generate_repo_ref_event(), + ], + )?; + Ok(()) + }), + )) + } + fn expect_confirm_prompt(p: &mut CliTester) -> Result { p.expect("fetching updates...\r\n")?; p.expect_eventually("\r\n")?; // may be 'no updates' or some updates @@ -49,37 +68,62 @@ mod when_commits_behind_ask_to_proceed { ) } - #[test] - fn asked_with_default_no() -> Result<()> { + #[tokio::test] + #[serial] + async fn asked_with_default_no() -> Result<()> { let test_repo = prep_test_repo()?; + let mut r51 = create_relay_51()?; + // // check relay had the right number of events + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]); + expect_confirm_prompt(&mut p)?; + p.exit()?; + relay::shutdown_relay(8051)?; + Ok(()) + }); - let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]); - expect_confirm_prompt(&mut p)?; - p.exit()?; + // launch relay + r51.listen_until_close().await?; + cli_tester_handle.join().unwrap()?; Ok(()) } - #[test] - fn when_response_is_false_aborts() -> Result<()> { + #[tokio::test] + #[serial] + async fn when_response_is_false_aborts() -> Result<()> { let test_repo = prep_test_repo()?; + let mut r51 = create_relay_51()?; + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]); + expect_confirm_prompt(&mut p)?.succeeds_with(Some(false))?; + p.expect_end_with("Error: aborting so commits can be rebased\r\n")?; + relay::shutdown_relay(8051)?; + Ok(()) + }); - let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]); - - expect_confirm_prompt(&mut p)?.succeeds_with(Some(false))?; - - p.expect_end_with("Error: aborting so commits can be rebased\r\n")?; - + // launch relay + r51.listen_until_close().await?; + cli_tester_handle.join().unwrap()?; Ok(()) } - #[test] + + #[tokio::test] #[serial] - fn when_response_is_true_proceeds() -> Result<()> { + async fn when_response_is_true_proceeds() -> Result<()> { let test_repo = prep_test_repo()?; + let mut r51 = create_relay_51()?; + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]); + expect_confirm_prompt(&mut p)?.succeeds_with(Some(true))?; + p.expect("? include cover letter")?; + p.exit()?; + relay::shutdown_relay(8051)?; + Ok(()) + }); - let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]); - expect_confirm_prompt(&mut p)?.succeeds_with(Some(true))?; - p.expect("? include cover letter")?; - p.exit()?; + // launch relay + r51.listen_until_close().await?; + cli_tester_handle.join().unwrap()?; Ok(()) } } @@ -1620,7 +1664,7 @@ mod root_proposal_specified_using_in_reply_to_with_range_of_head_2_and_cover_let .unwrap() .as_slice()[1], // id of state nevent - "431e58eb8e1b4e20292d1d5bbe81d5cfb042e1bc165de32eddfdd52245a4cce4", + "000c104861e34a453481ab23e7de21a6baf475b394479705363b035936732528", ); } Ok(()) -- cgit v1.2.3 From dee39c39116773fde22c4fe30a87d54d1d3658e2 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 5 Aug 2025 11:08:26 +0100 Subject: feat(send): push PR to custom clone url if the repo doesnt list any grasp servers, or pushing to them fails --- src/bin/git_remote_nostr/push.rs | 44 +++++++++++++++++++-------- src/bin/ngit/sub_commands/send.rs | 64 ++++++++++++++++++++++++++++++--------- src/lib/push.rs | 55 ++++++++++++++------------------- 3 files changed, 103 insertions(+), 60 deletions(-) (limited to 'src/bin/git_remote_nostr') diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 3967699..4552b91 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -450,18 +450,38 @@ async fn generate_patches_or_pr_event_or_pr_updates( signer: &Arc, term: &Term, ) -> Result> { - let mut events: Vec = vec![]; let parent_is_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)); let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead); if use_pr { - for event in push_refs_and_generate_pr_or_pr_update_event( + let repo_grasps = repo_ref.grasp_servers(); + let repo_grasp_clone_urls: Vec = repo_ref + .git_server + .iter() + .filter(|s| is_grasp_server(s, &repo_grasps)) + .cloned() + .collect(); + + if repo_grasp_clone_urls.is_empty() { + // TODO get grasp_default_set servers that aren't in repo_grasps + // cycle through until one succeeds TODO create + // personal-fork announcement with grasp servers and + // push, after a few seconds push ref/nostr/eventid. if + // one success break out of for loop and continue + + bail!( + "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." + ); + } + + if let (Some(events), _) = push_refs_and_generate_pr_or_pr_update_event( git_repo, repo_ref, ahead.first().context("no commits to push")?, user_ref, root_proposal, &None, + &repo_grasp_clone_urls, signer, term, ) @@ -471,12 +491,17 @@ async fn generate_patches_or_pr_event_or_pr_updates( } else { "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." } - )? - { - events.push(event); + )? { + Ok(events) + } else { + bail!( + "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" + ); + // TODO suggest `ngit send` where user could specify their own clone + // url to push to once that feature is added } } else { - for patch in generate_cover_letter_and_patch_events( + generate_cover_letter_and_patch_events( None, git_repo, ahead, @@ -485,13 +510,8 @@ async fn generate_patches_or_pr_event_or_pr_updates( &root_proposal.map(|proposal| proposal.id.to_string()), &[], ) - .await? - { - events.push(patch); - } + .await } - - Ok(events) } type HashMapUrlRefspecs = HashMap>; diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index 0aefb03..69ad1e6 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs @@ -1,11 +1,13 @@ -use std::path::Path; +use std::{path::Path, str::FromStr}; use anyhow::{Context, Result, bail}; use console::Style; use ngit::{ client::{Params, send_events}, + git::nostr_url::CloneUrl, git_events::{EventRefType, KIND_PULL_REQUEST, generate_cover_letter_and_patch_events}, push::push_refs_and_generate_pr_or_pr_update_event, + repo_ref::is_grasp_server, utils::proposal_tip_is_pr_or_pr_update, }; use nostr::{ToBech32, event::Event, nips::nip19::Nip19Event}; @@ -192,20 +194,52 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re commits.reverse(); let events = if as_pr { - push_refs_and_generate_pr_or_pr_update_event( - &git_repo, - &repo_ref, - commits.last().context("no commits")?, - &user_ref, - root_proposal.as_ref(), - &cover_letter_title_description, - &signer, - &console::Term::stdout(), - ) - .await? - - // TODO - // - allow specifying clone url and ref + let repo_grasps = repo_ref.grasp_servers(); + let repo_grasp_clone_urls: Vec = repo_ref + .git_server + .iter() + .filter(|s| is_grasp_server(s, &repo_grasps)) + .cloned() + .collect(); + if repo_grasp_clone_urls.is_empty() { + println!( + "The repository doesn't list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request." + ); + } + let mut to_try = repo_grasp_clone_urls.clone(); + let mut tried = vec![]; + loop { + let (events, _server_responses) = push_refs_and_generate_pr_or_pr_update_event( + &git_repo, + &repo_ref, + commits.last().context("no commits")?, + &user_ref, + root_proposal.as_ref(), + &cover_letter_title_description, + &repo_grasp_clone_urls, + &signer, + &console::Term::stdout(), + ) + .await?; + for url in to_try { + tried.push(url); + } + to_try = vec![]; + if let Some(events) = events { + break events; + } + let clone_url = Interactor::default() + .input( + PromptInputParms::default().with_prompt("git repo url with write permission"), + )? + .clone(); + if CloneUrl::from_str(&clone_url).is_ok() { + to_try.push(clone_url); + // TODO customise ref to push + } else { + println!("invalid clone url"); + } + } } else { let events = generate_cover_letter_and_patch_events( cover_letter_title_description.clone(), diff --git a/src/lib/push.rs b/src/lib/push.rs index bcd368b..4c2d8f1 100644 --- a/src/lib/push.rs +++ b/src/lib/push.rs @@ -4,7 +4,7 @@ use std::{ time::Instant, }; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Result, anyhow}; use auth_git2::GitAuthenticator; use console::Term; use nostr::{ @@ -25,7 +25,7 @@ use crate::{ }, git_events::generate_unsigned_pr_or_update_event, login::user::UserRef, - repo_ref::{RepoRef, is_grasp_server, normalize_grasp_server_url}, + repo_ref::{RepoRef, normalize_grasp_server_url}, utils::{ Direction, get_short_git_server_name, get_write_protocols_to_try, join_with_and, set_protocol_preference, @@ -329,19 +329,14 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( user_ref: &UserRef, root_proposal: Option<&Event>, title_description_overide: &Option<(String, String)>, + servers: &[String], signer: &Arc, term: &Term, -) -> Result> { - let mut events: Vec = vec![]; - let repo_grasps = repo_ref.grasp_servers(); - let repo_grasp_clone_urls = repo_ref - .git_server - .iter() - .filter(|s| is_grasp_server(s, &repo_grasps)); +) -> Result<(Option>, Vec<(String, Result<()>)>)> { + let mut responses = vec![]; let mut unsigned_pr_event: Option = None; - let mut failed_clone_urls = vec![]; - for clone_url in repo_grasp_clone_urls { + for clone_url in servers { let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { unsigned_pr_event.clone() } else { @@ -360,7 +355,6 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( let refspec = format!("{}:refs/nostr/{}", tip, draft_pr_event.id()); if let Err(error) = push_to_remote_url(git_repo, clone_url, &[refspec], term) { - failed_clone_urls.push(clone_url); term.write_line( format!( "push: error sending commit data to {}: {error}", @@ -368,7 +362,9 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( ) .as_str(), )?; + responses.push((clone_url.clone(), Err(error))); } else { + responses.push((clone_url.clone(), Ok(()))); term.write_line( format!( "push: commit data sent to {}", @@ -379,17 +375,6 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( unsigned_pr_event = Some(draft_pr_event); } } - if unsigned_pr_event.is_none() { - bail!( - "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." - ); - - // TODO get grasp_default_set servers that aren't in repo_grasps - // cycle through until one succeeds TODO create - // personal-fork announcement with grasp servers and - // push, after a few seconds push ref/nostr/eventid. if - // one success break out of for loop and continue - } if let Some(unsigned_pr_event) = unsigned_pr_event { let pr_event = sign_draft_event( unsigned_pr_event, @@ -404,21 +389,25 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( .to_string(), ) .await?; - events.push(pr_event); if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) { - events.push( - create_close_status_for_original_patch(signer, repo_ref, root_proposal.unwrap()) + Ok(( + Some(vec![ + pr_event, + create_close_status_for_original_patch( + signer, + repo_ref, + root_proposal.unwrap(), + ) .await?, - ); + ]), + responses, + )) + } else { + Ok((Some(vec![pr_event]), responses)) } } else { - bail!( - "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" - ); - // TODO suggest `ngit send` where user could specify their own clone - // url to push to once that feature is added + Ok((None, responses)) } - Ok(events) } async fn create_close_status_for_original_patch( -- cgit v1.2.3 From 29f61ffdf155ea88b8d9aec23d28cf70baba577e Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 5 Aug 2025 11:38:09 +0100 Subject: feat(send): custom ref for PR clone url allow specifying ref for pushing PR to custom clone url --- src/bin/git_remote_nostr/push.rs | 1 + src/bin/ngit/sub_commands/send.rs | 14 +++++++++++++- src/lib/push.rs | 8 +++++++- 3 files changed, 21 insertions(+), 2 deletions(-) (limited to 'src/bin/git_remote_nostr') diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 4552b91..f98e792 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -482,6 +482,7 @@ async fn generate_patches_or_pr_event_or_pr_updates( root_proposal, &None, &repo_grasp_clone_urls, + None, signer, term, ) diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index 69ad1e6..609812b 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs @@ -208,6 +208,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re } let mut to_try = repo_grasp_clone_urls.clone(); let mut tried = vec![]; + let mut git_ref = None; loop { let (events, _server_responses) = push_refs_and_generate_pr_or_pr_update_event( &git_repo, @@ -217,6 +218,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re root_proposal.as_ref(), &cover_letter_title_description, &repo_grasp_clone_urls, + git_ref.clone(), &signer, &console::Term::stdout(), ) @@ -235,7 +237,17 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re .clone(); if CloneUrl::from_str(&clone_url).is_ok() { to_try.push(clone_url); - // TODO customise ref to push + let mut git_ref_or_branch_name = Interactor::default() + .input( + PromptInputParms::default() + .with_prompt("ref / branch name") + .with_default(git_ref.unwrap_or("refs/nostr/".to_string())), + )? + .clone(); + if !git_ref_or_branch_name.starts_with("refs/") { + git_ref_or_branch_name = format!("refs/heads/{git_ref_or_branch_name}"); + } + git_ref = Some(git_ref_or_branch_name); } else { println!("invalid clone url"); } diff --git a/src/lib/push.rs b/src/lib/push.rs index 4c2d8f1..c202397 100644 --- a/src/lib/push.rs +++ b/src/lib/push.rs @@ -330,6 +330,7 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( root_proposal: Option<&Event>, title_description_overide: &Option<(String, String)>, servers: &[String], + git_ref: Option, signer: &Arc, term: &Term, ) -> Result<(Option>, Vec<(String, Result<()>)>)> { @@ -352,7 +353,12 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( )? }; - let refspec = format!("{}:refs/nostr/{}", tip, draft_pr_event.id()); + let git_ref_used = git_ref + .clone() + .unwrap_or("refs/nostr/".to_string()) + .replace("", &draft_pr_event.id().to_string()); + + let refspec = format!("{tip}:{git_ref_used}"); if let Err(error) = push_to_remote_url(git_repo, clone_url, &[refspec], term) { term.write_line( -- cgit v1.2.3 From afaa472196059bbfbd41a39b23d8ffb4c28d0805 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 7 Aug 2025 08:41:39 +0100 Subject: refactor: rename fn `is_grasp_server_in_list` to make it's purpose clearer --- src/bin/git_remote_nostr/fetch.rs | 4 ++-- src/bin/git_remote_nostr/push.rs | 6 +++--- src/bin/ngit/sub_commands/send.rs | 8 ++++---- src/lib/list.rs | 4 ++-- src/lib/repo_ref.rs | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) (limited to 'src/bin/git_remote_nostr') diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs index 221d964..2cc87da 100644 --- a/src/bin/git_remote_nostr/fetch.rs +++ b/src/bin/git_remote_nostr/fetch.rs @@ -18,7 +18,7 @@ use ngit::{ }, git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, tag_value}, login::get_curent_user, - repo_ref::{RepoRef, is_grasp_server}, + repo_ref::{RepoRef, is_grasp_server_in_list}, utils::{ Direction, find_proposal_and_patches_by_branch_name, get_oids_from_fetch_batch, get_open_or_draft_proposals, get_read_protocols_to_try, join_with_and, @@ -96,7 +96,7 @@ pub async fn run_fetch( git_server_url, &repo_ref.to_nostr_git_url(&None), &term, - is_grasp_server(git_server_url, &repo_ref.grasp_servers()), + is_grasp_server_in_list(git_server_url, &repo_ref.grasp_servers()), ) { errors.push(error); } diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index f98e792..d42c904 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -20,7 +20,7 @@ use ngit::{ list::list_from_remotes, login::{self, user::UserRef}, push::{push_refs_and_generate_pr_or_pr_update_event, push_to_remote}, - repo_ref::{self, get_repo_config_from_yaml, is_grasp_server}, + repo_ref::{self, get_repo_config_from_yaml, is_grasp_server_in_list}, repo_state, utils::{ find_proposal_and_patches_by_branch_name, get_all_proposals, get_remote_name_by_url, @@ -153,7 +153,7 @@ pub async fn run_push( &repo_ref.to_nostr_git_url(&None), &remote_refspecs, &term, - is_grasp_server(&git_server_url, &repo_ref.grasp_servers()), + is_grasp_server_in_list(&git_server_url, &repo_ref.grasp_servers()), ); } } @@ -458,7 +458,7 @@ async fn generate_patches_or_pr_event_or_pr_updates( let repo_grasp_clone_urls: Vec = repo_ref .git_server .iter() - .filter(|s| is_grasp_server(s, &repo_grasps)) + .filter(|s| is_grasp_server_in_list(s, &repo_grasps)) .cloned() .collect(); diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index fa2b64d..05054fd 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs @@ -13,7 +13,7 @@ use ngit::{ push::push_refs_and_generate_pr_or_pr_update_event, repo_ref::{ format_grasp_server_url_as_clone_url, format_grasp_server_url_as_relay_url, - is_grasp_server, normalize_grasp_server_url, + is_grasp_server_in_list, normalize_grasp_server_url, }, utils::proposal_tip_is_pr_or_pr_update, }; @@ -252,7 +252,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re } // also use repo grasp servers for url in &repo_ref.git_server { - if is_grasp_server(url, &repo_grasps) && !to_try.contains(url) { + if is_grasp_server_in_list(url, &repo_grasps) && !to_try.contains(url) { to_try.push(url.clone()); } } @@ -287,7 +287,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re .map(std::string::ToString::to_string) .filter(|g| { // is a grasp server not in list of tried - !is_grasp_server(g, &tried) + !is_grasp_server_in_list(g, &tried) }) .collect(); @@ -338,7 +338,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re let default_choices: Vec = client .get_grasp_default_set() .iter() - .filter(|g| !is_grasp_server(g, &tried)) + .filter(|g| !is_grasp_server_in_list(g, &tried)) .cloned() .collect(); let selections = vec![true; default_choices.len()]; // all selected by default diff --git a/src/lib/list.rs b/src/lib/list.rs index b940546..b867858 100644 --- a/src/lib/list.rs +++ b/src/lib/list.rs @@ -9,7 +9,7 @@ use crate::{ Repo, RepoActions, nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, }, - repo_ref::is_grasp_server, + repo_ref::is_grasp_server_in_list, utils::{Direction, get_read_protocols_to_try, join_with_and, set_protocol_preference}, }; @@ -23,7 +23,7 @@ pub fn list_from_remotes( let mut remote_states = HashMap::new(); let mut errors = HashMap::new(); for url in git_servers { - let is_grasp_server = is_grasp_server(url, grasp_servers); + let is_grasp_server = is_grasp_server_in_list(url, grasp_servers); match list_from_remote(term, git_repo, url, decoded_nostr_url, is_grasp_server) { Err(error) => { errors.insert(url, error); diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs index e3f71a1..5e857e9 100644 --- a/src/lib/repo_ref.rs +++ b/src/lib/repo_ref.rs @@ -740,7 +740,7 @@ pub fn extract_npub(s: &str) -> Result<&str> { } // this should be called is_grasp_server_in_list -pub fn is_grasp_server(url: &str, grasp_servers: &[String]) -> bool { +pub fn is_grasp_server_in_list(url: &str, grasp_servers: &[String]) -> bool { if !grasp_servers.is_empty() { if let Ok(url) = normalize_grasp_server_url(url) { grasp_servers.iter().any(|s| { -- cgit v1.2.3