upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/sub_commands/list.rs627
-rw-r--r--src/sub_commands/send.rs7
2 files changed, 582 insertions, 52 deletions
diff --git a/src/sub_commands/list.rs b/src/sub_commands/list.rs
index b8c2919..9d02eb1 100644
--- a/src/sub_commands/list.rs
+++ b/src/sub_commands/list.rs
@@ -1,3 +1,5 @@
1use std::{io::Write, ops::Add};
2
1use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
2 4
3use super::send::event_is_patch_set_root; 5use super::send::event_is_patch_set_root;
@@ -8,9 +10,12 @@ use crate::client::MockConnect;
8use crate::{ 10use crate::{
9 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, 11 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms},
10 client::Connect, 12 client::Connect,
11 git::{Repo, RepoActions}, 13 git::{str_to_sha1, Repo, RepoActions},
12 repo_ref::{self, RepoRef, REPO_REF_KIND}, 14 repo_ref::{self, RepoRef, REPO_REF_KIND},
13 sub_commands::send::{event_is_cover_letter, event_to_cover_letter, PATCH_KIND}, 15 sub_commands::send::{
16 commit_msg_from_patch_oneliner, event_is_cover_letter, event_to_cover_letter,
17 patch_supports_commit_ids, PATCH_KIND,
18 },
14 Cli, 19 Cli,
15}; 20};
16 21
@@ -56,64 +61,582 @@ pub async fn launch(_cli_args: &Cli, _args: &SubCommandArgs) -> Result<()> {
56 return Ok(()); 61 return Ok(());
57 } 62 }
58 63
59 let selected_index = Interactor::default().choice( 64 loop {
60 PromptChoiceParms::default() 65 let selected_index = Interactor::default().choice(
61 .with_prompt("all proposals") 66 PromptChoiceParms::default()
62 .with_choices( 67 .with_prompt("all proposals")
63 proposal_events 68 .with_choices(
64 .iter() 69 proposal_events
65 .map(|e| { 70 .iter()
66 if let Ok(cl) = event_to_cover_letter(e) { 71 .map(|e| {
67 cl.title 72 if let Ok(cl) = event_to_cover_letter(e) {
68 } else if let Ok(msg) = tag_value(e, "description") { 73 cl.title
69 msg.split('\n').collect::<Vec<&str>>()[0].to_string() 74 } else if let Ok(msg) = tag_value(e, "description") {
70 } else { 75 msg.split('\n').collect::<Vec<&str>>()[0].to_string()
71 e.id.to_string() 76 } else {
72 } 77 e.id.to_string()
73 }) 78 }
74 .collect(), 79 })
75 ), 80 .collect(),
76 )?; 81 ),
77 82 )?;
78 println!("finding commits..."); 83
79 84 let cover_letter = event_to_cover_letter(&proposal_events[selected_index])
80 let commits_events: Vec<nostr::Event> = 85 .context("cannot extract proposal details from proposal root event")?;
81 find_commits_for_proposal_root_event(&client, &proposal_events[selected_index], &repo_ref) 86
82 .await?; 87 println!("finding commits...");
83 88
84 confirm_checkout(&git_repo)?; 89 let commits_events: Vec<nostr::Event> = find_commits_for_proposal_root_event(
85 90 &client,
86 let most_recent_proposal_patch_chain = get_most_recent_patch_with_ancestors(commits_events) 91 &proposal_events[selected_index],
87 .context("cannot get most recent patch for proposal")?; 92 &repo_ref,
88 93 )
89 let branch_name: String = event_to_cover_letter(&proposal_events[selected_index]) 94 .await?;
90 .context("cannot assign a branch name as event is not a patch set root")? 95
91 .branch_name; 96 let Ok(most_recent_proposal_patch_chain) =
92 97 get_most_recent_patch_with_ancestors(commits_events.clone())
93 let applied = git_repo 98 else {
94 .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain) 99 if Interactor::default().confirm(
95 .context("cannot apply patch chain")?; 100 PromptConfirmParms::default()
96 101 .with_default(true)
97 if applied.is_empty() { 102 .with_prompt(
98 println!("checked out proposal branch. no new commits to pull"); 103 "cannot find any patches on this proposal. choose another proposal?",
99 } else { 104 ),
105 )? {
106 continue;
107 }
108 return Ok(());
109 };
110
111 let binding_patch_text_ref = format!("{} commits", most_recent_proposal_patch_chain.len());
112 let patch_text_ref = if most_recent_proposal_patch_chain.len().gt(&1) {
113 binding_patch_text_ref.as_str()
114 } else {
115 "1 commit"
116 };
117
118 let no_support_for_patches_as_branch = most_recent_proposal_patch_chain
119 .iter()
120 .any(|event| !patch_supports_commit_ids(event));
121
122 if no_support_for_patches_as_branch {
123 println!("{patch_text_ref}");
124 return match Interactor::default().choice(PromptChoiceParms::default().with_choices(
125 vec![
126 "learn why 'patch only' proposals can't be checked out".to_string(),
127 format!("apply to current branch with `git am`"),
128 format!("download to ./patches"),
129 "back".to_string(),
130 ],
131 ))? {
132 0 => {
133 println!("Some proposals are posted as 'patch only'\n");
134 println!(
135 "they are not anchored against a particular state of the code base like a standard proposal or a GitHub Pull Request can be\n"
136 );
137 println!(
138 "they are designed to reviewed by studying the diff (in a tool like gitworkshop.dev) and if acceptable by a maintainer, applied to the latest version of master with any conflicts resolved as the do so\n"
139 );
140 println!(
141 "this has proven to be a smoother workflow for large scale projects with a high frequency of changes, even when patches are exchanged via email\n"
142 );
143 println!(
144 "by default ngit posts proposals that support both the branch and patch model so either workflow can be used"
145 );
146 Interactor::default().choice(
147 PromptChoiceParms::default().with_choices(vec!["back".to_string()]),
148 )?;
149 continue;
150 }
151 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain),
152 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo),
153 3 => continue,
154 _ => {
155 bail!("unexpected choice")
156 }
157 };
158 }
159
160 let branch_exists = git_repo
161 .get_local_branch_names()
162 .context("gitlib2 will not show a list of local branch names")?
163 .iter()
164 .any(|n| n.eq(&cover_letter.branch_name));
165
166 let checked_out_proposal_branch = git_repo
167 .get_checked_out_branch_name()?
168 .eq(&cover_letter.branch_name);
169
170 let proposal_base_commit = str_to_sha1(&tag_value(
171 most_recent_proposal_patch_chain.last().context(
172 "there should be at least one patch as we have already checked for this",
173 )?,
174 "parent-commit",
175 )?)
176 .context("cannot get valid parent commit id from patch")?;
177
178 let (main_branch_name, master_tip) = git_repo.get_main_or_master_branch()?;
179
180 if !git_repo.does_commit_exist(&proposal_base_commit.to_string())? {
181 println!("your '{main_branch_name}' branch may not be up-to-date.");
182 println!("the proposal parent commit doesnt exist in your local repository.");
183 return match Interactor::default().choice(PromptChoiceParms::default().with_choices(
184 vec![
185 format!(
186 "manually run `git pull` on '{main_branch_name}' and select proposal again"
187 ),
188 format!("apply to current branch with `git am`"),
189 format!("download to ./patches"),
190 "back".to_string(),
191 ],
192 ))? {
193 0 | 3 => continue,
194 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain),
195 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo),
196 _ => {
197 bail!("unexpected choice")
198 }
199 };
200 }
201
202 let proposal_tip = str_to_sha1(
203 &get_commit_id_from_patch(most_recent_proposal_patch_chain.first().context(
204 "there should be at least one patch as we have already checked for this",
205 )?)
206 .context("cannot get valid commit_id from patch")?,
207 )
208 .context("cannot get valid commit_id from patch")?;
209
210 let (_, proposal_behind_main) =
211 git_repo.get_commits_ahead_behind(&master_tip, &proposal_base_commit)?;
212
213 // branch doesnt exist
214 if !branch_exists {
215 return match Interactor::default()
216 .choice(PromptChoiceParms::default().with_choices(vec![
217 format!(
218 "create and checkout proposal branch ({} ahead {} behind '{main_branch_name}')",
219 most_recent_proposal_patch_chain.len(),
220 proposal_behind_main.len(),
221 ),
222 format!("apply to current branch with `git am`"),
223 format!("download to ./patches"),
224 "back".to_string(),
225 ]))? {
226 0 => {
227 check_clean(&git_repo)?;
228 let _ = git_repo
229 .apply_patch_chain(
230 &cover_letter.branch_name,
231 most_recent_proposal_patch_chain,
232 )
233 .context("cannot apply patch chain")?;
234
235 println!(
236 "checked out proposal as '{}' branch",
237 cover_letter.branch_name
238 );
239 Ok(())
240 }
241 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain),
242 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo),
243 3 => continue,
244 _ => {
245 bail!("unexpected choice")
246 }
247 };
248 }
249
250 let local_branch_tip = git_repo.get_tip_of_local_branch(&cover_letter.branch_name)?;
251
252 // up-to-date
253 if proposal_tip.eq(&local_branch_tip) {
254 if checked_out_proposal_branch {
255 println!("branch checked out and up-to-date");
256 return match Interactor::default().choice(
257 PromptChoiceParms::default()
258 .with_choices(vec!["exit".to_string(), "back".to_string()]),
259 )? {
260 0 => Ok(()),
261 1 => continue,
262 _ => {
263 bail!("unexpected choice")
264 }
265 };
266 }
267
268 return match Interactor::default().choice(PromptChoiceParms::default().with_choices(
269 vec![
270 format!(
271 "checkout proposal branch ({} ahead {} behind '{main_branch_name}')",
272 most_recent_proposal_patch_chain.len(),
273 proposal_behind_main.len(),
274 ),
275 format!("apply to current branch with `git am`"),
276 format!("download to ./patches"),
277 "back".to_string(),
278 ],
279 ))? {
280 0 => {
281 check_clean(&git_repo)?;
282 git_repo.checkout(&cover_letter.branch_name)?;
283 println!(
284 "checked out proposal as '{}' branch",
285 cover_letter.branch_name
286 );
287 Ok(())
288 }
289 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain),
290 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo),
291 3 => continue,
292 _ => {
293 bail!("unexpected choice")
294 }
295 };
296 }
297
298 let (local_ahead_of_main, local_beind_main) =
299 git_repo.get_commits_ahead_behind(&master_tip, &local_branch_tip)?;
300
301 // new appendments to proposal
302 if let Some(index) = most_recent_proposal_patch_chain.iter().position(|patch| {
303 get_commit_id_from_patch(patch)
304 .unwrap_or_default()
305 .eq(&local_branch_tip.to_string())
306 }) {
307 return match Interactor::default().choice(PromptChoiceParms::default().with_choices(
308 vec![
309 format!("checkout proposal branch and apply {} appendments", &index,),
310 format!("apply to current branch with `git am`"),
311 format!("download to ./patches"),
312 "back".to_string(),
313 ],
314 ))? {
315 0 => {
316 check_clean(&git_repo)?;
317 git_repo.checkout(&cover_letter.branch_name)?;
318 let _ = git_repo
319 .apply_patch_chain(
320 &cover_letter.branch_name,
321 most_recent_proposal_patch_chain,
322 )
323 .context("cannot apply patch chain")?;
324 println!(
325 "checked out proposal branch and applied {} appendments ({} ahead {} behind '{main_branch_name}')",
326 &index,
327 local_ahead_of_main.len().add(&index),
328 local_beind_main.len(),
329 );
330 Ok(())
331 }
332 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain),
333 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo),
334 3 => continue,
335 _ => {
336 bail!("unexpected choice")
337 }
338 };
339 }
340
341 // tip of local in proposal history (new, ammended or rebased version but no
342 // local changes)
343 if commits_events.iter().any(|patch| {
344 get_commit_id_from_patch(patch)
345 .unwrap_or_default()
346 .eq(&local_branch_tip.to_string())
347 }) {
348 return match Interactor::default().choice(
349 PromptChoiceParms::default()
350 .with_choices(
351 vec![
352 format!(
353 "checkout new version of proposal branch ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}')",
354 most_recent_proposal_patch_chain.len(),
355 proposal_behind_main.len(),
356 local_ahead_of_main.len(),
357 local_beind_main.len(),
358 ),
359 format!(
360 "checkout existing outdated proposal branch ({} ahead {} behind '{main_branch_name}')",
361 local_ahead_of_main.len(),
362 local_beind_main.len(),
363 ),
364 format!("apply to current branch with `git am`"),
365 format!("download to ./patches"),
366 "back".to_string(),
367 ],
368 )
369 )? {
370 0 => {
371 check_clean(&git_repo)?;
372 git_repo.create_branch_at_commit(&cover_letter.branch_name, &proposal_base_commit.to_string())?;
373 git_repo.checkout(&cover_letter.branch_name)?;
374 let chain_length = most_recent_proposal_patch_chain.len();
375 let _ = git_repo
376 .apply_patch_chain(&cover_letter.branch_name, most_recent_proposal_patch_chain)
377 .context("cannot apply patch chain")?;
378 format!(
379 "checked out new version of proposal ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}')",
380 chain_length,
381 proposal_behind_main.len(),
382 local_ahead_of_main.len(),
383 local_beind_main.len(),
384 );
385 Ok(())
386 },
387 1 => {
388 check_clean(&git_repo)?;
389 git_repo.checkout(&cover_letter.branch_name)?;
390 format!(
391 "checked out old proposal in existing branch ({} ahead {} behind '{main_branch_name}')",
392 local_ahead_of_main.len(),
393 local_beind_main.len(),
394 );
395 Ok(())
396 },
397 2 => {launch_git_am_with_patches(most_recent_proposal_patch_chain)},
398 3 => {save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo)},
399 4 => { continue },
400 _ => { bail!("unexpected choice")}
401 };
402 }
403
404 // tip of proposal in branch in history (local appendments made)
405 if let Ok((local_ahead_of_proposal, _)) =
406 git_repo.get_commits_ahead_behind(&proposal_tip, &local_branch_tip)
407 {
408 println!(
409 "local proposal branch exists with {} unpublished commits on top of the most up-to-date version of the proposal",
410 local_ahead_of_proposal.len()
411 );
412 return match Interactor::default().choice(
413 PromptChoiceParms::default()
414 .with_choices(
415 vec![
416 format!(
417 "checkout proposal branch with {} unpublished commits ({} ahead {} behind '{main_branch_name}')",
418 local_ahead_of_proposal.len(),
419 local_ahead_of_main.len(),
420 proposal_behind_main.len(),
421 ),
422 "back".to_string(),
423 ],
424 )
425 )? {
426 0 => {
427 git_repo.checkout(&cover_letter.branch_name)?;
428 format!(
429 "checked out proposal branch with {} unpublished commits ({} ahead {} behind '{main_branch_name}')",
430 local_ahead_of_proposal.len(),
431 local_ahead_of_main.len(),
432 proposal_behind_main.len(),
433 );
434 Ok(())
435
436 },
437 1 => { continue },
438 _ => { bail!("unexpected choice")}
439 };
440 }
441
442 // if tip of proposal commits exist (were once part of branch but have been
443 // ammended and git clean up job hasn't removed them)
444 if git_repo.does_commit_exist(&proposal_tip.to_string())? {
445 println!(
446 "you have previously applied the latest version of the proposal ({} ahead {} behind '{main_branch_name}') but your local proposal branch has other unpublished changes ({} ahead {} behind '{main_branch_name}')",
447 most_recent_proposal_patch_chain.len(),
448 proposal_behind_main.len(),
449 local_ahead_of_main.len(),
450 local_beind_main.len(),
451 );
452 return match Interactor::default().choice(
453 PromptChoiceParms::default()
454 .with_choices(
455 vec![
456 format!(
457 "checkout local branch with unpublished changes ({} ahead {} behind '{main_branch_name}')",
458 local_ahead_of_main.len(),
459 local_beind_main.len(),
460 ),
461 format!(
462 "discard local branch with old version ({} ahead {} behind '{main_branch_name}') and checkout latest published version ({} ahead {} behind '{main_branch_name}')",
463 most_recent_proposal_patch_chain.len(),
464 proposal_behind_main.len(),
465 local_ahead_of_main.len(),
466 local_beind_main.len(),
467 ),
468 format!("apply to current branch with `git am`"),
469 format!("download to ./patches"),
470 "back".to_string(),
471 ],
472 )
473 )? {
474 0 => {
475 check_clean(&git_repo)?;
476 git_repo.checkout(&cover_letter.branch_name)?;
477 format!(
478 "checked out old proposal in existing branch ({} ahead {} behind '{main_branch_name}')",
479 local_ahead_of_main.len(),
480 local_beind_main.len(),
481 );
482 Ok(())
483 },
484 1 => {
485 check_clean(&git_repo)?;
486 git_repo.create_branch_at_commit(&cover_letter.branch_name, &proposal_base_commit.to_string())?;
487 git_repo.checkout(&cover_letter.branch_name)?;
488 let chain_length = most_recent_proposal_patch_chain.len();
489 let _ = git_repo
490 .apply_patch_chain(&cover_letter.branch_name, most_recent_proposal_patch_chain)
491 .context("cannot apply patch chain")?;
492 format!(
493 "checked out new version of proposal ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}')",
494 chain_length,
495 proposal_behind_main.len(),
496 local_ahead_of_main.len(),
497 local_beind_main.len(),
498 );
499 Ok(())
500 },
501 2 => {launch_git_am_with_patches(most_recent_proposal_patch_chain)},
502 3 => {save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo)},
503 4 => { continue },
504 _ => { bail!("unexpected choice")}
505 };
506 }
507
100 println!( 508 println!(
101 "checked out proposal branch. pulled {} new commits", 509 "your local proposal branch ({} ahead {} behind '{main_branch_name}') has conflicting changes with the latest published proposal ({} ahead {} behind '{main_branch_name}')",
102 applied.len(), 510 local_ahead_of_main.len(),
511 local_beind_main.len(),
512 most_recent_proposal_patch_chain.len(),
513 proposal_behind_main.len(),
103 ); 514 );
515
516 return match Interactor::default().choice(
517 PromptChoiceParms::default()
518 .with_choices(
519 vec![
520 format!(
521 "checkout local branch with unpublished changes ({} ahead {} behind '{main_branch_name}')",
522 local_ahead_of_main.len(),
523 local_beind_main.len(),
524 ),
525 format!(
526 "discard local branch with unpublished version ({} ahead {} behind '{main_branch_name}') and checkout latest published version ({} ahead {} behind '{main_branch_name}'). consider creating a temporary branch with your existing unchanges first.",
527 most_recent_proposal_patch_chain.len(),
528 proposal_behind_main.len(),
529 local_ahead_of_main.len(),
530 local_beind_main.len(),
531 ),
532 format!("apply to current branch with `git am`"),
533 format!("download to ./patches"),
534 "back".to_string(),
535 ],
536 )
537 )? {
538 0 => {
539 check_clean(&git_repo)?;
540 git_repo.checkout(&cover_letter.branch_name)?;
541 format!(
542 "checked out old proposal in existing branch ({} ahead {} behind '{main_branch_name}')",
543 local_ahead_of_main.len(),
544 local_beind_main.len(),
545 );
546 Ok(())
547 },
548 1 => {
549 check_clean(&git_repo)?;
550 git_repo.create_branch_at_commit(&cover_letter.branch_name, &proposal_base_commit.to_string())?;
551 git_repo.checkout(&cover_letter.branch_name)?;
552 let chain_length = most_recent_proposal_patch_chain.len();
553 let _ = git_repo
554 .apply_patch_chain(&cover_letter.branch_name, most_recent_proposal_patch_chain)
555 .context("cannot apply patch chain")?;
556 format!(
557 "checked out new version of proposal ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}'). consider creating a temporary branch with your existing unchanges first.",
558 chain_length,
559 proposal_behind_main.len(),
560 local_ahead_of_main.len(),
561 local_beind_main.len(),
562 );
563 Ok(())
564 },
565 2 => {launch_git_am_with_patches(most_recent_proposal_patch_chain)},
566 3 => {save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo)},
567 4 => { continue },
568 _ => { bail!("unexpected choice")}
569 };
104 } 570 }
571}
572
573fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> {
574 println!("applying to current branch with `git am`");
575 // TODO: add PATCH x/n to appended patches
576 patches.reverse();
577
578 let mut am = std::process::Command::new("git")
579 .arg("am")
580 .stdin(std::process::Stdio::piped())
581 .stdout(std::process::Stdio::inherit())
582 .stderr(std::process::Stdio::inherit())
583 .spawn()
584 .context("failed to spawn git am")?;
585
586 let stdin = am
587 .stdin
588 .as_mut()
589 .context("git am process failed to take stdin")?;
590
591 for patch in patches {
592 stdin
593 .write(format!("{}\n\n", patch.content).as_bytes())
594 .context("failed to write patch content into git am stdin buffer")?;
595 }
596 stdin.flush()?;
597 let output = am
598 .wait_with_output()
599 .context("failed to read git am stdout")?;
600 print!("{:?}", output.stdout);
105 Ok(()) 601 Ok(())
106} 602}
107 603
108fn confirm_checkout(git_repo: &Repo) -> Result<()> { 604fn event_id_extra_shorthand(event: &nostr::Event) -> String {
109 if !Interactor::default().confirm( 605 event.id.to_string()[..5].to_string()
110 PromptConfirmParms::default() 606}
111 .with_prompt("check out branch?") 607
112 .with_default(true), 608fn save_patches_to_dir(mut patches: Vec<nostr::Event>, git_repo: &Repo) -> Result<()> {
113 )? { 609 // TODO: add PATCH x/n to appended patches
114 bail!("Exiting..."); 610 patches.reverse();
611 let path = git_repo.get_path()?.join("patches");
612 std::fs::create_dir_all(&path)?;
613 let id = event_id_extra_shorthand(
614 patches
615 .first()
616 .context("there must be at least one patch to save")?,
617 );
618 for (i, patch) in patches.iter().enumerate() {
619 let path = path.join(format!(
620 "{}-{:0>4}-{}.patch",
621 &id,
622 i.add(&1),
623 commit_msg_from_patch_oneliner(patch)?
624 ));
625 let mut file = std::fs::OpenOptions::new()
626 .create(true)
627 .write(true)
628 .truncate(true)
629 .open(path)
630 .context("open new patch file with write and truncate options")?;
631 file.write_all(patch.content().as_bytes())?;
632 file.write_all("\n\n".as_bytes())?;
633 file.flush()?;
115 } 634 }
635 println!("created {} patch files in ./patches/{id}-*", patches.len());
636 Ok(())
637}
116 638
639fn check_clean(git_repo: &Repo) -> Result<()> {
117 if git_repo.has_outstanding_changes()? { 640 if git_repo.has_outstanding_changes()? {
118 bail!( 641 bail!(
119 "cannot pull proposal branch when repository is not clean. discard or stash (un)staged changes and try again." 642 "cannot pull proposal branch when repository is not clean. discard or stash (un)staged changes and try again."
diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs
index c3b3fda..004d263 100644
--- a/src/sub_commands/send.rs
+++ b/src/sub_commands/send.rs
@@ -532,6 +532,13 @@ pub fn event_is_patch_set_root(event: &nostr::Event) -> bool {
532 event.kind.as_u64().eq(&PATCH_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("root")) 532 event.kind.as_u64().eq(&PATCH_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("root"))
533} 533}
534 534
535pub fn patch_supports_commit_ids(event: &nostr::Event) -> bool {
536 event.kind.as_u64().eq(&PATCH_KIND)
537 && event
538 .iter_tags()
539 .any(|t| t.as_vec()[0].eq("commit-pgp-sig"))
540}
541
535#[allow(clippy::too_many_arguments)] 542#[allow(clippy::too_many_arguments)]
536pub fn generate_patch_event( 543pub fn generate_patch_event(
537 git_repo: &Repo, 544 git_repo: &Repo,