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 +++++++++- 5 files changed, 137 insertions(+), 50 deletions(-) (limited to 'src') 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)>, -- cgit v1.2.3