upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/send.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-08-04 11:50:39 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2025-08-05 09:23:01 +0100
commitf48677bad3f3dabb80992806e0e4c8ad4d45c716 (patch)
tree9d63debff0b602a15df56008cc739c087fbe8b26 /src/bin/ngit/sub_commands/send.rs
parentf76fe63da5f2c2f85215e86c8ecc63eda7c93902 (diff)
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.
Diffstat (limited to 'src/bin/ngit/sub_commands/send.rs')
-rw-r--r--src/bin/ngit/sub_commands/send.rs137
1 files changed, 90 insertions, 47 deletions
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};
4use console::Style; 4use console::Style;
5use ngit::{ 5use ngit::{
6 client::{Params, send_events}, 6 client::{Params, send_events},
7 git_events::{EventRefType, generate_cover_letter_and_patch_events}, 7 git_events::{EventRefType, KIND_PULL_REQUEST, generate_cover_letter_and_patch_events},
8 push::push_refs_and_generate_pr_or_pr_update_event,
9 utils::proposal_tip_is_pr_or_pr_update,
8}; 10};
9use nostr::{ToBech32, nips::nip19::Nip19Event}; 11use nostr::{ToBech32, event::Event, nips::nip19::Nip19Event};
10use nostr_sdk::hashes::sha1::Hash as Sha1Hash; 12use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
11 13
12use crate::{ 14use crate::{
@@ -60,12 +62,14 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re
60 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; 62 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
61 } 63 }
62 64
63 let (root_proposal_id, mention_tags) = 65 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
64 get_root_proposal_id_and_mentions_from_in_reply_to(git_repo.get_path()?, &args.in_reply_to) 66
67 let (root_proposal, mention_tags) =
68 get_root_proposal_and_mentions_from_in_reply_to(git_repo.get_path()?, &args.in_reply_to)
65 .await?; 69 .await?;
66 70
67 if let Some(root_ref) = args.in_reply_to.first() { 71 if let Some(root_ref) = args.in_reply_to.first() {
68 if root_proposal_id.is_some() { 72 if root_proposal.is_some() {
69 println!("creating proposal revision for: {root_ref}"); 73 println!("creating proposal revision for: {root_ref}");
70 } 74 }
71 } 75 }
@@ -112,7 +116,30 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re
112 &main_tip, 116 &main_tip,
113 )?; 117 )?;
114 118
115 let title = if args.no_cover_letter { 119 let as_pr = {
120 if let Some(root_proposal) = &root_proposal {
121 proposal_tip_is_pr_or_pr_update(git_repo_path, &repo_ref, &root_proposal.id).await?
122 } else {
123 false
124 }
125 } || git_repo.are_commits_too_big_for_patches(&commits);
126
127 let title = if as_pr {
128 match &args.title {
129 Some(t) => Some(t.clone()),
130 None => {
131 if root_proposal.is_none() {
132 Some(
133 Interactor::default()
134 .input(PromptInputParms::default().with_prompt("title"))?
135 .clone(),
136 )
137 } else {
138 None
139 }
140 }
141 }
142 } else if args.no_cover_letter {
116 None 143 None
117 } else { 144 } else {
118 match &args.title { 145 match &args.title {
@@ -142,7 +169,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re
142 t.clone() 169 t.clone()
143 } else { 170 } else {
144 Interactor::default() 171 Interactor::default()
145 .input(PromptInputParms::default().with_prompt("cover letter description"))? 172 .input(PromptInputParms::default().with_prompt("description"))?
146 .clone() 173 .clone()
147 }, 174 },
148 )) 175 ))
@@ -161,42 +188,58 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re
161 188
162 client.set_signer(signer.clone()).await; 189 client.set_signer(signer.clone()).await;
163 190
164 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
165
166 // oldest first 191 // oldest first
167 commits.reverse(); 192 commits.reverse();
168 193
169 let events = generate_cover_letter_and_patch_events( 194 let events = if as_pr {
170 cover_letter_title_description.clone(), 195 push_refs_and_generate_pr_or_pr_update_event(
171 &git_repo, 196 &git_repo,
172 &commits, 197 &repo_ref,
173 &signer, 198 commits.last().context("no commits")?,
174 &repo_ref, 199 &user_ref,
175 &root_proposal_id, 200 root_proposal.as_ref(),
176 &mention_tags, 201 &cover_letter_title_description,
177 ) 202 &signer,
178 .await?; 203 &console::Term::stdout(),
204 )
205 .await?
179 206
180 println!( 207 // TODO
181 "posting {} patch{} {} a covering letter...", 208 // - allow specifying clone url and ref
182 if cover_letter_title_description.is_none() { 209 } else {
183 events.len() 210 let events = generate_cover_letter_and_patch_events(
184 } else { 211 cover_letter_title_description.clone(),
185 events.len() - 1 212 &git_repo,
186 }, 213 &commits,
187 if cover_letter_title_description.is_none() && events.len().eq(&1) 214 &signer,
188 || cover_letter_title_description.is_some() && events.len().eq(&2) 215 &repo_ref,
189 { 216 &root_proposal.as_ref().map(|e| e.id.to_string()),
190 "" 217 &mention_tags,
191 } else { 218 )
192 "es" 219 .await?;
193 }, 220
194 if cover_letter_title_description.is_none() { 221 println!(
195 "without" 222 "posting {} patch{} {} a covering letter...",
196 } else { 223 if cover_letter_title_description.is_none() {
197 "with" 224 events.len()
198 } 225 } else {
199 ); 226 events.len() - 1
227 },
228 if cover_letter_title_description.is_none() && events.len().eq(&1)
229 || cover_letter_title_description.is_some() && events.len().eq(&2)
230 {
231 ""
232 } else {
233 "es"
234 },
235 if cover_letter_title_description.is_none() {
236 "without"
237 } else {
238 "with"
239 }
240 );
241 events
242 };
200 243
201 send_events( 244 send_events(
202 &client, 245 &client,
@@ -209,7 +252,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re
209 ) 252 )
210 .await?; 253 .await?;
211 254
212 if root_proposal_id.is_none() { 255 if root_proposal.is_none() {
213 if let Some(event) = events.first() { 256 if let Some(event) = events.first() {
214 let event_bech32 = if let Some(relay) = repo_ref.relays.first() { 257 let event_bech32 = if let Some(relay) = repo_ref.relays.first() {
215 Nip19Event { 258 Nip19Event {
@@ -376,11 +419,11 @@ fn summarise_commit_for_selection(git_repo: &Repo, commit: &Sha1Hash) -> Result<
376 )) 419 ))
377} 420}
378 421
379async fn get_root_proposal_id_and_mentions_from_in_reply_to( 422async fn get_root_proposal_and_mentions_from_in_reply_to(
380 git_repo_path: &Path, 423 git_repo_path: &Path,
381 in_reply_to: &[String], 424 in_reply_to: &[String],
382) -> Result<(Option<String>, Vec<nostr::Tag>)> { 425) -> Result<(Option<Event>, Vec<nostr::Tag>)> {
383 let root_proposal_id = if let Some(first) = in_reply_to.first() { 426 let root_proposal = if let Some(first) = in_reply_to.first() {
384 match event_tag_from_nip19_or_hex(first, "in-reply-to", EventRefType::Root, true, false)? 427 match event_tag_from_nip19_or_hex(first, "in-reply-to", EventRefType::Root, true, false)?
385 .as_standardized() 428 .as_standardized()
386 { 429 {
@@ -398,8 +441,8 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to(
398 .await?; 441 .await?;
399 442
400 if let Some(first) = events.iter().find(|e| e.id.eq(event_id)) { 443 if let Some(first) = events.iter().find(|e| e.id.eq(event_id)) {
401 if event_is_patch_set_root(first) { 444 if event_is_patch_set_root(first) || first.kind.eq(&KIND_PULL_REQUEST) {
402 Some(event_id.to_string()) 445 Some(first.clone())
403 } else { 446 } else {
404 None 447 None
405 } 448 }
@@ -415,7 +458,7 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to(
415 458
416 let mut mention_tags = vec![]; 459 let mut mention_tags = vec![];
417 for (i, reply_to) in in_reply_to.iter().enumerate() { 460 for (i, reply_to) in in_reply_to.iter().enumerate() {
418 if i.ne(&0) || root_proposal_id.is_none() { 461 if i.ne(&0) || root_proposal.is_none() {
419 mention_tags.push( 462 mention_tags.push(
420 event_tag_from_nip19_or_hex( 463 event_tag_from_nip19_or_hex(
421 reply_to, 464 reply_to,
@@ -431,7 +474,7 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to(
431 } 474 }
432 } 475 }
433 476
434 Ok((root_proposal_id, mention_tags)) 477 Ok((root_proposal, mention_tags))
435} 478}
436 479
437// TODO 480// TODO