From 757c2f888b2be2b37ea01e02a6c020c5f8c7aa9c Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 18 Jul 2025 09:33:48 +0100 Subject: feat: fetch PR and PRUpdate events as the first stage of adding support --- src/lib/client.rs | 45 ++++++++++++++++++++++++++++++++++++--------- src/lib/git_events.rs | 3 +++ 2 files changed, 39 insertions(+), 9 deletions(-) (limited to 'src/lib') diff --git a/src/lib/client.rs b/src/lib/client.rs index e808bea..ae3b414 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -31,6 +31,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressState, P use mockall::*; use nostr::{ Event, + event::{TagKind, TagStandard}, filter::Alphabet, nips::{nip01::Coordinate, nip19::Nip19Coordinate}, signer::SignerBackend, @@ -47,7 +48,8 @@ use crate::{ get_dirs, git::{Repo, RepoActions, get_git_config_item}, git_events::{ - event_is_cover_letter, event_is_patch_set_root, event_is_revision_root, status_kinds, + KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_cover_letter, + event_is_patch_set_root, event_is_revision_root, status_kinds, }, login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, repo_ref::RepoRef, @@ -1459,7 +1461,7 @@ async fn process_fetched_events( report.updated_state = Some((event.created_at, event.id)); } } - } else if event_is_patch_set_root(event) { + } else if event_is_patch_set_root(event) || event.kind.eq(&KIND_PULL_REQUEST) { fresh_proposal_roots.insert(event.id); report.proposals.insert(event.id); if !request.contributors.contains(&event.pubkey) @@ -1487,12 +1489,23 @@ async fn process_fetched_events( } for event in &events { if !request.existing_events.contains(&event.id) - && !event + && (!event .tags .event_ids() .any(|id| report.proposals.contains(id)) + || event + .tags + .filter_standardized(TagKind::Custom(std::borrow::Cow::Borrowed("E"))) + .filter_map(|t| match t { + TagStandard::Event { event_id, .. } => Some(event_id), + TagStandard::EventReport(event_id, ..) => Some(event_id), + _ => None, + }) + .any(|id| report.proposals.contains(id))) { - if event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event) { + if (event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event)) + || event.kind.eq(&KIND_PULL_REQUEST_UPDATE) + { report.commits.insert(event.id); } else if status_kinds().contains(&event.kind) { report.statuses.insert(event.id); @@ -1570,7 +1583,7 @@ pub fn get_fetch_filters( get_filter_state_events(repo_coordinates), get_filter_repo_events(repo_coordinates), nostr::Filter::default() - .kinds(vec![Kind::GitPatch, Kind::EventDeletion]) + .kinds(vec![Kind::GitPatch, Kind::EventDeletion, KIND_PULL_REQUEST]) .custom_tags( SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), repo_coordinates @@ -1584,15 +1597,29 @@ pub fn get_fetch_filters( vec![] } else { vec![ - nostr::Filter::default() - .events(proposal_ids.clone()) - .kinds([vec![Kind::GitPatch, Kind::EventDeletion], status_kinds()].concat()), + nostr::Filter::default().events(proposal_ids.clone()).kinds( + [ + vec![ + Kind::GitPatch, + Kind::EventDeletion, + KIND_PULL_REQUEST_UPDATE, + ], + status_kinds(), + ] + .concat(), + ), nostr::Filter::default() .custom_tags( SingleLetterTag::uppercase(Alphabet::E), proposal_ids.clone(), ) - .kinds([vec![Kind::GitPatch, Kind::EventDeletion], status_kinds()].concat()), + .kinds( + [ + vec![Kind::EventDeletion, KIND_PULL_REQUEST_UPDATE], + status_kinds(), + ] + .concat(), + ), ] }, if required_profiles.is_empty() { diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 69406c1..80793bd 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs @@ -58,6 +58,9 @@ pub fn status_kinds() -> Vec { ] } +pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); +pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); + pub fn event_is_patch_set_root(event: &Event) -> bool { event.kind.eq(&Kind::GitPatch) && event -- cgit v1.2.3 From 3eb2354edb8e76428625d5645e110c30aa1ccc2a Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 18 Jul 2025 11:56:15 +0100 Subject: feat(pr): list PR and PR updates remote will list the refs under `pr/*` namespace. `ngit list` will display in the list of open / draft proposals. it won't yet fetch the related oids to enable fetching or checking out the branch. --- src/bin/git_remote_nostr/list.rs | 56 +++++++++++--- src/bin/git_remote_nostr/utils.rs | 25 +++--- src/bin/ngit/sub_commands/list.rs | 157 ++++++++++++++++++++++++++------------ src/lib/client.rs | 37 +++++++-- src/lib/git_events.rs | 50 +++++++----- 5 files changed, 233 insertions(+), 92 deletions(-) (limited to 'src/lib') diff --git a/src/bin/git_remote_nostr/list.rs b/src/bin/git_remote_nostr/list.rs index b9fb0c0..7bdf170 100644 --- a/src/bin/git_remote_nostr/list.rs +++ b/src/bin/git_remote_nostr/list.rs @@ -11,7 +11,7 @@ use ngit::{ self, nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, }, - git_events::event_to_cover_letter, + git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, tag_value}, login::get_curent_user, repo_ref::{self, is_grasp_server}, }; @@ -122,6 +122,16 @@ async fn get_open_and_draft_proposals_state( // without trusting commit_id we must apply each patch which requires the oid of // the parent so we much do a fetch + + // As we are fetching from git servers we mighgt as well get oids from pull + // request too + // TODO get Pull Request and Pull Request Update Events add these to + // refs/nostr/ + // TODO prepare PRs and PRS oids to try and fetch from repo servers that are or + // clone urls in PR/update event we are using anyway. TODO after we tried + // and failed to get them from these server we should fallback to fetch them + // from listed clone urls in PR/update but not during list, only during fetch + for (git_server_url, (oids_from_git_servers, is_grasp_server)) in remote_states { if fetch_from_git_server( git_repo, @@ -144,7 +154,7 @@ async fn get_open_and_draft_proposals_state( let mut state = HashMap::new(); let open_and_draft_proposals = get_open_or_draft_proposals(git_repo, repo_ref).await?; let current_user = get_curent_user(git_repo)?; - for (_, (proposal, patches)) in open_and_draft_proposals { + for (_, (proposal, events_to_apply)) in open_and_draft_proposals { if let Ok(cl) = event_to_cover_letter(&proposal) { if let Ok(mut branch_name) = cl.get_branch_name_with_pr_prefix_and_shorthand_id() { branch_name = if let Some(public_key) = current_user { @@ -156,15 +166,43 @@ async fn get_open_and_draft_proposals_state( } else { branch_name }; - match make_commits_for_proposal(git_repo, repo_ref, &patches) { - Ok(tip) => { - state.insert(format!("refs/heads/{branch_name}"), tip); + // if events_to_apply contains a PR or PR Update event it should be the only + // event in the Vec + if let Some(pr_or_pr_update) = events_to_apply + .iter() + .find(|e| e.kind.eq(&KIND_PULL_REQUEST) || e.kind.eq(&KIND_PULL_REQUEST_UPDATE)) + { + match tag_value(pr_or_pr_update, "c") { + Ok(tip) => { + state.insert(format!("refs/heads/{branch_name}"), tip); + } + Err(_) => { + let _ = term.write_line( + format!( + "WARNING: failed to fetch branch {branch_name} error: {} event poorly formatted", + if pr_or_pr_update.kind.eq(&KIND_PULL_REQUEST) { + "PR" + } else { + "PR update" + } + ) + .as_str(), + ); + } } - Err(error) => { - let _ = term.write_line( - format!("WARNING: failed to fetch branch {branch_name} error: {error}") + } else { + match make_commits_for_proposal(git_repo, repo_ref, &events_to_apply) { + Ok(tip) => { + state.insert(format!("refs/heads/{branch_name}"), tip); + } + Err(error) => { + let _ = term.write_line( + format!( + "WARNING: failed to fetch branch {branch_name} error: {error}" + ) .as_str(), - ); + ); + } } } } diff --git a/src/bin/git_remote_nostr/utils.rs b/src/bin/git_remote_nostr/utils.rs index dc75872..d0b4e7e 100644 --- a/src/bin/git_remote_nostr/utils.rs +++ b/src/bin/git_remote_nostr/utils.rs @@ -10,7 +10,7 @@ use anyhow::{Context, Result, bail}; use git2::Repository; use ngit::{ client::{ - get_all_proposal_patch_events_from_cache, get_events_from_local_cache, + get_all_proposal_patch_pr_pr_update_events_from_cache, get_events_from_local_cache, get_proposals_and_revisions_from_cache, }, git::{ @@ -18,7 +18,7 @@ use ngit::{ nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, }, git_events::{ - event_is_revision_root, get_most_recent_patch_with_ancestors, + event_is_revision_root, get_pr_tip_event_or_most_recent_patch_with_ancestors, is_event_proposal_root_for_branch, status_kinds, }, repo_ref::RepoRef, @@ -140,12 +140,15 @@ pub async fn get_open_or_draft_proposals( Kind::GitStatusOpen }; if [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&status) { - if let Ok(commits_events) = - get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id) - .await + if let Ok(commits_events) = get_all_proposal_patch_pr_pr_update_events_from_cache( + git_repo_path, + repo_ref, + &proposal.id, + ) + .await { if let Ok(most_recent_proposal_patch_chain) = - get_most_recent_patch_with_ancestors(commits_events.clone()) + get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone()) { open_or_draft_proposals .insert(proposal.id, (proposal, most_recent_proposal_patch_chain)); @@ -172,11 +175,15 @@ pub async fn get_all_proposals( let mut all_proposals = HashMap::new(); for proposal in proposals { - if let Ok(commits_events) = - get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id).await + if let Ok(commits_events) = get_all_proposal_patch_pr_pr_update_events_from_cache( + git_repo_path, + repo_ref, + &proposal.id, + ) + .await { if let Ok(most_recent_proposal_patch_chain) = - get_most_recent_patch_with_ancestors(commits_events.clone()) + get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone()) { all_proposals.insert(proposal.id, (proposal, most_recent_proposal_patch_chain)); } diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs index 0330be1..a90b28e 100644 --- a/src/bin/ngit/sub_commands/list.rs +++ b/src/bin/ngit/sub_commands/list.rs @@ -3,10 +3,12 @@ use std::{io::Write, ops::Add}; use anyhow::{Context, Result, bail}; use ngit::{ client::{ - Params, get_all_proposal_patch_events_from_cache, get_proposals_and_revisions_from_cache, + Params, get_all_proposal_patch_pr_pr_update_events_from_cache, + get_proposals_and_revisions_from_cache, }, git_events::{ - get_commit_id_from_patch, get_most_recent_patch_with_ancestors, status_kinds, tag_value, + KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch, + get_pr_tip_event_or_most_recent_patch_with_ancestors, status_kinds, tag_value, }, }; use nostr_sdk::Kind; @@ -184,21 +186,22 @@ pub async fn launch() -> Result<()> { let cover_letter = event_to_cover_letter(proposals_for_status[selected_index]) .context("failed to extract proposal details from proposal root event")?; - let commits_events: Vec = get_all_proposal_patch_events_from_cache( - git_repo_path, - &repo_ref, - &proposals_for_status[selected_index].id, - ) - .await?; + let commits_events: Vec = + get_all_proposal_patch_pr_pr_update_events_from_cache( + git_repo_path, + &repo_ref, + &proposals_for_status[selected_index].id, + ) + .await?; - let Ok(most_recent_proposal_patch_chain) = - get_most_recent_patch_with_ancestors(commits_events.clone()) + let Ok(most_recent_proposal_patch_chain_or_pr_or_pr_update) = + get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone()) else { if Interactor::default().confirm( PromptConfirmParms::default() .with_default(true) .with_prompt( - "failed to find any patches on this proposal. choose another proposal?", + "failed to find any PR or patch events on this proposal. choose another proposal?", ), )? { continue; @@ -208,15 +211,37 @@ pub async fn launch() -> Result<()> { // for commit in &most_recent_proposal_patch_chain { // println!("recent_event: {:?}", commit.as_json()); // } + if most_recent_proposal_patch_chain_or_pr_or_pr_update + .iter() + .any(|e| [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&e.kind)) + { + match Interactor::default().choice( + PromptChoiceParms::default() + .with_prompt("this is new PR event kind which ngit doesnt yet support") + .with_default(0) + .with_choices(vec!["back to proposals".to_string()]), + )? { + 0 => continue, + _ => { + bail!("unexpected choice") + } + }; + } - let binding_patch_text_ref = format!("{} commits", most_recent_proposal_patch_chain.len()); - let patch_text_ref = if most_recent_proposal_patch_chain.len().gt(&1) { + let binding_patch_text_ref = format!( + "{} commits", + most_recent_proposal_patch_chain_or_pr_or_pr_update.len() + ); + let patch_text_ref = if most_recent_proposal_patch_chain_or_pr_or_pr_update + .len() + .gt(&1) + { binding_patch_text_ref.as_str() } else { "1 commit" }; - let no_support_for_patches_as_branch = most_recent_proposal_patch_chain + let no_support_for_patches_as_branch = most_recent_proposal_patch_chain_or_pr_or_pr_update .iter() .any(|event| !patch_supports_commit_ids(event)); @@ -253,8 +278,13 @@ pub async fn launch() -> Result<()> { )?; continue; } - 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), - 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), + 1 => { + launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update) + } + 2 => save_patches_to_dir( + most_recent_proposal_patch_chain_or_pr_or_pr_update, + &git_repo, + ), 3 => continue, _ => { bail!("unexpected choice") @@ -277,9 +307,11 @@ pub async fn launch() -> Result<()> { .eq(&cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?); let proposal_base_commit = str_to_sha1(&tag_value( - most_recent_proposal_patch_chain.last().context( - "there should be at least one patch as we have already checked for this", - )?, + most_recent_proposal_patch_chain_or_pr_or_pr_update + .last() + .context( + "there should be at least one patch as we have already checked for this", + )?, "parent-commit", )?) .context("failed to get valid parent commit id from patch")?; @@ -300,8 +332,8 @@ pub async fn launch() -> Result<()> { ], ))? { 0 | 3 => continue, - 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), - 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), + 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update), + 2 => save_patches_to_dir(most_recent_proposal_patch_chain_or_pr_or_pr_update, &git_repo), _ => { bail!("unexpected choice") } @@ -309,9 +341,13 @@ pub async fn launch() -> Result<()> { } let proposal_tip = str_to_sha1( - &get_commit_id_from_patch(most_recent_proposal_patch_chain.first().context( - "there should be at least one patch as we have already checked for this", - )?) + &get_commit_id_from_patch( + most_recent_proposal_patch_chain_or_pr_or_pr_update + .first() + .context( + "there should be at least one patch as we have already checked for this", + )?, + ) .context("failed to get valid commit_id from patch")?, ) .context("failed to get valid commit_id from patch")?; @@ -325,7 +361,7 @@ pub async fn launch() -> Result<()> { .choice(PromptChoiceParms::default().with_default(0).with_choices(vec![ format!( "create and checkout proposal branch ({} ahead {} behind '{main_branch_name}')", - most_recent_proposal_patch_chain.len(), + most_recent_proposal_patch_chain_or_pr_or_pr_update.len(), proposal_behind_main.len(), ), format!("apply to current branch with `git am`"), @@ -337,7 +373,7 @@ pub async fn launch() -> Result<()> { let _ = git_repo .apply_patch_chain( &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, - most_recent_proposal_patch_chain, + most_recent_proposal_patch_chain_or_pr_or_pr_update, ) .context("failed to apply patch chain")?; @@ -347,8 +383,8 @@ pub async fn launch() -> Result<()> { ); Ok(()) } - 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), - 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), + 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update), + 2 => save_patches_to_dir(most_recent_proposal_patch_chain_or_pr_or_pr_update, &git_repo), 3 => continue, _ => { bail!("unexpected choice") @@ -382,7 +418,7 @@ pub async fn launch() -> Result<()> { .with_choices(vec![ format!( "checkout proposal branch ({} ahead {} behind '{main_branch_name}')", - most_recent_proposal_patch_chain.len(), + most_recent_proposal_patch_chain_or_pr_or_pr_update.len(), proposal_behind_main.len(), ), format!("apply to current branch with `git am`"), @@ -401,8 +437,13 @@ pub async fn launch() -> Result<()> { ); Ok(()) } - 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), - 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), + 1 => { + launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update) + } + 2 => save_patches_to_dir( + most_recent_proposal_patch_chain_or_pr_or_pr_update, + &git_repo, + ), 3 => continue, _ => { bail!("unexpected choice") @@ -414,11 +455,14 @@ pub async fn launch() -> Result<()> { git_repo.get_commits_ahead_behind(&master_tip, &local_branch_tip)?; // new appendments to proposal - if let Some(index) = most_recent_proposal_patch_chain.iter().position(|patch| { - get_commit_id_from_patch(patch) - .unwrap_or_default() - .eq(&local_branch_tip.to_string()) - }) { + if let Some(index) = most_recent_proposal_patch_chain_or_pr_or_pr_update + .iter() + .position(|patch| { + get_commit_id_from_patch(patch) + .unwrap_or_default() + .eq(&local_branch_tip.to_string()) + }) + { return match Interactor::default().choice( PromptChoiceParms::default() .with_default(0) @@ -437,7 +481,7 @@ pub async fn launch() -> Result<()> { let _ = git_repo .apply_patch_chain( &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, - most_recent_proposal_patch_chain, + most_recent_proposal_patch_chain_or_pr_or_pr_update, ) .context("failed to apply patch chain")?; println!( @@ -448,8 +492,13 @@ pub async fn launch() -> Result<()> { ); Ok(()) } - 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), - 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), + 1 => { + launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update) + } + 2 => save_patches_to_dir( + most_recent_proposal_patch_chain_or_pr_or_pr_update, + &git_repo, + ), 3 => continue, _ => { bail!("unexpected choice") @@ -467,7 +516,7 @@ pub async fn launch() -> Result<()> { }) { println!( "updated proposal available ({} ahead {} behind '{main_branch_name}'). existing version is {} ahead {} behind '{main_branch_name}'", - most_recent_proposal_patch_chain.len(), + most_recent_proposal_patch_chain_or_pr_or_pr_update.len(), proposal_behind_main.len(), local_ahead_of_main.len(), local_beind_main.len(), @@ -492,11 +541,11 @@ pub async fn launch() -> Result<()> { git_repo.checkout( &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, )?; - let chain_length = most_recent_proposal_patch_chain.len(); + let chain_length = most_recent_proposal_patch_chain_or_pr_or_pr_update.len(); let _ = git_repo .apply_patch_chain( &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, - most_recent_proposal_patch_chain, + most_recent_proposal_patch_chain_or_pr_or_pr_update, ) .context("failed to apply patch chain")?; println!( @@ -520,8 +569,13 @@ pub async fn launch() -> Result<()> { ); Ok(()) } - 2 => launch_git_am_with_patches(most_recent_proposal_patch_chain), - 3 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), + 2 => { + launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update) + } + 3 => save_patches_to_dir( + most_recent_proposal_patch_chain_or_pr_or_pr_update, + &git_repo, + ), 4 => continue, _ => { bail!("unexpected choice") @@ -581,7 +635,7 @@ pub async fn launch() -> Result<()> { if git_repo.does_commit_exist(&proposal_tip.to_string())? { println!( "you have previously applied the latest version of the proposal ({} ahead {} behind '{main_branch_name}') but your local proposal branch has amended or rebased it ({} ahead {} behind '{main_branch_name}')", - most_recent_proposal_patch_chain.len(), + most_recent_proposal_patch_chain_or_pr_or_pr_update.len(), proposal_behind_main.len(), local_ahead_of_main.len(), local_beind_main.len(), @@ -594,7 +648,7 @@ pub async fn launch() -> Result<()> { "your local proposal branch ({} ahead {} behind '{main_branch_name}') has conflicting changes with the latest published proposal ({} ahead {} behind '{main_branch_name}')", local_ahead_of_main.len(), local_beind_main.len(), - most_recent_proposal_patch_chain.len(), + most_recent_proposal_patch_chain_or_pr_or_pr_update.len(), proposal_behind_main.len(), ); @@ -639,11 +693,11 @@ pub async fn launch() -> Result<()> { &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, &proposal_base_commit.to_string(), )?; - let chain_length = most_recent_proposal_patch_chain.len(); + let chain_length = most_recent_proposal_patch_chain_or_pr_or_pr_update.len(); let _ = git_repo .apply_patch_chain( &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, - most_recent_proposal_patch_chain, + most_recent_proposal_patch_chain_or_pr_or_pr_update, ) .context("failed to apply patch chain")?; @@ -658,8 +712,11 @@ pub async fn launch() -> Result<()> { ); Ok(()) } - 2 => launch_git_am_with_patches(most_recent_proposal_patch_chain), - 3 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), + 2 => launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update), + 3 => save_patches_to_dir( + most_recent_proposal_patch_chain_or_pr_or_pr_update, + &git_repo, + ), 4 => continue, _ => { bail!("unexpected choice") diff --git a/src/lib/client.rs b/src/lib/client.rs index ae3b414..1f3b08c 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -1811,7 +1811,7 @@ pub async fn get_proposals_and_revisions_from_cache( git_repo_path, vec![ nostr::Filter::default() - .kind(nostr::Kind::GitPatch) + .kinds([nostr::Kind::GitPatch, KIND_PULL_REQUEST]) .custom_tags( nostr::SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), repo_coordinates @@ -1823,7 +1823,7 @@ pub async fn get_proposals_and_revisions_from_cache( ) .await? .iter() - .filter(|e| event_is_patch_set_root(e)) + .filter(|e| event_is_patch_set_root(e) || e.kind.eq(&KIND_PULL_REQUEST)) .cloned() .collect::>(); proposals.sort_by_key(|e| e.created_at); @@ -1831,7 +1831,7 @@ pub async fn get_proposals_and_revisions_from_cache( Ok(proposals) } -pub async fn get_all_proposal_patch_events_from_cache( +pub async fn get_all_proposal_patch_pr_pr_update_events_from_cache( git_repo_path: &Path, repo_ref: &RepoRef, proposal_id: &nostr::EventId, @@ -1840,10 +1840,21 @@ pub async fn get_all_proposal_patch_events_from_cache( git_repo_path, vec![ nostr::Filter::default() - .kind(nostr::Kind::GitPatch) + .kinds([ + nostr::Kind::GitPatch, + KIND_PULL_REQUEST, + KIND_PULL_REQUEST_UPDATE, + ]) .event(*proposal_id), nostr::Filter::default() - .kind(nostr::Kind::GitPatch) + .kinds([ + nostr::Kind::GitPatch, + KIND_PULL_REQUEST, + KIND_PULL_REQUEST_UPDATE, + ]) + .custom_tag(SingleLetterTag::uppercase(Alphabet::E), *proposal_id), + nostr::Filter::default() + .kinds([nostr::Kind::GitPatch, KIND_PULL_REQUEST]) .id(*proposal_id), ], ) @@ -1876,8 +1887,20 @@ pub async fn get_all_proposal_patch_events_from_cache( git_repo_path, vec![ nostr::Filter::default() - .kind(nostr::Kind::GitPatch) - .events(revision_roots) + .kinds([ + nostr::Kind::GitPatch, + KIND_PULL_REQUEST, + KIND_PULL_REQUEST_UPDATE, + ]) + .events(revision_roots.clone()) + .authors(permissioned_users.clone()), + nostr::Filter::default() + .kinds([ + nostr::Kind::GitPatch, + KIND_PULL_REQUEST, + KIND_PULL_REQUEST_UPDATE, + ]) + .custom_tags(SingleLetterTag::uppercase(Alphabet::E), revision_roots) .authors(permissioned_users.clone()), ], ) diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 80793bd..7b25daf 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs @@ -70,11 +70,16 @@ pub fn event_is_patch_set_root(event: &Event) -> bool { } pub fn event_is_revision_root(event: &Event) -> bool { - event.kind.eq(&Kind::GitPatch) + (event.kind.eq(&Kind::GitPatch) && event .tags .iter() - .any(|t| t.as_slice().len() > 1 && t.as_slice()[1].eq("revision-root")) + .any(|t| t.as_slice().len() > 1 && t.as_slice()[1].eq("revision-root"))) + || (event.kind.eq(&KIND_PULL_REQUEST) + && event + .tags + .iter() + .any(|t| t.as_slice().len() > 1 && t.as_slice()[0].eq("e"))) } pub fn patch_supports_commit_ids(event: &Event) -> bool { @@ -534,13 +539,22 @@ pub fn commit_msg_from_patch_oneliner(patch: &nostr::Event) -> Result { } pub fn event_to_cover_letter(event: &nostr::Event) -> Result { - if !event_is_patch_set_root(event) { + if !event.kind.eq(&KIND_PULL_REQUEST) && !event_is_patch_set_root(event) { bail!("event is not a patch set root event (root patch or cover letter)") } - let title = commit_msg_from_patch_oneliner(event)?; - let full = commit_msg_from_patch(event)?; - let description = full[title.len()..].trim().to_string(); + let title = if event.kind.eq(&KIND_PULL_REQUEST) { + tag_value(event, "subject").unwrap_or("untitled".to_owned()) + } else { + commit_msg_from_patch_oneliner(event)? + }; + let description = if event.kind.eq(&KIND_PULL_REQUEST) { + event.content.clone() + } else { + commit_msg_from_patch(event)?[title.len()..] + .trim() + .to_string() + }; Ok(CoverLetter { title: title.clone(), @@ -572,25 +586,25 @@ fn safe_branch_name_for_pr(s: &str) -> String { .collect() } -pub fn get_most_recent_patch_with_ancestors( - mut patches: Vec, +pub fn get_pr_tip_event_or_most_recent_patch_with_ancestors( + mut proposal_events: Vec, ) -> Result> { - patches.sort_by_key(|e| e.created_at); + proposal_events.sort_by_key(|e| e.created_at); - let youngest_patch = patches.last().context("no patches found")?; + let youngest = proposal_events.last().context("no proposal events found")?; - let patches_with_youngest_created_at: Vec<&nostr::Event> = patches + let events_with_youngest_created_at: Vec<&nostr::Event> = proposal_events .iter() - .filter(|p| p.created_at.eq(&youngest_patch.created_at)) + .filter(|p| p.created_at.eq(&youngest.created_at)) .collect(); let mut res = vec![]; - let mut event_id_to_search = patches_with_youngest_created_at + let mut event_id_to_search = events_with_youngest_created_at .clone() .iter() .find(|p| { - !patches_with_youngest_created_at.iter().any(|p2| { + !events_with_youngest_created_at.iter().any(|p2| { if let Ok(reply_to) = get_event_parent_id(p2) { reply_to.eq(&p.id.to_string()) } else { @@ -598,16 +612,18 @@ pub fn get_most_recent_patch_with_ancestors( } }) }) - .context("failed to find patches_with_youngest_created_at")? + .context("failed to find events_with_youngest_created_at")? .id .to_string(); - while let Some(event) = patches + while let Some(event) = proposal_events .iter() .find(|e| e.id.to_string().eq(&event_id_to_search)) { res.push(event.clone()); - if event_is_patch_set_root(event) { + if [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind) + || event_is_patch_set_root(event) + { break; } event_id_to_search = get_event_parent_id(event).unwrap_or_default(); -- cgit v1.2.3 From a3d4c8eaa263f4adb174ac81c4248fa200e1857e Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 18 Jul 2025 17:33:11 +0100 Subject: feat(pr): fetch pr and pr updates from clone urls we try and get them from clone urls of repo and fallback to those specified by contributor --- src/bin/git_remote_nostr/fetch.rs | 105 +++++++++++++++++++++++++++++++++----- src/bin/ngit/sub_commands/list.rs | 6 ++- src/lib/client.rs | 10 +++- src/lib/git_events.rs | 13 +++++ 4 files changed, 118 insertions(+), 16 deletions(-) (limited to 'src/lib') diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs index b191850..f3d4362 100644 --- a/src/bin/git_remote_nostr/fetch.rs +++ b/src/bin/git_remote_nostr/fetch.rs @@ -1,6 +1,6 @@ use core::str; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, io::Stdin, sync::{Arc, Mutex}, time::Instant, @@ -16,7 +16,7 @@ use ngit::{ nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, utils::check_ssh_keys, }, - git_events::tag_value, + git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, tag_value}, login::get_curent_user, repo_ref::{RepoRef, is_grasp_server}, }; @@ -37,38 +37,78 @@ pub async fn run_fetch( ) -> Result<()> { let mut fetch_batch = get_oids_from_fetch_batch(stdin, oid, refstr)?; - let oids_from_git_servers = fetch_batch + let oids_from_state = fetch_batch .iter() .filter(|(refstr, _)| !refstr.contains("refs/heads/pr/")) .map(|(_, oid)| oid.clone()) .collect::>(); + let pr_oid_clone_url_map = identify_clone_urls_for_oids_from_pr_pr_update_events( + fetch_batch.values().collect::>(), + git_repo, + repo_ref, + ) + .await?; + + let oids_to_fetch_from_git_servers = [ + oids_from_state.clone(), + pr_oid_clone_url_map + .keys() + .cloned() + .collect::>(), + ] + .concat(); + + let git_servers = { + let mut seen: HashSet = HashSet::new(); + let mut out: Vec = vec![]; + for server in &repo_ref.git_server { + if seen.insert(server.clone()) { + out.push(server.clone()); + } + } + for url in pr_oid_clone_url_map.values().flatten() { + if seen.insert(url.clone()) { + out.push(url.clone()); + } + } + out + }; + let mut errors = vec![]; let term = console::Term::stderr(); - for git_server_url in &repo_ref.git_server { + for git_server_url in &git_servers { + let oids_to_fetch_from_server = oids_to_fetch_from_git_servers + .clone() + .into_iter() + .filter(|oid| !git_repo.does_commit_exist(oid).unwrap_or(false)) + .collect::>(); + + if oids_to_fetch_from_server.is_empty() { + continue; + } + let term = console::Term::stderr(); if let Err(error) = fetch_from_git_server( git_repo, - &oids_from_git_servers, + &oids_from_state, git_server_url, &repo_ref.to_nostr_git_url(&None), &term, is_grasp_server(git_server_url, &repo_ref.grasp_servers()), ) { errors.push(error); - } else { - break; } } - if oids_from_git_servers + if oids_from_state .iter() .any(|oid| !git_repo.does_commit_exist(oid).unwrap()) && !errors.is_empty() { bail!( - "fetch: failed to fetch objects in nostr state event from:\r\n{}", + "fetch: failed to fetch objects from:\r\n{}", errors .iter() .map(|e| format!(" - {e}")) @@ -79,12 +119,43 @@ pub async fn run_fetch( fetch_batch.retain(|refstr, _| refstr.contains("refs/heads/pr/")); - fetch_open_or_draft_proposals(git_repo, &term, repo_ref, &fetch_batch).await?; + fetch_open_or_draft_proposals_from_patches(git_repo, &term, repo_ref, &fetch_batch).await?; + // TODO fetch_open_or_draft_proposals just needs to do it for patches term.flush()?; println!(); Ok(()) } +async fn identify_clone_urls_for_oids_from_pr_pr_update_events( + oids: Vec<&String>, + git_repo: &Repo, + repo_ref: &RepoRef, +) -> Result>> { + let mut map: HashMap> = HashMap::new(); + + let open_and_draft_proposals = get_open_or_draft_proposals(git_repo, repo_ref).await?; + + for (_, (_, events)) in open_and_draft_proposals { + for event in events { + if [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind) { + if let Ok(c) = tag_value(&event, "c") { + if oids.contains(&&c) { + for tag in event.tags.as_slice() { + if tag.kind().eq(&nostr::event::TagKind::Clone) { + for clone_url in tag.as_slice().iter().skip(1) { + map.entry(c.clone()).or_default().push(clone_url.clone()); + } + } + } + } + } + } + } + } + + Ok(map) +} + pub fn make_commits_for_proposal( git_repo: &Repo, repo_ref: &RepoRef, @@ -128,7 +199,7 @@ pub fn make_commits_for_proposal( Ok(tip_commit_id) } -async fn fetch_open_or_draft_proposals( +async fn fetch_open_or_draft_proposals_from_patches( git_repo: &Repo, term: &console::Term, repo_ref: &RepoRef, @@ -140,12 +211,19 @@ async fn fetch_open_or_draft_proposals( let current_user = get_curent_user(git_repo)?; for refstr in proposal_refs.keys() { - if let Some((_, (_, patches))) = find_proposal_and_patches_by_branch_name( + if let Some((_, (_, events_to_apply))) = find_proposal_and_patches_by_branch_name( refstr, &open_and_draft_proposals, current_user.as_ref(), ) { - if let Err(error) = make_commits_for_proposal(git_repo, repo_ref, patches) { + if events_to_apply + .iter() + .any(|e| e.kind.eq(&KIND_PULL_REQUEST) || e.kind.eq(&KIND_PULL_REQUEST_UPDATE)) + { + // do nothing - we fetch these oids as part of run_fetch + } else if let Err(error) = + make_commits_for_proposal(git_repo, repo_ref, events_to_apply) + { term.write_line( format!("WARNING: failed to create branch for {refstr}, error: {error}",) .as_str(), @@ -429,6 +507,7 @@ fn fetch_from_git_server_url( remote_callbacks.credentials(auth.credentials(&git_config)); } fetch_options.remote_callbacks(remote_callbacks); + git_server_remote.download(oids, Some(&mut fetch_options))?; git_server_remote.disconnect()?; diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs index a90b28e..9e35b33 100644 --- a/src/bin/ngit/sub_commands/list.rs +++ b/src/bin/ngit/sub_commands/list.rs @@ -219,7 +219,11 @@ pub async fn launch() -> Result<()> { PromptChoiceParms::default() .with_prompt("this is new PR event kind which ngit doesnt yet support") .with_default(0) - .with_choices(vec!["back to proposals".to_string()]), + .with_choices(vec![ + // TODO enable checkout by fetching oids, creating / updating branch and + // checking out + "back to proposals".to_string(), + ]), )? { 0 => continue, _ => { diff --git a/src/lib/client.rs b/src/lib/client.rs index 1f3b08c..091d68d 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -49,7 +49,8 @@ use crate::{ git::{Repo, RepoActions, get_git_config_item}, git_events::{ KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_cover_letter, - event_is_patch_set_root, event_is_revision_root, status_kinds, + event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update, + status_kinds, }, login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, repo_ref::RepoRef, @@ -1824,6 +1825,7 @@ pub async fn get_proposals_and_revisions_from_cache( .await? .iter() .filter(|e| event_is_patch_set_root(e) || e.kind.eq(&KIND_PULL_REQUEST)) + .filter(|e| e.kind.eq(&Kind::GitPatch) || event_is_valid_pr_or_pr_update(e)) .cloned() .collect::>(); proposals.sort_by_key(|e| e.created_at); @@ -1874,7 +1876,11 @@ pub async fn get_all_proposal_patch_pr_pr_update_events_from_cache( .iter() .copied() .collect(); - commit_events.retain(|e| permissioned_users.contains(&e.pubkey)); + + commit_events.retain(|e| { + permissioned_users.contains(&e.pubkey) + && (e.kind.eq(&Kind::GitPatch) || event_is_valid_pr_or_pr_update(e)) + }); let revision_roots: HashSet = commit_events .iter() diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 7b25daf..09ec040 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs @@ -90,6 +90,19 @@ pub fn patch_supports_commit_ids(event: &Event) -> bool { .any(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq("commit-pgp-sig")) } +pub fn event_is_valid_pr_or_pr_update(event: &Event) -> bool { + [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind) + && event.tags.iter().any(|t| { + t.as_slice().len().gt(&1) + && t.as_slice()[0].eq("c") + && git2::Oid::from_str(&t.as_slice()[1]).is_ok() + }) + && event + .tags + .iter() + .any(|t| t.as_slice().len().gt(&1) && t.as_slice()[0].eq("clone")) +} + #[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_lines)] pub async fn generate_patch_event( -- cgit v1.2.3 From d1283a6b55826175423bd382a859928e0f92ffe7 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 22 Jul 2025 13:47:28 +0100 Subject: fix: remove blossom from grasp server detection a grasp server doesnt need to appear in repo announcement event `blossoms` tag as blossom has been removed from the grasp spec --- src/bin/ngit/sub_commands/init.rs | 1 - src/lib/repo_ref.rs | 23 +---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) (limited to 'src/lib') diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs index 1242e45..86d7f8a 100644 --- a/src/bin/ngit/sub_commands/init.rs +++ b/src/bin/ngit/sub_commands/init.rs @@ -269,7 +269,6 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { repo_ref.as_ref(), &args.relays, &args.clone_url, - &args.blossoms, &identifier, ); let mut selections: Vec = vec![true; options.len()]; // Initialize selections based on existing options diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs index 0236e34..bca4a3b 100644 --- a/src/lib/repo_ref.rs +++ b/src/lib/repo_ref.rs @@ -309,7 +309,7 @@ impl RepoRef { } pub fn grasp_servers(&self) -> Vec { - detect_existing_grasp_servers(Some(self), &[], &[], &[], &self.identifier) + detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier) } } @@ -593,7 +593,6 @@ pub fn detect_existing_grasp_servers( repo_ref: Option<&RepoRef>, args_relays: &[String], args_clone_url: &[String], - args_blossoms: &[String], identifier: &str, ) -> Vec { // Collect clone URLs from arguments or repo_ref @@ -617,18 +616,6 @@ pub fn detect_existing_grasp_servers( Vec::new() }; - // Collect blossom server URLs from arguments or repo_ref - let blossoms: Vec = if !args_blossoms.is_empty() { - args_blossoms - .iter() - .filter_map(|r| Url::parse(r).ok()) - .collect() - } else if let Some(repo) = repo_ref { - repo.blossoms.clone() - } else { - Vec::new() - }; - let mut existing_grasp_servers = Vec::new(); for url in &clone_urls { let Ok(formatted_as_grasp_server_url) = normalize_grasp_server_url(url) else { @@ -655,14 +642,6 @@ pub fn detect_existing_grasp_servers( continue; } - let matches_blossoms = blossoms.iter().any(|r| { - normalize_grasp_server_url(r.as_str()) - .is_ok_and(|r| r.eq(&formatted_as_grasp_server_url)) - }); - if !matches_blossoms { - continue; - } - existing_grasp_servers.push(formatted_as_grasp_server_url); } existing_grasp_servers -- cgit v1.2.3 From f4e1df4c718a3755ffe50e99946996729f3504e9 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 22 Jul 2025 17:14:21 +0100 Subject: feat(pr): generate pr event > oversized patch but only for new proposals --- src/bin/git_remote_nostr/push.rs | 126 ++++++++++++++++++++++++++++++++----- src/lib/client.rs | 26 +++++++- src/lib/git_events.rs | 130 +++++++++++++++++++++++++++++++++------ 3 files changed, 247 insertions(+), 35 deletions(-) (limited to 'src/lib') 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 use console::Term; use git::{RepoActions, sha1_to_oid}; use git_events::{ - generate_cover_letter_and_patch_events, generate_patch_event, get_commit_id_from_patch, + generate_cover_letter_and_patch_events, generate_patch_event, generate_unsigned_pr_event, + get_commit_id_from_patch, }; use git2::{Oid, Repository}; use ngit::{ cli_interactor::count_lines_per_msg_vec, - client::{self, get_event_from_cache_by_id}, + client::{self, get_event_from_cache_by_id, sign_draft_event}, git::{ self, nostr_url::{CloneUrl, NostrUrlDecoded}, @@ -25,10 +26,10 @@ use ngit::{ }, git_events::{self, event_to_cover_letter, get_event_root}, login::{self, user::UserRef}, - repo_ref::{self, get_repo_config_from_yaml, is_grasp_server}, + repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url}, repo_state, }; -use nostr::nips::nip10::Marker; +use nostr::{event::UnsignedEvent, nips::nip10::Marker}; use nostr_sdk::{ Event, EventBuilder, EventId, Kind, NostrSigner, PublicKey, RelayUrl, Tag, TagStandard, hashes::sha1::Hash as Sha1Hash, @@ -404,18 +405,11 @@ async fn process_proposal_refspecs( let (mut ahead, _) = git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; ahead.reverse(); - for patch in generate_cover_letter_and_patch_events( - None, - git_repo, - &ahead, - signer, - repo_ref, - &None, - &[], - ) - .await? + for event in + generate_patches_or_pr_event(git_repo, repo_ref, &ahead, user_ref, signer, term) + .await? { - events.push(patch); + events.push(event); } } } @@ -423,6 +417,108 @@ async fn process_proposal_refspecs( Ok((events, rejected_proposal_refspecs)) } +async fn generate_patches_or_pr_event( + git_repo: &Repo, + repo_ref: &RepoRef, + ahead: &[Sha1Hash], + user_ref: &UserRef, + signer: &Arc, + term: &Term, +) -> Result> { + let mut events: Vec = vec![]; + let use_pr = ahead.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 + } + }); + + 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_event( + git_repo, + repo_ref, + &user_ref.public_key, + 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(), + )?; + } 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() { + // TODO get fallback grasp 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, "Pull Request".to_string()).await?; + events.push(pr_event); + } else { + bail!("could not find a grasp server that accepts the Pull Request refs"); + } + } else { + for patch in generate_cover_letter_and_patch_events( + None, + git_repo, + ahead, + signer, + repo_ref, + &None, + &[], + ) + .await? + { + events.push(patch); + } + } + + Ok(events) +} + fn push_to_remote( git_repo: &Repo, 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 use mockall::*; use nostr::{ Event, - event::{TagKind, TagStandard}, + event::{TagKind, TagStandard, UnsignedEvent}, filter::Alphabet, nips::{nip01::Coordinate, nip19::Nip19Coordinate}, signer::SignerBackend, @@ -814,6 +814,30 @@ pub async fn sign_event( } } +pub async fn sign_draft_event( + draft_event: UnsignedEvent, + signer: &Arc, + description: String, +) -> Result { + if signer.backend() == SignerBackend::NostrConnect { + let term = console::Term::stderr(); + term.write_line(&format!( + "signing event ({description}) with remote signer..." + ))?; + let event = signer + .sign_event(draft_event) + .await + .context("failed to sign event")?; + term.clear_last_lines(1)?; + Ok(event) + } else { + signer + .sign_event(draft_event) + .await + .context("failed to sign event") + } +} + pub async fn fetch_public_key(signer: &Arc) -> Result { if signer.backend() == SignerBackend::NostrConnect { 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 @@ use std::{str::FromStr, sync::Arc}; use anyhow::{Context, Result, bail}; -use nostr::nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}; +use nostr::{ + event::UnsignedEvent, + nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}, +}; use nostr_sdk::{ Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind, TagStandard, hashes::sha1::Hash as Sha1Hash, @@ -347,6 +350,111 @@ pub fn event_tag_from_nip19_or_hex( } } +pub fn generate_unsigned_pr_event( + git_repo: &Repo, + repo_ref: &RepoRef, + signing_public_key: &PublicKey, + commit: &Sha1Hash, + clone_url_hint: &[&str], + mentions: &[nostr::Tag], +) -> Result { + let title = git_repo.get_commit_message_summary(commit)?; + + let description = { + let mut description = git_repo.get_commit_message(commit)?.trim().to_string(); + if let Some(remaining_description) = description.strip_prefix(&title) { + description = remaining_description.trim().to_string(); + } + description + }; + + let root_commit = git_repo + .get_root_commit() + .context("failed to get root commit of the repository")?; + + Ok(EventBuilder::new(KIND_PULL_REQUEST, description) + .tags( + [ + repo_ref + .maintainers + .iter() + .map(|m| { + Tag::from_standardized(TagStandard::Coordinate { + coordinate: Coordinate { + kind: nostr::Kind::GitRepoAnnouncement, + public_key: *m, + identifier: repo_ref.identifier.to_string(), + }, + relay_url: repo_ref.relays.first().cloned(), + uppercase: false, + }) + }) + .collect::>(), + mentions.to_vec(), + vec![ + Tag::from_standardized(TagStandard::Subject(title.clone())), + Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")), + vec![format!("{commit}")], + ), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")), + clone_url_hint + .iter() + .map(|s| s.to_string()) + .collect::>(), + ), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), + vec![format!("git Pull Request: {}", title.clone())], + ), + ], + if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) + { + vec![branch_name_tag] + } else { + vec![] + }, + repo_ref + .maintainers + .iter() + .map(|pk| Tag::public_key(*pk)) + .collect(), + ] + .concat(), + ) + .build(*signing_public_key)) +} + +fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option { + if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { + if !branch_name.eq("main") + && !branch_name.eq("master") + && !branch_name.eq("origin/main") + && !branch_name.eq("origin/master") + { + Some(Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), + vec![ + if let Some(branch_name) = branch_name.strip_prefix("pr/") { + branch_name.to_string() + } else { + branch_name + } + .chars() + .take(60) + .collect::(), + ], + )) + } else { + None + } + } else { + None + } +} + #[allow(clippy::too_many_lines)] pub async fn generate_cover_letter_and_patch_events( cover_letter_title_description: Option<(String, String)>, @@ -409,24 +517,8 @@ pub async fn generate_cover_letter_and_patch_events( // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding // a change like this, or the removal of this tag will require the actual branch name to be tracked // so pulling and pushing still work - if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { - if !branch_name.eq("main") - && !branch_name.eq("master") - && !branch_name.eq("origin/main") - && !branch_name.eq("origin/master") - { - vec![ - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), - vec![if let Some(branch_name) = branch_name.strip_prefix("pr/") { - branch_name.to_string() - } else { - branch_name - }.chars().take(60).collect::()], - ), - ] - } - else { vec![] } + if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) { + vec![branch_name_tag] } else { vec![] }, -- cgit v1.2.3 From ecfb54e1c89455590f816152b9efb722f0115bf1 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 23 Jul 2025 08:51:21 +0100 Subject: 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 --- src/bin/git_remote_nostr/push.rs | 155 ++++++++++++++++++++++++++++++++------- src/lib/git_events.rs | 143 +++++++++++++++++++++++------------- 2 files changed, 220 insertions(+), 78 deletions(-) (limited to 'src/lib') 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 use console::Term; use git::{RepoActions, sha1_to_oid}; use git_events::{ - generate_cover_letter_and_patch_events, generate_patch_event, generate_unsigned_pr_event, - get_commit_id_from_patch, + generate_cover_letter_and_patch_events, generate_patch_event, + generate_unsigned_pr_or_update_event, get_commit_id_from_patch, }; use git2::{Oid, Repository}; use ngit::{ @@ -24,7 +24,7 @@ use ngit::{ nostr_url::{CloneUrl, NostrUrlDecoded}, oid_to_shorthand_string, }, - git_events::{self, event_to_cover_letter, get_event_root}, + git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root}, login::{self, user::UserRef}, repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url}, repo_state, @@ -325,14 +325,14 @@ async fn process_proposal_refspecs( let (mut ahead, _) = git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; ahead.reverse(); - for patch in generate_cover_letter_and_patch_events( - None, + for patch in generate_patches_or_pr_event_or_pr_updates( git_repo, + repo_ref, &ahead, + user_ref, + Some(proposal), signer, - repo_ref, - &Some(proposal.id.to_string()), - &[], + term, ) .await? { @@ -356,6 +356,23 @@ async fn process_proposal_refspecs( }; let mut parent_patch = tip_patch.clone(); ahead.reverse(); + if proposal.kind.eq(&KIND_PULL_REQUEST) + || are_commits_too_big_for_patches(git_repo, &ahead) + { + for event in generate_patches_or_pr_event_or_pr_updates( + git_repo, + repo_ref, + &ahead, + user_ref, + Some(proposal), + signer, + term, + ) + .await? + { + events.push(event); + } + } for (i, commit) in ahead.iter().enumerate() { let new_patch = generate_patch_event( git_repo, @@ -405,9 +422,10 @@ async fn process_proposal_refspecs( let (mut ahead, _) = git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; ahead.reverse(); - for event in - generate_patches_or_pr_event(git_repo, repo_ref, &ahead, user_ref, signer, term) - .await? + for event in generate_patches_or_pr_event_or_pr_updates( + git_repo, repo_ref, &ahead, user_ref, None, signer, term, + ) + .await? { events.push(event); } @@ -417,25 +435,32 @@ async fn process_proposal_refspecs( Ok((events, rejected_proposal_refspecs)) } -async fn generate_patches_or_pr_event( +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, repo_ref: &RepoRef, ahead: &[Sha1Hash], user_ref: &UserRef, + root_proposal: Option<&Event>, signer: &Arc, term: &Term, ) -> Result> { let mut events: Vec = vec![]; - let use_pr = ahead.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 - } - }); + let use_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)) + || are_commits_too_big_for_patches(git_repo, ahead); if use_pr { let repo_grasps = repo_ref.grasp_servers(); @@ -450,10 +475,11 @@ async fn generate_patches_or_pr_event( let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event { unsigned_pr_event.clone() } else { - generate_unsigned_pr_event( + 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], &[], @@ -494,9 +520,30 @@ async fn generate_patches_or_pr_event( // for loop and continue } if let Some(unsigned_pr_event) = unsigned_pr_event { - let pr_event = - sign_draft_event(unsigned_pr_event, signer, "Pull Request".to_string()).await?; + 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!("could not find a grasp server that accepts the Pull Request refs"); } @@ -507,7 +554,7 @@ async fn generate_patches_or_pr_event( ahead, signer, repo_ref, - &None, + &root_proposal.map(|proposal| proposal.id.to_string()), &[], ) .await? @@ -1487,6 +1534,62 @@ 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/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( } } -pub fn generate_unsigned_pr_event( +pub fn generate_unsigned_pr_or_update_event( git_repo: &Repo, repo_ref: &RepoRef, signing_public_key: &PublicKey, + root_proposal: Option<&Event>, commit: &Sha1Hash, clone_url_hint: &[&str], mentions: &[nostr::Tag], @@ -372,59 +373,97 @@ pub fn generate_unsigned_pr_event( .get_root_commit() .context("failed to get root commit of the repository")?; - Ok(EventBuilder::new(KIND_PULL_REQUEST, description) - .tags( - [ - repo_ref - .maintainers - .iter() - .map(|m| { - Tag::from_standardized(TagStandard::Coordinate { - coordinate: Coordinate { - kind: nostr::Kind::GitRepoAnnouncement, - public_key: *m, - identifier: repo_ref.identifier.to_string(), - }, - relay_url: repo_ref.relays.first().cloned(), - uppercase: false, - }) + Ok(if root_proposal.is_some() { + EventBuilder::new(KIND_PULL_REQUEST_UPDATE, "") + } else { + EventBuilder::new(KIND_PULL_REQUEST, description) + } + .tags( + [ + repo_ref + .maintainers + .iter() + .map(|m| { + Tag::from_standardized(TagStandard::Coordinate { + coordinate: Coordinate { + kind: nostr::Kind::GitRepoAnnouncement, + public_key: *m, + identifier: repo_ref.identifier.to_string(), + }, + relay_url: repo_ref.relays.first().cloned(), + uppercase: false, }) - .collect::>(), - mentions.to_vec(), - vec![ - Tag::from_standardized(TagStandard::Subject(title.clone())), - Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")), - vec![format!("{commit}")], - ), - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")), - clone_url_hint - .iter() - .map(|s| s.to_string()) - .collect::>(), - ), - Tag::custom( + }) + .collect::>(), + mentions.to_vec(), + if let Some(root_proposal) = root_proposal { + [ + vec![Tag::custom( nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), - vec![format!("git Pull Request: {}", title.clone())], - ), - ], - if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) - { - vec![branch_name_tag] - } else { - vec![] - }, - repo_ref - .maintainers - .iter() - .map(|pk| Tag::public_key(*pk)) - .collect(), - ] - .concat(), - ) - .build(*signing_public_key)) + vec![format!("git Pull Request Update")], + )], + if root_proposal.kind.eq(&KIND_PULL_REQUEST) { + vec![ + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("E")), + vec![root_proposal.id], + ), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("P")), + vec![root_proposal.pubkey], + ), + ] + } else { + // root proposal is a Patch - so use e tag per nip34 spec + vec![Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("e")), + vec![root_proposal.id], + )] + }, + ] + .concat() + } else { + [ + vec![ + Tag::from_standardized(TagStandard::Subject(title.clone())), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), + vec![format!("git Pull Request: {}", title.clone())], + ), + ], + if let Some(branch_name_tag) = + make_branch_name_tag_from_check_out_branch(git_repo) + { + vec![branch_name_tag] + } else { + vec![] + }, + ] + .concat() + }, + vec![ + Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")), + vec![format!("{commit}")], + ), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")), + clone_url_hint + .iter() + .map(|s| s.to_string()) + .collect::>(), + ), + ], + repo_ref + .maintainers + .iter() + .map(|pk| Tag::public_key(*pk)) + .collect(), + ] + .concat(), + ) + .build(*signing_public_key)) } fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option { -- cgit v1.2.3 From dd5300b301292c3944aad4dcdecf4802307c7ea2 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 23 Jul 2025 13:53:18 +0100 Subject: refactor: add fallback grasp servers to client so that they can be used as part of push and send --- src/bin/ngit/sub_commands/init.rs | 11 ++--------- src/lib/client.rs | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 10 deletions(-) (limited to 'src/lib') diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs index 86d7f8a..52e43e9 100644 --- a/src/bin/ngit/sub_commands/init.rs +++ b/src/bin/ngit/sub_commands/init.rs @@ -252,14 +252,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { args.blossoms.clone() }; - let fallback_grasp_servers = - if let Ok(Some(s)) = git_repo.get_git_config_item("nostr.grasp-default-set", None) { - s.split(';') - .filter_map(|url| normalize_grasp_server_url(url).ok()) // Attempt to parse and filter out errors - .collect() - } else { - vec!["relay.ngit.dev".to_string(), "gitnostr.com".to_string()] - }; + let fallback_grasp_servers = client.get_fallback_grasp_servers(); let selected_grasp_servers = if has_server_and_relay_flags { // ignore so a script running `ngit init` can contiue without prompts @@ -275,7 +268,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { let empty = options.is_empty(); for fallback in fallback_grasp_servers { // Check if any option contains the fallback as a substring - if !options.iter().any(|option| option.contains(&fallback)) { + if !options.iter().any(|option| option.contains(fallback)) { options.push(fallback.clone()); // Add fallback if not found selections.push(empty); // mark as selected if no existing ngit relay otherwise not } diff --git a/src/lib/client.rs b/src/lib/client.rs index 3fe2b57..2bdea42 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -53,7 +53,7 @@ use crate::{ status_kinds, }, login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, - repo_ref::RepoRef, + repo_ref::{RepoRef, normalize_grasp_server_url}, repo_state::RepoState, }; @@ -64,6 +64,7 @@ pub struct Client { more_fallback_relays: Vec, blaster_relays: Vec, fallback_signer_relays: Vec, + fallback_grasp_servers: Vec, relays_not_to_retry: Arc>>, } @@ -101,6 +102,7 @@ pub trait Connect { fn get_more_fallback_relays(&self) -> &Vec; fn get_blaster_relays(&self) -> &Vec; fn get_fallback_signer_relays(&self) -> &Vec; + fn get_fallback_grasp_servers(&self) -> &Vec; async fn send_event_to<'a>( &self, git_repo_path: Option<&'a Path>, @@ -154,6 +156,7 @@ impl Connect for Client { more_fallback_relays: opts.more_fallback_relays, blaster_relays: opts.blaster_relays, fallback_signer_relays: opts.fallback_signer_relays, + fallback_grasp_servers: opts.fallback_grasp_servers, relays_not_to_retry: Arc::new(RwLock::new(HashMap::new())), } } @@ -208,6 +211,10 @@ impl Connect for Client { &self.fallback_signer_relays } + fn get_fallback_grasp_servers(&self) -> &Vec { + &self.fallback_grasp_servers + } + async fn send_event_to<'a>( &self, git_repo_path: Option<&'a Path>, @@ -692,6 +699,7 @@ pub struct Params { pub more_fallback_relays: Vec, pub blaster_relays: Vec, pub fallback_signer_relays: Vec, + pub fallback_grasp_servers: Vec, } impl Default for Params { @@ -734,6 +742,11 @@ impl Default for Params { } else { vec!["wss://relay.nsec.app".to_string()] }, + fallback_grasp_servers: if std::env::var("NGITTEST").is_ok() { + vec![] + } else { + vec!["relay.ngit.dev".to_string(), "gitnostr.com".to_string()] + }, } } } @@ -773,6 +786,17 @@ impl Params { .map(|relay_url| relay_url.to_string()) // Convert RelayUrl back to String .collect(); } + if let Ok(Some(grasp_default_servers)) = + get_git_config_item(git_repo, "nostr.grasp-default-set") + { + let new_default_grasp_servers: Vec = grasp_default_servers + .split(';') + .filter_map(|url| normalize_grasp_server_url(url).ok()) // Attempt to parse and filter out errors + .collect(); + if !new_default_grasp_servers.is_empty() { + params.fallback_grasp_servers = new_default_grasp_servers; + } + } } params } -- cgit v1.2.3 From 055316ba3e50ffc3efb9be5f60afda669d74e548 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 23 Jul 2025 14:02:09 +0100 Subject: refactor: Rename fallback relays and grasp servers Rename `params.fallback_relays` and `client.fallback_relays` to `relay_default_set`. Rename `params.fallback_grasp_servers` to `grasp_default_set`. This includes updating associated getters and usages across the codebase. --- src/bin/ngit/sub_commands/init.rs | 6 +++--- src/lib/client.rs | 40 +++++++++++++++++++-------------------- src/lib/login/fresh.rs | 4 ++-- 3 files changed, 25 insertions(+), 25 deletions(-) (limited to 'src/lib') diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs index 52e43e9..eaaf83d 100644 --- a/src/bin/ngit/sub_commands/init.rs +++ b/src/bin/ngit/sub_commands/init.rs @@ -229,7 +229,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { .map(std::string::ToString::to_string) .collect::>() } else { - client.get_fallback_relays().clone() + client.get_relay_default_set().clone() } } else { args.relays.clone() @@ -252,7 +252,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { args.blossoms.clone() }; - let fallback_grasp_servers = client.get_fallback_grasp_servers(); + let fallback_grasp_servers = client.get_grasp_default_set(); let selected_grasp_servers = if has_server_and_relay_flags { // ignore so a script running `ngit init` can contiue without prompts @@ -456,7 +456,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { let mut selections: Vec = vec![true; options.len()]; // add fallback relays as options - for relay in client.get_fallback_relays().clone() { + for relay in client.get_relay_default_set().clone() { if !options.iter().any(|r| r.contains(&relay)) && !formatted_selected_grasp_servers .iter() diff --git a/src/lib/client.rs b/src/lib/client.rs index 2bdea42..6f28cff 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -60,11 +60,11 @@ use crate::{ #[allow(clippy::struct_field_names)] pub struct Client { client: nostr_sdk::Client, - fallback_relays: Vec, + relay_default_set: Vec, more_fallback_relays: Vec, blaster_relays: Vec, fallback_signer_relays: Vec, - fallback_grasp_servers: Vec, + grasp_default_set: Vec, relays_not_to_retry: Arc>>, } @@ -98,11 +98,11 @@ pub trait Connect { async fn set_signer(&mut self, signer: Arc); async fn connect(&self, relay_url: &RelayUrl) -> Result<()>; async fn disconnect(&self) -> Result<()>; - fn get_fallback_relays(&self) -> &Vec; + fn get_relay_default_set(&self) -> &Vec; fn get_more_fallback_relays(&self) -> &Vec; fn get_blaster_relays(&self) -> &Vec; fn get_fallback_signer_relays(&self) -> &Vec; - fn get_fallback_grasp_servers(&self) -> &Vec; + fn get_grasp_default_set(&self) -> &Vec; async fn send_event_to<'a>( &self, git_repo_path: Option<&'a Path>, @@ -152,11 +152,11 @@ impl Connect for Client { .opts(Options::new().relay_limits(RelayLimits::disable())) .build() }, - fallback_relays: opts.fallback_relays, + relay_default_set: opts.relay_default_set, more_fallback_relays: opts.more_fallback_relays, blaster_relays: opts.blaster_relays, fallback_signer_relays: opts.fallback_signer_relays, - fallback_grasp_servers: opts.fallback_grasp_servers, + grasp_default_set: opts.grasp_default_set, relays_not_to_retry: Arc::new(RwLock::new(HashMap::new())), } } @@ -195,8 +195,8 @@ impl Connect for Client { Ok(()) } - fn get_fallback_relays(&self) -> &Vec { - &self.fallback_relays + fn get_relay_default_set(&self) -> &Vec { + &self.relay_default_set } fn get_more_fallback_relays(&self) -> &Vec { @@ -211,8 +211,8 @@ impl Connect for Client { &self.fallback_signer_relays } - fn get_fallback_grasp_servers(&self) -> &Vec { - &self.fallback_grasp_servers + fn get_grasp_default_set(&self) -> &Vec { + &self.grasp_default_set } async fn send_event_to<'a>( @@ -345,8 +345,8 @@ impl Connect for Client { trusted_maintainer_coordinate: Option<&'a Nip19Coordinate>, user_profiles: &HashSet, ) -> Result<(Vec>, MultiProgress)> { - let fallback_relays = &self - .fallback_relays + let relay_default_set = &self + .relay_default_set .iter() .filter_map(|r| RelayUrl::parse(r).ok()) .collect::>(); @@ -355,7 +355,7 @@ impl Connect for Client { git_repo_path, trusted_maintainer_coordinate, user_profiles, - fallback_relays.clone(), + relay_default_set.clone(), ) .await?; @@ -695,18 +695,18 @@ async fn get_events_of( pub struct Params { pub keys: Option, - pub fallback_relays: Vec, + pub relay_default_set: Vec, pub more_fallback_relays: Vec, pub blaster_relays: Vec, pub fallback_signer_relays: Vec, - pub fallback_grasp_servers: Vec, + pub grasp_default_set: Vec, } impl Default for Params { fn default() -> Self { Params { keys: None, - fallback_relays: if std::env::var("NGITTEST").is_ok() { + relay_default_set: if std::env::var("NGITTEST").is_ok() { vec![ "ws://localhost:8051".to_string(), "ws://localhost:8052".to_string(), @@ -742,7 +742,7 @@ impl Default for Params { } else { vec!["wss://relay.nsec.app".to_string()] }, - fallback_grasp_servers: if std::env::var("NGITTEST").is_ok() { + grasp_default_set: if std::env::var("NGITTEST").is_ok() { vec![] } else { vec!["relay.ngit.dev".to_string(), "gitnostr.com".to_string()] @@ -765,7 +765,7 @@ impl Params { .collect(); // elsewhere it is assumed this isn't empty if !new_default_relays.is_empty() { - params.fallback_relays = new_default_relays; + params.relay_default_set = new_default_relays; } } if let Ok(Some(relay_blasters)) = @@ -794,7 +794,7 @@ impl Params { .filter_map(|url| normalize_grasp_server_url(url).ok()) // Attempt to parse and filter out errors .collect(); if !new_default_grasp_servers.is_empty() { - params.fallback_grasp_servers = new_default_grasp_servers; + params.grasp_default_set = new_default_grasp_servers; } } } @@ -1995,7 +1995,7 @@ pub async fn send_events( silent: bool, ) -> Result<()> { let fallback = [ - client.get_fallback_relays().clone(), + client.get_relay_default_set().clone(), if events.iter().any(|e| e.kind.eq(&Kind::GitRepoAnnouncement)) { client.get_blaster_relays().clone() } else { diff --git a/src/lib/login/fresh.rs b/src/lib/login/fresh.rs index a169177..358045a 100644 --- a/src/lib/login/fresh.rs +++ b/src/lib/login/fresh.rs @@ -728,7 +728,7 @@ async fn signup( EventBuilder::metadata(&Metadata::new().name(name)).sign_with_keys(&keys)?; let relay_list = EventBuilder::relay_list( client - .get_fallback_relays() + .get_relay_default_set() .iter() .map(|s| (RelayUrl::parse(s).unwrap(), None)), ) @@ -738,7 +738,7 @@ async fn signup( client, None, vec![profile, relay_list], - client.get_fallback_relays().clone(), + client.get_relay_default_set().clone(), vec![], true, false, -- cgit v1.2.3 From 42498f5d4a67f7f17c01534b3dcd3ed99d724d5c Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 23 Jul 2025 17:04:52 +0100 Subject: fix(remote): patch to pr upgrade use pr kind and include e tag, etc per nip --- src/lib/git_events.rs | 202 +++++++++++++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 86 deletions(-) (limited to 'src/lib') diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 7bca63b..7cd64ab 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs @@ -359,9 +359,25 @@ pub fn generate_unsigned_pr_or_update_event( clone_url_hint: &[&str], mentions: &[nostr::Tag], ) -> Result { - let title = git_repo.get_commit_message_summary(commit)?; + let root_patch_cover_letter = if let Some(root_proposal) = root_proposal { + if root_proposal.kind.eq(&Kind::GitPatch) { + Some(event_to_cover_letter(root_proposal)?) + } else { + None + } + } else { + None + }; - let description = { + let title = 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 { + cl.description.clone() + } else { let mut description = git_repo.get_commit_message(commit)?.trim().to_string(); if let Some(remaining_description) = description.strip_prefix(&title) { description = remaining_description.trim().to_string(); @@ -373,97 +389,111 @@ pub fn generate_unsigned_pr_or_update_event( .get_root_commit() .context("failed to get root commit of the repository")?; - Ok(if root_proposal.is_some() { - EventBuilder::new(KIND_PULL_REQUEST_UPDATE, "") - } else { - EventBuilder::new(KIND_PULL_REQUEST, description) - } - .tags( + let pr_update_specific_tags = |root_proposal: &Event| { + vec![ + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), + vec![format!("git Pull Request Update")], + ), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("E")), + vec![root_proposal.id], + ), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("P")), + vec![root_proposal.pubkey], + ), + ] + }; + let pr_specific_tags = || { [ - repo_ref - .maintainers - .iter() - .map(|m| { - Tag::from_standardized(TagStandard::Coordinate { - coordinate: Coordinate { - kind: nostr::Kind::GitRepoAnnouncement, - public_key: *m, - identifier: repo_ref.identifier.to_string(), - }, - relay_url: repo_ref.relays.first().cloned(), - uppercase: false, - }) - }) - .collect::>(), - mentions.to_vec(), - if let Some(root_proposal) = root_proposal { - [ - vec![Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), - vec![format!("git Pull Request Update")], - )], - if root_proposal.kind.eq(&KIND_PULL_REQUEST) { - vec![ - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("E")), - vec![root_proposal.id], - ), - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("P")), - vec![root_proposal.pubkey], - ), - ] - } else { - // root proposal is a Patch - so use e tag per nip34 spec - vec![Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("e")), - vec![root_proposal.id], - )] - }, - ] - .concat() - } else { - [ - vec![ - Tag::from_standardized(TagStandard::Subject(title.clone())), - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), - vec![format!("git Pull Request: {}", title.clone())], - ), - ], - if let Some(branch_name_tag) = - make_branch_name_tag_from_check_out_branch(git_repo) - { - vec![branch_name_tag] - } else { - vec![] - }, - ] - .concat() - }, vec![ - Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")), - vec![format!("{commit}")], - ), + Tag::from_standardized(TagStandard::Subject(title.clone())), Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")), - clone_url_hint - .iter() - .map(|s| s.to_string()) - .collect::>(), + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), + vec![format!("git Pull Request: {}", title.clone())], ), ], - repo_ref - .maintainers - .iter() - .map(|pk| Tag::public_key(*pk)) - .collect(), + if let Some(cl) = &root_patch_cover_letter { + vec![ + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("e")), + vec![root_proposal.unwrap().id], + ), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), + vec![cl.branch_name_without_id_or_prefix.clone()], + ), + Tag::public_key(root_proposal.unwrap().pubkey), + ] + } else if let Some(branch_name_tag) = + make_branch_name_tag_from_check_out_branch(git_repo) + { + vec![branch_name_tag] + } else { + vec![] + }, ] - .concat(), + .concat() + }; + + Ok( + if root_proposal.is_some() && root_patch_cover_letter.is_none() { + EventBuilder::new(KIND_PULL_REQUEST_UPDATE, "") + } else { + EventBuilder::new(KIND_PULL_REQUEST, description) + } + .tags( + [ + repo_ref + .maintainers + .iter() + .map(|m| { + Tag::from_standardized(TagStandard::Coordinate { + coordinate: Coordinate { + kind: nostr::Kind::GitRepoAnnouncement, + public_key: *m, + identifier: repo_ref.identifier.to_string(), + }, + relay_url: repo_ref.relays.first().cloned(), + uppercase: false, + }) + }) + .collect::>(), + mentions.to_vec(), + if let Some(root_proposal) = root_proposal { + if root_patch_cover_letter.is_none() { + pr_update_specific_tags(root_proposal) + } else { + pr_specific_tags() + } + } else { + pr_specific_tags() + }, + vec![ + Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")), + vec![format!("{commit}")], + ), + Tag::custom( + nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")), + clone_url_hint + .iter() + .map(|s| s.to_string()) + .collect::>(), + ), + ], + repo_ref + .maintainers + .iter() + .map(|pk| Tag::public_key(*pk)) + .collect(), + ] + .concat(), + ) + .build(*signing_public_key), ) - .build(*signing_public_key)) } fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option { -- cgit v1.2.3 From 9357b62b9824299be6fc85b09f57d93d3902f79a Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 25 Jul 2025 11:48:22 +0100 Subject: refactor: abstract `get_status` for use by `ngit list` --- src/bin/git_remote_nostr/utils.rs | 58 ++++++++------------------------------- src/bin/ngit/sub_commands/list.rs | 5 +++- src/lib/git_events.rs | 56 ++++++++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 48 deletions(-) (limited to 'src/lib') diff --git a/src/bin/git_remote_nostr/utils.rs b/src/bin/git_remote_nostr/utils.rs index 8433967..2cb85bf 100644 --- a/src/bin/git_remote_nostr/utils.rs +++ b/src/bin/git_remote_nostr/utils.rs @@ -18,9 +18,8 @@ use ngit::{ nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, }, git_events::{ - KIND_PULL_REQUEST, event_is_revision_root, - get_pr_tip_event_or_most_recent_patch_with_ancestors, is_event_proposal_root_for_branch, - status_kinds, + 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, }; @@ -104,7 +103,10 @@ pub async fn get_open_or_draft_proposals( get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) .await? .iter() - .filter(|e| !event_is_revision_root(e)) + .filter(|e| + // If we wanted to treat to list Pull Requests that revise a Patch we would do this: + // e.kind.eq(&KIND_PULL_REQUEST) || + !event_is_revision_root(e)) .cloned() .collect(); @@ -124,48 +126,9 @@ pub async fn get_open_or_draft_proposals( }; let mut open_or_draft_proposals = HashMap::new(); - let get_status = |proposal: &Event| { - if let Some(e) = statuses - .iter() - .filter(|e| { - status_kinds().contains(&e.kind) - && e.tags.iter().any(|t| { - t.as_slice().len() > 1 && t.as_slice()[1].eq(&proposal.id.to_string()) - }) - && (proposal.pubkey.eq(&e.pubkey) || repo_ref.maintainers.contains(&e.pubkey)) - }) - .collect::>() - .first() - { - e.kind - } else { - Kind::GitStatusOpen - } - }; - - let is_proposal_pr_revision_of_patch = |proposal: &Event, patch: &Event| { - proposal.kind.eq(&KIND_PULL_REQUEST) - && proposal.tags.clone().into_iter().any(|t| { - t.as_slice().len() > 1 - && t.as_slice()[0].eq("e") - && t.as_slice()[1].eq(&patch.id.to_string()) - && [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&get_status(proposal)) - }) - }; - for proposal in &proposals { - let status = get_status(proposal); - if [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&status) - || // or patch has been revised as a PR which is open or draft - ( - status.eq(&Kind::GitStatusClosed) && - proposal.kind.eq(&Kind::GitPatch) && - proposals.iter() - .any(|p| { - is_proposal_pr_revision_of_patch(p, proposal) - }) - ) - { + let status = get_status(proposal, repo_ref, &statuses, &proposals); + if [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&status) { if let Ok(commits_events) = get_all_proposal_patch_pr_pr_update_events_from_cache( git_repo_path, repo_ref, @@ -196,7 +159,10 @@ pub async fn get_all_proposals( get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) .await? .iter() - .filter(|e| !event_is_revision_root(e)) + .filter(|e| + // If we wanted to treat to list Pull Requests that revise a Patch we would do this: + // e.kind.eq(&KIND_PULL_REQUEST) || + !event_is_revision_root(e)) .cloned() .collect(); diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs index 9e35b33..95e17f3 100644 --- a/src/bin/ngit/sub_commands/list.rs +++ b/src/bin/ngit/sub_commands/list.rs @@ -72,7 +72,10 @@ pub async fn launch() -> Result<()> { let proposals: Vec = proposals_and_revisions .iter() - .filter(|e| !event_is_revision_root(e)) + .filter(|e| + // If we wanted to treat to list Pull Requests that revise a Patch we would do this: + // e.kind.eq(&KIND_PULL_REQUEST) || + !event_is_revision_root(e)) .cloned() .collect(); diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 7cd64ab..2e1f215 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs @@ -835,7 +835,61 @@ pub fn is_event_proposal_root_for_branch( || cl .get_branch_name_with_pr_prefix_and_shorthand_id() .is_ok_and(|s| s.eq(&branch_name)) - }) && !event_is_revision_root(e)) + }) && ( + // If we wanted to treat to list Pull Requests that revise a Patch we would do this: + // Note: whilst this the the case elsewhere event_is_revision_root is used, there is more to + // think about here? + // e.kind.eq(&KIND_PULL_REQUEST) || + !event_is_revision_root(e) + )) +} + +pub fn get_status( + proposal: &Event, + repo_ref: &RepoRef, + all_status_in_repo: &[Event], + all_pr_roots_in_repo: &[Event], +) -> Kind { + let get_direct_status = |proposal: &Event| { + if let Some(e) = all_status_in_repo + .iter() + .filter(|e| { + status_kinds().contains(&e.kind) + && e.tags.iter().any(|t| { + t.as_slice().len() > 1 && t.as_slice()[1].eq(&proposal.id.to_string()) + }) + && (proposal.pubkey.eq(&e.pubkey) || repo_ref.maintainers.contains(&e.pubkey)) + }) + .collect::>() + .first() + { + e.kind + } else { + Kind::GitStatusOpen + } + }; + let is_proposal_pr_revision_of_patch = |proposal: &Event, patch: &Event| { + proposal.kind.eq(&KIND_PULL_REQUEST) + && proposal.tags.clone().into_iter().any(|t| { + t.as_slice().len() > 1 + && t.as_slice()[0].eq("e") + && t.as_slice()[1].eq(&patch.id.to_string()) + }) + }; + + let direct_status = get_direct_status(proposal); + if direct_status.eq(&Kind::GitStatusClosed) && proposal.kind.eq(&Kind::GitPatch) { + if let Some(pr_revision) = all_pr_roots_in_repo + .iter() + .find(|p| is_proposal_pr_revision_of_patch(p, proposal)) + { + get_direct_status(pr_revision) + } else { + direct_status + } + } else { + direct_status + } } #[cfg(test)] -- cgit v1.2.3 From 0cad465dd3f78bd6c680067d12d396d4782829bf Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 25 Jul 2025 15:52:19 +0100 Subject: fix(list): improve pr unsupport text and show a more helpful message when proposal can be checked out using the remote --- src/bin/ngit/sub_commands/list.rs | 35 ++++++++++++++++++++++++++++------- src/lib/git/mod.rs | 20 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) (limited to 'src/lib') diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs index c3c8c71..0083c91 100644 --- a/src/bin/ngit/sub_commands/list.rs +++ b/src/bin/ngit/sub_commands/list.rs @@ -206,13 +206,32 @@ pub async fn launch() -> Result<()> { { match Interactor::default().choice( PromptChoiceParms::default() - .with_prompt("this is new PR event kind which ngit doesnt yet support") + .with_prompt( + "this is new PR event kind which isn't supported in `ngit list` yet", + ) .with_default(0) - .with_choices(vec![ - // TODO enable checkout by fetching oids, creating / updating branch and - // checking out - "back to proposals".to_string(), - ]), + .with_choices( + if [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&selected_status) + && git_repo + .get_first_nostr_remote_when_in_ngit_binary() + .await + .is_ok_and(|r| r.is_some()) + { + vec![ + format!( + "I'll manually checkout the proposal at remote branch '{}'", + cover_letter + .get_branch_name_with_pr_prefix_and_shorthand_id() + .unwrap() + ), + // TODO fetch oids and follow similar logic for dealing with + // conflcts as with patches below + "back to proposals".to_string(), + ] + } else { + vec!["back to proposals".to_string()] + }, + ), )? { 0 => continue, _ => { @@ -251,7 +270,9 @@ pub async fn launch() -> Result<()> { ]), )? { 0 => { - println!("Some proposals are posted as patch without listing a parent commit\n"); + println!( + "Some proposals are posted as patch without listing a parent commit\n" + ); println!( "they are not anchored against a particular state of the code base like a standard patch or a pull request can be\n" ); diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index d4bf2f5..b275b49 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs @@ -10,6 +10,7 @@ use nostr_sdk::{ Tags, hashes::{Hash, sha1::Hash as Sha1Hash}, }; +use nostr_url::NostrUrlDecoded; use crate::git_events::{get_commit_id_from_patch, tag_value}; pub mod identify_ahead_behind; @@ -92,6 +93,10 @@ pub trait RepoActions { fn get_git_config_item(&self, item: &str, global: Option) -> Result>; fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>; fn remove_git_config_item(&self, item: &str, global: bool) -> Result; + #[allow(async_fn_in_trait)] + async fn get_first_nostr_remote_when_in_ngit_binary( + &self, + ) -> Result>; } impl RepoActions for Repo { @@ -796,6 +801,21 @@ impl RepoActions for Repo { Ok(true) } } + + async fn get_first_nostr_remote_when_in_ngit_binary( + &self, + ) -> Result> { + for remote_name in self.git_repo.remotes()?.iter().flatten() { + if let Some(remote_url) = self.git_repo.find_remote(remote_name)?.url() { + if let Ok(nostr_url_decoded) = + NostrUrlDecoded::parse_and_resolve(remote_url, &Some(self)).await + { + return Ok(Some((remote_name.to_string(), nostr_url_decoded))); + } + } + } + Ok(None) + } } fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { -- cgit v1.2.3