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:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-02-13 14:52:24 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2024-02-13 15:55:54 +0000
commitcf319efc6dcdc6c54564cb84e13218edbf3643fa (patch)
treeccccf807fac6c2ab242b2d6bb322c679ae5b94f7 /src
parent3112576195aef212622d27ad9164336796c1953e (diff)
feat!: nip34 make pr event optional
use first patch as thread root if pr event isn't present. begin renaming pr event to cover letter. fix patch ordering upon creation. patches were in youngest first order which caused: - `PATCH n/t`to be in reverse order - the youngest patch was the marked root - oldest patch replied to the youngest fix finding most recent patch event. when a patch in a set is the most recent it will share a created_at with other patches. previously the first patch recieved from relay in the set would be used. now it finds the first patch with that created_at which isn't also a parent of another patch with the same created_at.
Diffstat (limited to 'src')
-rw-r--r--src/git.rs5
-rw-r--r--src/sub_commands/prs/create.rs171
-rw-r--r--src/sub_commands/prs/list.rs215
-rw-r--r--src/sub_commands/pull.rs62
-rw-r--r--src/sub_commands/push.rs64
5 files changed, 291 insertions, 226 deletions
diff --git a/src/git.rs b/src/git.rs
index 24afe76..cd42724 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -1265,6 +1265,7 @@ mod tests {
1265 &RepoRef::try_from(generate_repo_ref_event()).unwrap(), 1265 &RepoRef::try_from(generate_repo_ref_event()).unwrap(),
1266 None, 1266 None,
1267 None, 1267 None,
1268 None,
1268 ) 1269 )
1269 } 1270 }
1270 fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> { 1271 fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> {
@@ -1418,9 +1419,7 @@ mod tests {
1418 let git_repo = Repo::from_path(&original_repo.dir)?; 1419 let git_repo = Repo::from_path(&original_repo.dir)?;
1419 1420
1420 let mut events = generate_pr_and_patch_events( 1421 let mut events = generate_pr_and_patch_events(
1421 // Some(("test".to_string(), "test".to_string())), 1422 Some(("test".to_string(), "test".to_string())),
1422 "title",
1423 "description",
1424 &git_repo, 1423 &git_repo,
1425 &vec![oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)], 1424 &vec![oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)],
1426 &TEST_KEY_1_KEYS, 1425 &TEST_KEY_1_KEYS,
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs
index 83a3942..e5a7c1e 100644
--- a/src/sub_commands/prs/create.rs
+++ b/src/sub_commands/prs/create.rs
@@ -5,6 +5,7 @@ use futures::future::join_all;
5use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 5use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
6use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind}; 6use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind};
7 7
8use super::list::tag_value;
8#[cfg(not(test))] 9#[cfg(not(test))]
9use crate::client::Client; 10use crate::client::Client;
10#[cfg(test)] 11#[cfg(test)]
@@ -32,6 +33,9 @@ pub struct SubCommandArgs {
32 #[clap(long)] 33 #[clap(long)]
33 /// destination branch (defaults to main or master) 34 /// destination branch (defaults to main or master)
34 to_branch: Option<String>, 35 to_branch: Option<String>,
36 /// don't ask about a cover letter
37 #[arg(long, action)]
38 no_cover_letter: bool,
35} 39}
36 40
37#[allow(clippy::too_many_lines)] 41#[allow(clippy::too_many_lines)]
@@ -42,7 +46,7 @@ pub async fn launch(
42) -> Result<()> { 46) -> Result<()> {
43 let git_repo = Repo::discover().context("cannot find a git repository")?; 47 let git_repo = Repo::discover().context("cannot find a git repository")?;
44 48
45 let (from_branch, to_branch, ahead, behind) = 49 let (from_branch, to_branch, mut ahead, behind) =
46 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?; 50 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?;
47 51
48 if ahead.is_empty() { 52 if ahead.is_empty() {
@@ -79,34 +83,44 @@ pub async fn launch(
79 ); 83 );
80 } 84 }
81 85
82 let title = match &args.title { 86 let title = if args.no_cover_letter {
83 Some(t) => t.clone(), 87 None
84 None => Interactor::default() 88 } else {
85 .input(PromptInputParms::default().with_prompt("title"))? 89 match &args.title {
86 .clone(), 90 Some(t) => Some(t.clone()),
91 None => {
92 if Interactor::default().confirm(
93 PromptConfirmParms::default()
94 .with_default(false)
95 .with_prompt("include cover letter?"),
96 )? {
97 Some(
98 Interactor::default()
99 .input(PromptInputParms::default().with_prompt("title"))?
100 .clone(),
101 )
102 } else {
103 None
104 }
105 }
106 }
87 }; 107 };
88 108
89 let description = match &args.description { 109 let cover_letter_title_description = if let Some(title) = title {
90 Some(t) => t.clone(), 110 Some((
91 None => Interactor::default() 111 title,
92 .input(PromptInputParms::default().with_prompt("description (Optional)"))?, 112 if let Some(t) = &args.description {
113 t.clone()
114 } else {
115 Interactor::default()
116 .input(PromptInputParms::default().with_prompt("cover letter description"))?
117 .clone()
118 },
119 ))
120 } else {
121 None
93 }; 122 };
94 123
95 // let cover_letter_title_description = if let Some(title) = title {
96 // Some((
97 // title,
98 // if let Some(t) = &args.description {
99 // t.clone()
100 // } else {
101 // Interactor::default()
102 // .input(PromptInputParms::default().with_prompt("cover letter
103 // description"))? .clone()
104 // },
105 // ))
106 // } else {
107 // None
108 // };
109
110 #[cfg(not(test))] 124 #[cfg(not(test))]
111 let mut client = Client::default(); 125 let mut client = Client::default();
112 #[cfg(test)] 126 #[cfg(test)]
@@ -127,10 +141,11 @@ pub async fn launch(
127 ) 141 )
128 .await?; 142 .await?;
129 143
144 // oldest first
145 ahead.reverse();
146
130 let events = generate_pr_and_patch_events( 147 let events = generate_pr_and_patch_events(
131 // cover_letter_title_description, 148 cover_letter_title_description.clone(),
132 &title,
133 &description,
134 &git_repo, 149 &git_repo,
135 &ahead, 150 &ahead,
136 &keys, 151 &keys,
@@ -138,8 +153,17 @@ pub async fn launch(
138 )?; 153 )?;
139 154
140 println!( 155 println!(
141 "posting 1 pull request with {} commits...", 156 "posting {} patches {} a covering letter...",
142 events.len() - 1 157 if cover_letter_title_description.is_none() {
158 events.len()
159 } else {
160 events.len() - 1
161 },
162 if cover_letter_title_description.is_none() {
163 "without"
164 } else {
165 "with"
166 }
143 ); 167 );
144 168
145 send_events( 169 send_events(
@@ -329,9 +353,7 @@ pub static PR_KIND: u64 = 318;
329pub static PATCH_KIND: u64 = 1617; 353pub static PATCH_KIND: u64 = 1617;
330 354
331pub fn generate_pr_and_patch_events( 355pub fn generate_pr_and_patch_events(
332 title: &str, 356 cover_letter_title_description: Option<(String, String)>,
333 description: &str,
334 // cover_letter_title_description: Option<(String, String)>,
335 git_repo: &Repo, 357 git_repo: &Repo,
336 commits: &Vec<Sha1Hash>, 358 commits: &Vec<Sha1Hash>,
337 keys: &nostr::Keys, 359 keys: &nostr::Keys,
@@ -343,8 +365,7 @@ pub fn generate_pr_and_patch_events(
343 365
344 let mut events = vec![]; 366 let mut events = vec![];
345 367
346 // if let Some((title, description)) = cover_letter_title_description { 368 if let Some((title, description)) = cover_letter_title_description {
347 if !title.is_empty() {
348 events.push(EventBuilder::new( 369 events.push(EventBuilder::new(
349 nostr::event::Kind::Custom(PR_KIND), 370 nostr::event::Kind::Custom(PR_KIND),
350 format!( 371 format!(
@@ -400,6 +421,15 @@ pub fn generate_pr_and_patch_events(
400 } else { 421 } else {
401 Some(((i + 1).try_into()?, commits.len().try_into()?)) 422 Some(((i + 1).try_into()?, commits.len().try_into()?))
402 }, 423 },
424 if events.is_empty() {
425 if let Ok(branch_name) = git_repo.get_checked_out_branch_name() {
426 Some(branch_name)
427 } else {
428 None
429 }
430 } else {
431 None
432 },
403 ) 433 )
404 .context("failed to generate patch event")?, 434 .context("failed to generate patch event")?,
405 ); 435 );
@@ -410,36 +440,72 @@ pub fn generate_pr_and_patch_events(
410pub struct CoverLetter { 440pub struct CoverLetter {
411 pub title: String, 441 pub title: String,
412 pub description: String, 442 pub description: String,
413 pub branch_name: Option<String>, 443 pub branch_name: String,
414} 444}
415 445
416fn event_is_cover_letter(event: &nostr::Event) -> bool { 446pub fn event_is_cover_letter(event: &nostr::Event) -> bool {
417 event.kind.as_u64().eq(&PR_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter")) 447 event.kind.as_u64().eq(&PR_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter"))
418} 448}
419pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { 449pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> {
420 if !event_is_cover_letter(event) { 450 if !event_is_patch_set_root(event) {
421 bail!("event is not a cover letter") 451 bail!("event is not a patch set root event (root patch or cover letter)")
422 } 452 }
423 let title_index = event 453 let title_index = event
424 .content 454 .content
425 .find("] ") 455 .find("] ")
426 .context("event is not formatted as a cover letter patch")? 456 .context("event is not formatted as a patch or cover letter")?
427 + 2; 457 + 2;
428 let description_index = event.content[title_index..] 458 let description_index = event.content[title_index..]
429 .find('\n') 459 .find('\n')
430 .unwrap_or(event.content.len() - 1 - title_index) 460 .unwrap_or(event.content.len() - 1 - title_index)
431 + title_index; 461 + title_index;
432 462
463 let title = if let Ok(msg) = tag_value(event, "description") {
464 msg.split('\n').collect::<Vec<&str>>()[0].to_string()
465 } else {
466 event.content[title_index..description_index].to_string()
467 };
468
469 // note: if the description field is removed from patch events like in gitstr,
470 // then this will show entire patch. I'm not sure it is ever displayed though
471 let description = if let Ok(msg) = tag_value(event, "description") {
472 if let Some((_before, after)) = msg.split_once('\n') {
473 after.trim().to_string()
474 } else {
475 String::new()
476 }
477 } else {
478 event.content[description_index..].trim().to_string()
479 };
480
433 Ok(CoverLetter { 481 Ok(CoverLetter {
434 title: event.content[title_index..description_index].to_string(), 482 title: title.clone(),
435 description: event.content[description_index..].trim().to_string(), 483 description,
436 branch_name: event 484 // TODO should this be prefixed by format!("{}-"e.id.to_string()[..5]?)
437 .iter_tags() 485 branch_name: if let Ok(name) = tag_value(event, "branch-name") {
438 .find(|t| t.as_vec()[0].eq("branch-name")) 486 name
439 .map(|tag| tag.as_vec()[1].clone()), 487 } else {
488 let s = title
489 .replace(' ', "-")
490 .chars()
491 .map(|c| {
492 if c.is_ascii_alphanumeric() || c.eq(&'/') {
493 c
494 } else {
495 '-'
496 }
497 })
498 .collect();
499 s
500 },
440 }) 501 })
441} 502}
442 503
504pub fn event_is_patch_set_root(event: &nostr::Event) -> bool {
505 (event.kind.as_u64().eq(&PR_KIND) || event.kind.as_u64().eq(&PATCH_KIND))
506 && event.iter_tags().any(|t| t.as_vec()[1].eq("root"))
507}
508
443#[allow(clippy::too_many_arguments)] 509#[allow(clippy::too_many_arguments)]
444pub fn generate_patch_event( 510pub fn generate_patch_event(
445 git_repo: &Repo, 511 git_repo: &Repo,
@@ -450,6 +516,7 @@ pub fn generate_patch_event(
450 repo_ref: &RepoRef, 516 repo_ref: &RepoRef,
451 parent_patch_event_id: Option<nostr::EventId>, 517 parent_patch_event_id: Option<nostr::EventId>,
452 series_count: Option<(u64, u64)>, 518 series_count: Option<(u64, u64)>,
519 branch_name: Option<String>,
453) -> Result<nostr::Event> { 520) -> Result<nostr::Event> {
454 let commit_parent = git_repo 521 let commit_parent = git_repo
455 .get_commit_parent(commit) 522 .get_commit_parent(commit)
@@ -496,6 +563,18 @@ pub fn generate_patch_event(
496 } else { 563 } else {
497 vec![] 564 vec![]
498 }, 565 },
566 if let Some(branch_name) = branch_name {
567 if thread_event_id.is_none() {
568 vec![
569 Tag::Generic(
570 TagKind::Custom("branch-name".to_string()),
571 vec![branch_name.to_string()],
572 )
573 ]
574 }
575 else { vec![]}
576 }
577 else { vec![]},
499 // whilst it is in nip34 draft to tag the maintainers 578 // whilst it is in nip34 draft to tag the maintainers
500 // I'm not sure it is a good idea because if they are 579 // I'm not sure it is a good idea because if they are
501 // interested in all patches then their specialised 580 // interested in all patches then their specialised
diff --git a/src/sub_commands/prs/list.rs b/src/sub_commands/prs/list.rs
index bc85eed..36cbd02 100644
--- a/src/sub_commands/prs/list.rs
+++ b/src/sub_commands/prs/list.rs
@@ -1,5 +1,6 @@
1use anyhow::{bail, Context, Result}; 1use anyhow::{bail, Context, Result};
2 2
3use super::create::event_is_patch_set_root;
3#[cfg(not(test))] 4#[cfg(not(test))]
4use crate::client::Client; 5use crate::client::Client;
5#[cfg(test)] 6#[cfg(test)]
@@ -8,8 +9,10 @@ use crate::{
8 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, 9 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms},
9 client::Connect, 10 client::Connect,
10 git::{Repo, RepoActions}, 11 git::{Repo, RepoActions},
11 repo_ref::{self}, 12 repo_ref::{self, RepoRef, REPO_REF_KIND},
12 sub_commands::prs::create::{event_to_cover_letter, PATCH_KIND, PR_KIND}, 13 sub_commands::prs::create::{
14 event_is_cover_letter, event_to_cover_letter, PATCH_KIND, PR_KIND,
15 },
13 Cli, 16 Cli,
14}; 17};
15 18
@@ -51,40 +54,8 @@ pub async fn launch(
51 54
52 println!("finding PRs..."); 55 println!("finding PRs...");
53 56
54 let pr_events: Vec<nostr::Event> = client 57 let pr_events: Vec<nostr::Event> =
55 .get_events( 58 find_pr_events(&client, &repo_ref, &root_commit.to_string()).await?;
56 repo_ref.relays.clone(),
57 vec![
58 nostr::Filter::default()
59 .kind(nostr::Kind::Custom(PR_KIND))
60 .reference(format!("{root_commit}")),
61 ],
62 )
63 .await?
64 .iter()
65 .filter(|e| {
66 e.kind.as_u64() == PR_KIND
67 && e.tags
68 .iter()
69 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}")))
70 })
71 .map(std::borrow::ToOwned::to_owned)
72 .collect();
73
74 // let pr_branch_names: Vec<String> = pr_events
75 // .iter()
76 // .map(|e| {
77 // format!(
78 // "{}-{}",
79 // &e.id.to_string()[..5],
80 // if let Some(t) = e.tags.iter().find(|t| t.as_vec()[0] ==
81 // "branch-name") { t.as_vec()[1].to_string()
82 // } else {
83 // "".to_string()
84 // } // git_repo.get_checked_out_branch_name(),
85 // )
86 // })
87 // .collect();
88 59
89 let selected_index = Interactor::default().choice( 60 let selected_index = Interactor::default().choice(
90 PromptChoiceParms::default() 61 PromptChoiceParms::default()
@@ -95,6 +66,8 @@ pub async fn launch(
95 .map(|e| { 66 .map(|e| {
96 if let Ok(cl) = event_to_cover_letter(e) { 67 if let Ok(cl) = event_to_cover_letter(e) {
97 cl.title 68 cl.title
69 } else if let Ok(msg) = tag_value(e, "description") {
70 msg.split('\n').collect::<Vec<&str>>()[0].to_string()
98 } else { 71 } else {
99 e.id.to_string() 72 e.id.to_string()
100 } 73 }
@@ -102,49 +75,20 @@ pub async fn launch(
102 .collect(), 75 .collect(),
103 ), 76 ),
104 )?; 77 )?;
105 // println!("prs:{:?}", &pr_events);
106 78
107 println!("finding commits..."); 79 println!("finding commits...");
108 80
109 let commits_events: Vec<nostr::Event> = client 81 let commits_events: Vec<nostr::Event> =
110 .get_events( 82 find_commits_for_pr_event(&client, &pr_events[selected_index], &repo_ref).await?;
111 repo_ref.relays.clone(),
112 vec![
113 nostr::Filter::default()
114 .kind(nostr::Kind::Custom(PATCH_KIND))
115 .event(pr_events[selected_index].id),
116 ],
117 )
118 .await?
119 .iter()
120 .filter(|e| {
121 e.kind.as_u64() == PATCH_KIND
122 && e.tags.iter().any(|t| {
123 t.as_vec().len() > 2
124 && t.as_vec()[1].eq(&pr_events[selected_index].id.to_string())
125 })
126 })
127 .map(std::borrow::ToOwned::to_owned)
128 .collect();
129 83
130 confirm_checkout(&git_repo)?; 84 confirm_checkout(&git_repo)?;
131 85
132 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) 86 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events)
133 .context("cannot get most recent patch for PR")?; 87 .context("cannot get most recent patch for PR")?;
134 88
135 let branch_name: String = if let Ok(cl) = event_to_cover_letter(&pr_events[selected_index]) { 89 let branch_name: String = event_to_cover_letter(&pr_events[selected_index])
136 if let Some(name) = cl.branch_name { 90 .context("cannot assign a branch name as event is not a patch set root")?
137 name 91 .branch_name;
138 } else {
139 cl.title
140 .replace(' ', "-")
141 .chars()
142 .filter(|c| c.is_ascii_alphanumeric() || c.eq(&'/'))
143 .collect()
144 }
145 } else {
146 bail!("Placeholder not a cover letter")
147 };
148 92
149 let applied = git_repo 93 let applied = git_repo
150 .apply_patch_chain(&branch_name, most_recent_pr_patch_chain) 94 .apply_patch_chain(&branch_name, most_recent_pr_patch_chain)
@@ -193,20 +137,139 @@ pub fn get_most_recent_patch_with_ancestors(
193) -> Result<Vec<nostr::Event>> { 137) -> Result<Vec<nostr::Event>> {
194 patches.sort_by_key(|e| e.created_at); 138 patches.sort_by_key(|e| e.created_at);
195 139
196 let mut res = vec![]; 140 let first_patch = patches.first().context("no patches found")?;
197 141
198 let latest_commit_id = tag_value(patches.first().context("no patches found")?, "commit")?; 142 let patches_with_youngest_created_at: Vec<&nostr::Event> = patches
143 .iter()
144 .filter(|p| p.created_at.eq(&first_patch.created_at))
145 .collect();
146
147 let latest_commit_id = tag_value(
148 // get the first patch which isn't a parent of a patch event created at the same
149 // time
150 patches_with_youngest_created_at
151 .clone()
152 .iter()
153 .find(|p| {
154 if let Ok(commit) = tag_value(p, "commit") {
155 !patches_with_youngest_created_at.iter().any(|p2| {
156 if let Ok(parent) = tag_value(p2, "parent-commit") {
157 commit.eq(&parent)
158 } else {
159 false // skip
160 }
161 })
162 } else {
163 false // skip
164 }
165 })
166 .context("cannot find patches_with_youngest_created_at")?,
167 "commit",
168 )?;
169
170 let mut res = vec![];
199 171
200 let mut commit_id_to_search = latest_commit_id; 172 let mut commit_id_to_search = latest_commit_id;
201 173
202 while let Some(event) = patches.iter().find(|e| { 174 while let Some(event) = patches.iter().find(|e| {
203 tag_value(e, "commit") 175 if let Ok(commit) = tag_value(e, "commit") {
204 .context("patch event doesnt contain commit tag") 176 commit.eq(&commit_id_to_search)
205 .unwrap() 177 } else {
206 .eq(&commit_id_to_search) 178 false // skip
179 }
207 }) { 180 }) {
208 res.push(event.clone()); 181 res.push(event.clone());
209 commit_id_to_search = tag_value(event, "parent-commit")?; 182 commit_id_to_search = tag_value(event, "parent-commit")?;
210 } 183 }
211 Ok(res) 184 Ok(res)
212} 185}
186
187pub async fn find_pr_events(
188 #[cfg(test)] client: &crate::client::MockConnect,
189 #[cfg(not(test))] client: &Client,
190 repo_ref: &RepoRef,
191 root_commit: &str,
192) -> Result<Vec<nostr::Event>> {
193 Ok(client
194 .get_events(
195 repo_ref.relays.clone(),
196 vec![
197 nostr::Filter::default()
198 .kinds(vec![
199 nostr::Kind::Custom(PR_KIND),
200 nostr::Kind::Custom(PATCH_KIND),
201 ])
202 .custom_tag(nostr::Alphabet::T, vec!["root"])
203 .identifiers(
204 repo_ref
205 .maintainers
206 .iter()
207 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
208 ),
209 // also pick up prs from the same repo but no target at our maintainers repo events
210 nostr::Filter::default()
211 .kinds(vec![
212 nostr::Kind::Custom(PR_KIND),
213 nostr::Kind::Custom(PATCH_KIND),
214 ])
215 .custom_tag(nostr::Alphabet::T, vec!["root"])
216 .reference(root_commit),
217 ],
218 )
219 .await
220 .context("cannot get pr events")?
221 .iter()
222 .filter(|e| {
223 event_is_patch_set_root(e)
224 && (e
225 .tags
226 .iter()
227 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(root_commit))
228 || e.tags.iter().any(|t| {
229 t.as_vec().len() > 1
230 && repo_ref
231 .maintainers
232 .iter()
233 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier))
234 .any(|d| t.as_vec()[1].eq(&d))
235 }))
236 })
237 .map(std::borrow::ToOwned::to_owned)
238 .collect::<Vec<nostr::Event>>())
239}
240
241pub async fn find_commits_for_pr_event(
242 #[cfg(test)] client: &crate::client::MockConnect,
243 #[cfg(not(test))] client: &Client,
244 pr_event: &nostr::Event,
245 repo_ref: &RepoRef,
246) -> Result<Vec<nostr::Event>> {
247 let mut patch_events: Vec<nostr::Event> = client
248 .get_events(
249 repo_ref.relays.clone(),
250 vec![
251 nostr::Filter::default()
252 .kind(nostr::Kind::Custom(PATCH_KIND))
253 // this requires every patch to reference the root event
254 // this will not pick up v2,v3 patch sets
255 // TODO: fetch commits for v2.. patch sets
256 .event(pr_event.id),
257 ],
258 )
259 .await
260 .context("cannot fetch patch events")?
261 .iter()
262 .filter(|e| {
263 e.kind.as_u64() == PATCH_KIND
264 && e.tags
265 .iter()
266 .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string()))
267 })
268 .map(std::borrow::ToOwned::to_owned)
269 .collect();
270
271 if !event_is_cover_letter(pr_event) {
272 patch_events.push(pr_event.clone());
273 }
274 Ok(patch_events)
275}
diff --git a/src/sub_commands/pull.rs b/src/sub_commands/pull.rs
index f3ae81f..2b20d3d 100644
--- a/src/sub_commands/pull.rs
+++ b/src/sub_commands/pull.rs
@@ -8,10 +8,8 @@ use crate::{
8 client::Connect, 8 client::Connect,
9 git::{Repo, RepoActions}, 9 git::{Repo, RepoActions},
10 repo_ref, 10 repo_ref,
11 repo_ref::REPO_REF_KIND, 11 sub_commands::{
12 sub_commands::prs::{ 12 prs::list::get_most_recent_patch_with_ancestors, push::fetch_pr_and_most_recent_patch_chain,
13 create::{PATCH_KIND, PR_KIND},
14 list::{get_most_recent_patch_with_ancestors, tag_value},
15 }, 13 },
16}; 14};
17 15
@@ -46,63 +44,15 @@ pub async fn launch() -> Result<()> {
46 ) 44 )
47 .await?; 45 .await?;
48 46
49 println!("finding PR event..."); 47 let (_pr_event, commit_events) =
50 48 fetch_pr_and_most_recent_patch_chain(&client, &repo_ref, &root_commit, &branch_name)
51 let pr_event: nostr::Event = client 49 .await?;
52 .get_events(
53 repo_ref.relays.clone(),
54 vec![
55 nostr::Filter::default()
56 .kind(nostr::Kind::Custom(PR_KIND))
57 .identifiers(
58 repo_ref
59 .maintainers
60 .iter()
61 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
62 ),
63 ],
64 )
65 .await?
66 .iter()
67 .find(|e| {
68 e.kind.as_u64() == PR_KIND
69 && e.tags
70 .iter()
71 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}")))
72 && tag_value(e, "branch-name")
73 .unwrap_or_default()
74 .eq(&branch_name)
75 })
76 .context("cannot find a PR event associated with the checked out branch name")?
77 .to_owned();
78
79 println!("found PR event. finding commits...");
80
81 let commits_events: Vec<nostr::Event> = client
82 .get_events(
83 repo_ref.relays.clone(),
84 vec![
85 nostr::Filter::default()
86 .kind(nostr::Kind::Custom(PATCH_KIND))
87 .event(pr_event.id),
88 ],
89 )
90 .await?
91 .iter()
92 .filter(|e| {
93 e.kind.as_u64() == PATCH_KIND
94 && e.tags
95 .iter()
96 .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string()))
97 })
98 .map(std::borrow::ToOwned::to_owned)
99 .collect();
100 50
101 if git_repo.has_outstanding_changes()? { 51 if git_repo.has_outstanding_changes()? {
102 bail!("cannot pull changes when repository is not clean. discard changes and try again."); 52 bail!("cannot pull changes when repository is not clean. discard changes and try again.");
103 } 53 }
104 54
105 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) 55 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commit_events)
106 .context("cannot get most recent patch for PR")?; 56 .context("cannot get most recent patch for PR")?;
107 57
108 let applied = git_repo 58 let applied = git_repo
diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs
index 61d5d46..eb42699 100644
--- a/src/sub_commands/push.rs
+++ b/src/sub_commands/push.rs
@@ -9,10 +9,13 @@ use crate::{
9 client::Connect, 9 client::Connect,
10 git::{str_to_sha1, Repo, RepoActions}, 10 git::{str_to_sha1, Repo, RepoActions},
11 login, 11 login,
12 repo_ref::{self, RepoRef, REPO_REF_KIND}, 12 repo_ref::{self, RepoRef},
13 sub_commands::prs::{ 13 sub_commands::prs::{
14 create::{generate_patch_event, send_events, PATCH_KIND, PR_KIND}, 14 create::{event_to_cover_letter, generate_patch_event, send_events},
15 list::{get_most_recent_patch_with_ancestors, tag_value}, 15 list::{
16 find_commits_for_pr_event, find_pr_events, get_most_recent_patch_with_ancestors,
17 tag_value,
18 },
16 }, 19 },
17 Cli, 20 Cli,
18}; 21};
@@ -111,6 +114,7 @@ pub async fn launch(cli_args: &Cli) -> Result<()> {
111 &repo_ref, 114 &repo_ref,
112 patch_events.last().map(nostr::Event::id), 115 patch_events.last().map(nostr::Event::id),
113 None, 116 None,
117 None,
114 ) 118 )
115 .context("cannot make patch event from commit")?, 119 .context("cannot make patch event from commit")?,
116 ); 120 );
@@ -131,7 +135,7 @@ pub async fn launch(cli_args: &Cli) -> Result<()> {
131 Ok(()) 135 Ok(())
132} 136}
133 137
134async fn fetch_pr_and_most_recent_patch_chain( 138pub async fn fetch_pr_and_most_recent_patch_chain(
135 #[cfg(test)] client: &crate::client::MockConnect, 139 #[cfg(test)] client: &crate::client::MockConnect,
136 #[cfg(not(test))] client: &Client, 140 #[cfg(not(test))] client: &Client,
137 repo_ref: &RepoRef, 141 repo_ref: &RepoRef,
@@ -140,54 +144,24 @@ async fn fetch_pr_and_most_recent_patch_chain(
140) -> Result<(nostr::Event, Vec<nostr::Event>)> { 144) -> Result<(nostr::Event, Vec<nostr::Event>)> {
141 println!("finding PR event..."); 145 println!("finding PR event...");
142 146
143 let pr_event: nostr::Event = client 147 let pr_events: Vec<nostr::Event> = find_pr_events(client, repo_ref, &root_commit.to_string())
144 .get_events( 148 .await
145 repo_ref.relays.clone(), 149 .context("cannot get pr events for repo")?;
146 vec![ 150
147 nostr::Filter::default() 151 let pr_event: nostr::Event = pr_events
148 .kind(nostr::Kind::Custom(PR_KIND))
149 .identifiers(
150 repo_ref
151 .maintainers
152 .iter()
153 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
154 ),
155 ],
156 )
157 .await?
158 .iter() 152 .iter()
159 .find(|e| { 153 .find(|e| {
160 e.kind.as_u64() == PR_KIND 154 event_to_cover_letter(e).is_ok_and(|cl| cl.branch_name.eq(branch_name))
161 && e.tags 155 // TODO remove the dependancy on same branch name and replace with
162 .iter() 156 // references stored in .git/ngit
163 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}")))
164 && tag_value(e, "branch-name")
165 .unwrap_or_default()
166 .eq(branch_name)
167 }) 157 })
168 .context("cannot find a PR event associated with the checked out branch name")? 158 .context("cannot find a PR event associated with the checked out branch name")?
169 .to_owned(); 159 .to_owned();
170 160
171 println!("found PR event. finding commits..."); 161 println!("found PR event. finding commits...");
172 162
173 let commits_events: Vec<nostr::Event> = client 163 let commits_events: Vec<nostr::Event> =
174 .get_events( 164 find_commits_for_pr_event(client, &pr_event, repo_ref).await?;
175 repo_ref.relays.clone(), 165
176 vec![
177 nostr::Filter::default()
178 .kind(nostr::Kind::Custom(PATCH_KIND))
179 .event(pr_event.id),
180 ],
181 )
182 .await?
183 .iter()
184 .filter(|e| {
185 e.kind.as_u64() == PATCH_KIND
186 && e.tags
187 .iter()
188 .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string()))
189 })
190 .map(std::borrow::ToOwned::to_owned)
191 .collect();
192 Ok((pr_event, commits_events)) 166 Ok((pr_event, commits_events))
193} 167}