upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-07-25 16:12:27 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2025-07-25 16:12:27 +0100
commite89dbc142f5a0a517f197562f5f228681d9aed47 (patch)
tree521dbec8d259f7c982345b40bb128a21795a2012 /src/bin
parent419b827f7c0d826f5eedf574bce0cf9b85cab4ca (diff)
parent0cad465dd3f78bd6c680067d12d396d4782829bf (diff)
Merge branch 'add-PR-feature-to-remote'
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/git_remote_nostr/fetch.rs105
-rw-r--r--src/bin/git_remote_nostr/list.rs56
-rw-r--r--src/bin/git_remote_nostr/push.rs333
-rw-r--r--src/bin/git_remote_nostr/utils.rs59
-rw-r--r--src/bin/ngit/sub_commands/init.rs16
-rw-r--r--src/bin/ngit/sub_commands/list.rs211
6 files changed, 592 insertions, 188 deletions
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 @@
1use core::str; 1use core::str;
2use std::{ 2use std::{
3 collections::HashMap, 3 collections::{HashMap, HashSet},
4 io::Stdin, 4 io::Stdin,
5 sync::{Arc, Mutex}, 5 sync::{Arc, Mutex},
6 time::Instant, 6 time::Instant,
@@ -16,7 +16,7 @@ use ngit::{
16 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 16 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
17 utils::check_ssh_keys, 17 utils::check_ssh_keys,
18 }, 18 },
19 git_events::tag_value, 19 git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, tag_value},
20 login::get_curent_user, 20 login::get_curent_user,
21 repo_ref::{RepoRef, is_grasp_server}, 21 repo_ref::{RepoRef, is_grasp_server},
22}; 22};
@@ -37,38 +37,78 @@ pub async fn run_fetch(
37) -> Result<()> { 37) -> Result<()> {
38 let mut fetch_batch = get_oids_from_fetch_batch(stdin, oid, refstr)?; 38 let mut fetch_batch = get_oids_from_fetch_batch(stdin, oid, refstr)?;
39 39
40 let oids_from_git_servers = fetch_batch 40 let oids_from_state = fetch_batch
41 .iter() 41 .iter()
42 .filter(|(refstr, _)| !refstr.contains("refs/heads/pr/")) 42 .filter(|(refstr, _)| !refstr.contains("refs/heads/pr/"))
43 .map(|(_, oid)| oid.clone()) 43 .map(|(_, oid)| oid.clone())
44 .collect::<Vec<String>>(); 44 .collect::<Vec<String>>();
45 45
46 let pr_oid_clone_url_map = identify_clone_urls_for_oids_from_pr_pr_update_events(
47 fetch_batch.values().collect::<Vec<&String>>(),
48 git_repo,
49 repo_ref,
50 )
51 .await?;
52
53 let oids_to_fetch_from_git_servers = [
54 oids_from_state.clone(),
55 pr_oid_clone_url_map
56 .keys()
57 .cloned()
58 .collect::<Vec<String>>(),
59 ]
60 .concat();
61
62 let git_servers = {
63 let mut seen: HashSet<String> = HashSet::new();
64 let mut out: Vec<String> = vec![];
65 for server in &repo_ref.git_server {
66 if seen.insert(server.clone()) {
67 out.push(server.clone());
68 }
69 }
70 for url in pr_oid_clone_url_map.values().flatten() {
71 if seen.insert(url.clone()) {
72 out.push(url.clone());
73 }
74 }
75 out
76 };
77
46 let mut errors = vec![]; 78 let mut errors = vec![];
47 let term = console::Term::stderr(); 79 let term = console::Term::stderr();
48 80
49 for git_server_url in &repo_ref.git_server { 81 for git_server_url in &git_servers {
82 let oids_to_fetch_from_server = oids_to_fetch_from_git_servers
83 .clone()
84 .into_iter()
85 .filter(|oid| !git_repo.does_commit_exist(oid).unwrap_or(false))
86 .collect::<Vec<String>>();
87
88 if oids_to_fetch_from_server.is_empty() {
89 continue;
90 }
91
50 let term = console::Term::stderr(); 92 let term = console::Term::stderr();
51 if let Err(error) = fetch_from_git_server( 93 if let Err(error) = fetch_from_git_server(
52 git_repo, 94 git_repo,
53 &oids_from_git_servers, 95 &oids_from_state,
54 git_server_url, 96 git_server_url,
55 &repo_ref.to_nostr_git_url(&None), 97 &repo_ref.to_nostr_git_url(&None),
56 &term, 98 &term,
57 is_grasp_server(git_server_url, &repo_ref.grasp_servers()), 99 is_grasp_server(git_server_url, &repo_ref.grasp_servers()),
58 ) { 100 ) {
59 errors.push(error); 101 errors.push(error);
60 } else {
61 break;
62 } 102 }
63 } 103 }
64 104
65 if oids_from_git_servers 105 if oids_from_state
66 .iter() 106 .iter()
67 .any(|oid| !git_repo.does_commit_exist(oid).unwrap()) 107 .any(|oid| !git_repo.does_commit_exist(oid).unwrap())
68 && !errors.is_empty() 108 && !errors.is_empty()
69 { 109 {
70 bail!( 110 bail!(
71 "fetch: failed to fetch objects in nostr state event from:\r\n{}", 111 "fetch: failed to fetch objects from:\r\n{}",
72 errors 112 errors
73 .iter() 113 .iter()
74 .map(|e| format!(" - {e}")) 114 .map(|e| format!(" - {e}"))
@@ -79,12 +119,43 @@ pub async fn run_fetch(
79 119
80 fetch_batch.retain(|refstr, _| refstr.contains("refs/heads/pr/")); 120 fetch_batch.retain(|refstr, _| refstr.contains("refs/heads/pr/"));
81 121
82 fetch_open_or_draft_proposals(git_repo, &term, repo_ref, &fetch_batch).await?; 122 fetch_open_or_draft_proposals_from_patches(git_repo, &term, repo_ref, &fetch_batch).await?;
123 // TODO fetch_open_or_draft_proposals just needs to do it for patches
83 term.flush()?; 124 term.flush()?;
84 println!(); 125 println!();
85 Ok(()) 126 Ok(())
86} 127}
87 128
129async fn identify_clone_urls_for_oids_from_pr_pr_update_events(
130 oids: Vec<&String>,
131 git_repo: &Repo,
132 repo_ref: &RepoRef,
133) -> Result<HashMap<String, Vec<String>>> {
134 let mut map: HashMap<String, Vec<String>> = HashMap::new();
135
136 let open_and_draft_proposals = get_open_or_draft_proposals(git_repo, repo_ref).await?;
137
138 for (_, (_, events)) in open_and_draft_proposals {
139 for event in events {
140 if [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind) {
141 if let Ok(c) = tag_value(&event, "c") {
142 if oids.contains(&&c) {
143 for tag in event.tags.as_slice() {
144 if tag.kind().eq(&nostr::event::TagKind::Clone) {
145 for clone_url in tag.as_slice().iter().skip(1) {
146 map.entry(c.clone()).or_default().push(clone_url.clone());
147 }
148 }
149 }
150 }
151 }
152 }
153 }
154 }
155
156 Ok(map)
157}
158
88pub fn make_commits_for_proposal( 159pub fn make_commits_for_proposal(
89 git_repo: &Repo, 160 git_repo: &Repo,
90 repo_ref: &RepoRef, 161 repo_ref: &RepoRef,
@@ -128,7 +199,7 @@ pub fn make_commits_for_proposal(
128 Ok(tip_commit_id) 199 Ok(tip_commit_id)
129} 200}
130 201
131async fn fetch_open_or_draft_proposals( 202async fn fetch_open_or_draft_proposals_from_patches(
132 git_repo: &Repo, 203 git_repo: &Repo,
133 term: &console::Term, 204 term: &console::Term,
134 repo_ref: &RepoRef, 205 repo_ref: &RepoRef,
@@ -140,12 +211,19 @@ async fn fetch_open_or_draft_proposals(
140 let current_user = get_curent_user(git_repo)?; 211 let current_user = get_curent_user(git_repo)?;
141 212
142 for refstr in proposal_refs.keys() { 213 for refstr in proposal_refs.keys() {
143 if let Some((_, (_, patches))) = find_proposal_and_patches_by_branch_name( 214 if let Some((_, (_, events_to_apply))) = find_proposal_and_patches_by_branch_name(
144 refstr, 215 refstr,
145 &open_and_draft_proposals, 216 &open_and_draft_proposals,
146 current_user.as_ref(), 217 current_user.as_ref(),
147 ) { 218 ) {
148 if let Err(error) = make_commits_for_proposal(git_repo, repo_ref, patches) { 219 if events_to_apply
220 .iter()
221 .any(|e| e.kind.eq(&KIND_PULL_REQUEST) || e.kind.eq(&KIND_PULL_REQUEST_UPDATE))
222 {
223 // do nothing - we fetch these oids as part of run_fetch
224 } else if let Err(error) =
225 make_commits_for_proposal(git_repo, repo_ref, events_to_apply)
226 {
149 term.write_line( 227 term.write_line(
150 format!("WARNING: failed to create branch for {refstr}, error: {error}",) 228 format!("WARNING: failed to create branch for {refstr}, error: {error}",)
151 .as_str(), 229 .as_str(),
@@ -429,6 +507,7 @@ fn fetch_from_git_server_url(
429 remote_callbacks.credentials(auth.credentials(&git_config)); 507 remote_callbacks.credentials(auth.credentials(&git_config));
430 } 508 }
431 fetch_options.remote_callbacks(remote_callbacks); 509 fetch_options.remote_callbacks(remote_callbacks);
510
432 git_server_remote.download(oids, Some(&mut fetch_options))?; 511 git_server_remote.download(oids, Some(&mut fetch_options))?;
433 512
434 git_server_remote.disconnect()?; 513 git_server_remote.disconnect()?;
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::{
11 self, 11 self,
12 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 12 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
13 }, 13 },
14 git_events::event_to_cover_letter, 14 git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, tag_value},
15 login::get_curent_user, 15 login::get_curent_user,
16 repo_ref::{self, is_grasp_server}, 16 repo_ref::{self, is_grasp_server},
17}; 17};
@@ -122,6 +122,16 @@ async fn get_open_and_draft_proposals_state(
122 122
123 // without trusting commit_id we must apply each patch which requires the oid of 123 // without trusting commit_id we must apply each patch which requires the oid of
124 // the parent so we much do a fetch 124 // the parent so we much do a fetch
125
126 // As we are fetching from git servers we mighgt as well get oids from pull
127 // request too
128 // TODO get Pull Request and Pull Request Update Events add these to
129 // refs/nostr/<event-id>
130 // TODO prepare PRs and PRS oids to try and fetch from repo servers that are or
131 // clone urls in PR/update event we are using anyway. TODO after we tried
132 // and failed to get them from these server we should fallback to fetch them
133 // from listed clone urls in PR/update but not during list, only during fetch
134
125 for (git_server_url, (oids_from_git_servers, is_grasp_server)) in remote_states { 135 for (git_server_url, (oids_from_git_servers, is_grasp_server)) in remote_states {
126 if fetch_from_git_server( 136 if fetch_from_git_server(
127 git_repo, 137 git_repo,
@@ -144,7 +154,7 @@ async fn get_open_and_draft_proposals_state(
144 let mut state = HashMap::new(); 154 let mut state = HashMap::new();
145 let open_and_draft_proposals = get_open_or_draft_proposals(git_repo, repo_ref).await?; 155 let open_and_draft_proposals = get_open_or_draft_proposals(git_repo, repo_ref).await?;
146 let current_user = get_curent_user(git_repo)?; 156 let current_user = get_curent_user(git_repo)?;
147 for (_, (proposal, patches)) in open_and_draft_proposals { 157 for (_, (proposal, events_to_apply)) in open_and_draft_proposals {
148 if let Ok(cl) = event_to_cover_letter(&proposal) { 158 if let Ok(cl) = event_to_cover_letter(&proposal) {
149 if let Ok(mut branch_name) = cl.get_branch_name_with_pr_prefix_and_shorthand_id() { 159 if let Ok(mut branch_name) = cl.get_branch_name_with_pr_prefix_and_shorthand_id() {
150 branch_name = if let Some(public_key) = current_user { 160 branch_name = if let Some(public_key) = current_user {
@@ -156,15 +166,43 @@ async fn get_open_and_draft_proposals_state(
156 } else { 166 } else {
157 branch_name 167 branch_name
158 }; 168 };
159 match make_commits_for_proposal(git_repo, repo_ref, &patches) { 169 // if events_to_apply contains a PR or PR Update event it should be the only
160 Ok(tip) => { 170 // event in the Vec
161 state.insert(format!("refs/heads/{branch_name}"), tip); 171 if let Some(pr_or_pr_update) = events_to_apply
172 .iter()
173 .find(|e| e.kind.eq(&KIND_PULL_REQUEST) || e.kind.eq(&KIND_PULL_REQUEST_UPDATE))
174 {
175 match tag_value(pr_or_pr_update, "c") {
176 Ok(tip) => {
177 state.insert(format!("refs/heads/{branch_name}"), tip);
178 }
179 Err(_) => {
180 let _ = term.write_line(
181 format!(
182 "WARNING: failed to fetch branch {branch_name} error: {} event poorly formatted",
183 if pr_or_pr_update.kind.eq(&KIND_PULL_REQUEST) {
184 "PR"
185 } else {
186 "PR update"
187 }
188 )
189 .as_str(),
190 );
191 }
162 } 192 }
163 Err(error) => { 193 } else {
164 let _ = term.write_line( 194 match make_commits_for_proposal(git_repo, repo_ref, &events_to_apply) {
165 format!("WARNING: failed to fetch branch {branch_name} error: {error}") 195 Ok(tip) => {
196 state.insert(format!("refs/heads/{branch_name}"), tip);
197 }
198 Err(error) => {
199 let _ = term.write_line(
200 format!(
201 "WARNING: failed to fetch branch {branch_name} error: {error}"
202 )
166 .as_str(), 203 .as_str(),
167 ); 204 );
205 }
168 } 206 }
169 } 207 }
170 } 208 }
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs
index 9ff8af0..909a0ab 100644
--- a/src/bin/git_remote_nostr/push.rs
+++ b/src/bin/git_remote_nostr/push.rs
@@ -12,23 +12,24 @@ use client::{get_events_from_local_cache, get_state_from_cache, send_events, sig
12use console::Term; 12use console::Term;
13use git::{RepoActions, sha1_to_oid}; 13use git::{RepoActions, sha1_to_oid};
14use git_events::{ 14use git_events::{
15 generate_cover_letter_and_patch_events, generate_patch_event, get_commit_id_from_patch, 15 generate_cover_letter_and_patch_events, generate_patch_event,
16 generate_unsigned_pr_or_update_event, get_commit_id_from_patch,
16}; 17};
17use git2::{Oid, Repository}; 18use git2::{Oid, Repository};
18use ngit::{ 19use ngit::{
19 cli_interactor::count_lines_per_msg_vec, 20 cli_interactor::count_lines_per_msg_vec,
20 client::{self, get_event_from_cache_by_id}, 21 client::{self, get_event_from_cache_by_id, sign_draft_event},
21 git::{ 22 git::{
22 self, 23 self,
23 nostr_url::{CloneUrl, NostrUrlDecoded}, 24 nostr_url::{CloneUrl, NostrUrlDecoded},
24 oid_to_shorthand_string, 25 oid_to_shorthand_string,
25 }, 26 },
26 git_events::{self, event_to_cover_letter, get_event_root}, 27 git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root},
27 login::{self, user::UserRef}, 28 login::{self, user::UserRef},
28 repo_ref::{self, get_repo_config_from_yaml, is_grasp_server}, 29 repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url},
29 repo_state, 30 repo_state,
30}; 31};
31use nostr::nips::nip10::Marker; 32use nostr::{event::UnsignedEvent, nips::nip10::Marker};
32use nostr_sdk::{ 33use nostr_sdk::{
33 Event, EventBuilder, EventId, Kind, NostrSigner, PublicKey, RelayUrl, Tag, TagStandard, 34 Event, EventBuilder, EventId, Kind, NostrSigner, PublicKey, RelayUrl, Tag, TagStandard,
34 hashes::sha1::Hash as Sha1Hash, 35 hashes::sha1::Hash as Sha1Hash,
@@ -65,7 +66,7 @@ pub async fn run_push(
65 .cloned() 66 .cloned()
66 .collect::<Vec<String>>(); 67 .collect::<Vec<String>>();
67 68
68 let mut git_server_refspecs = refspecs 69 let mut git_state_refspecs = refspecs
69 .iter() 70 .iter()
70 .filter(|r| !r.contains("refs/heads/pr/")) 71 .filter(|r| !r.contains("refs/heads/pr/"))
71 .cloned() 72 .cloned()
@@ -105,12 +106,12 @@ pub async fn run_push(
105 let (rejected_refspecs, remote_refspecs) = create_rejected_refspecs_and_remotes_refspecs( 106 let (rejected_refspecs, remote_refspecs) = create_rejected_refspecs_and_remotes_refspecs(
106 &term, 107 &term,
107 git_repo, 108 git_repo,
108 &git_server_refspecs, 109 &git_state_refspecs,
109 &existing_state, 110 &existing_state,
110 &list_outputs, 111 &list_outputs,
111 )?; 112 )?;
112 113
113 git_server_refspecs.retain(|refspec| { 114 git_state_refspecs.retain(|refspec| {
114 if let Some(rejected) = rejected_refspecs.get(&refspec.to_string()) { 115 if let Some(rejected) = rejected_refspecs.get(&refspec.to_string()) {
115 let (_, to) = refspec_to_from_to(refspec).unwrap(); 116 let (_, to) = refspec_to_from_to(refspec).unwrap();
116 println!("error {to} {} out of sync with nostr", rejected.join(" ")); 117 println!("error {to} {} out of sync with nostr", rejected.join(" "));
@@ -121,11 +122,11 @@ pub async fn run_push(
121 }); 122 });
122 123
123 // all refspecs aren't rejected 124 // all refspecs aren't rejected
124 if !(git_server_refspecs.is_empty() && proposal_refspecs.is_empty()) { 125 if !(git_state_refspecs.is_empty() && proposal_refspecs.is_empty()) {
125 let (rejected_proposal_refspecs, rejected) = create_and_publish_events( 126 let (rejected_proposal_refspecs, rejected) = create_and_publish_events_and_proposals(
126 git_repo, 127 git_repo,
127 repo_ref, 128 repo_ref,
128 &git_server_refspecs, 129 &git_state_refspecs,
129 &proposal_refspecs, 130 &proposal_refspecs,
130 client, 131 client,
131 existing_state, 132 existing_state,
@@ -134,7 +135,7 @@ pub async fn run_push(
134 .await?; 135 .await?;
135 136
136 if !rejected { 137 if !rejected {
137 for refspec in git_server_refspecs.iter().chain(proposal_refspecs.iter()) { 138 for refspec in git_state_refspecs.iter().chain(proposal_refspecs.iter()) {
138 if rejected_proposal_refspecs.contains(refspec) { 139 if rejected_proposal_refspecs.contains(refspec) {
139 continue; 140 continue;
140 } 141 }
@@ -153,7 +154,7 @@ pub async fn run_push(
153 for (git_server_url, remote_refspecs) in remote_refspecs { 154 for (git_server_url, remote_refspecs) in remote_refspecs {
154 let remote_refspecs = remote_refspecs 155 let remote_refspecs = remote_refspecs
155 .iter() 156 .iter()
156 .filter(|refspec| git_server_refspecs.contains(refspec)) 157 .filter(|refspec| git_state_refspecs.contains(refspec))
157 .cloned() 158 .cloned()
158 .collect::<Vec<String>>(); 159 .collect::<Vec<String>>();
159 if !refspecs.is_empty() { 160 if !refspecs.is_empty() {
@@ -174,7 +175,7 @@ pub async fn run_push(
174 Ok(()) 175 Ok(())
175} 176}
176 177
177async fn create_and_publish_events( 178async fn create_and_publish_events_and_proposals(
178 git_repo: &Repo, 179 git_repo: &Repo,
179 repo_ref: &RepoRef, 180 repo_ref: &RepoRef,
180 git_server_refspecs: &Vec<String>, 181 git_server_refspecs: &Vec<String>,
@@ -320,18 +321,23 @@ async fn process_proposal_refspecs(
320 { 321 {
321 if refspec.starts_with('+') { 322 if refspec.starts_with('+') {
322 // force push 323 // force push
323 let (_, main_tip) = git_repo.get_main_or_master_branch()?; 324 let (main_branch_name, main_tip) = git_repo.get_main_or_master_branch()?;
324 let (mut ahead, _) = 325 let (mut ahead, _) =
325 git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; 326 git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?;
326 ahead.reverse(); 327 ahead.reverse();
327 for patch in generate_cover_letter_and_patch_events( 328 if ahead.is_empty() {
328 None, 329 bail!(
330 "cannot push '{from}' as proposal as branch isn't ahead of '{main_branch_name}'"
331 );
332 }
333 for patch in generate_patches_or_pr_event_or_pr_updates(
329 git_repo, 334 git_repo,
335 repo_ref,
330 &ahead, 336 &ahead,
337 user_ref,
338 Some(proposal),
331 signer, 339 signer,
332 repo_ref, 340 term,
333 &Some(proposal.id.to_string()),
334 &[],
335 ) 341 )
336 .await? 342 .await?
337 { 343 {
@@ -355,27 +361,50 @@ async fn process_proposal_refspecs(
355 }; 361 };
356 let mut parent_patch = tip_patch.clone(); 362 let mut parent_patch = tip_patch.clone();
357 ahead.reverse(); 363 ahead.reverse();
358 for (i, commit) in ahead.iter().enumerate() { 364 if ahead.is_empty() {
359 let new_patch = generate_patch_event( 365 bail!(
366 "cannot push '{from}' as proposal as branch isn't ahead of proposal on nostr"
367 );
368 }
369 if proposal.kind.eq(&KIND_PULL_REQUEST)
370 || are_commits_too_big_for_patches(git_repo, &ahead)
371 {
372 for event in generate_patches_or_pr_event_or_pr_updates(
360 git_repo, 373 git_repo,
361 &git_repo.get_root_commit()?,
362 commit,
363 Some(thread_id),
364 signer,
365 repo_ref, 374 repo_ref,
366 Some(parent_patch.id), 375 &ahead,
367 Some(( 376 user_ref,
368 (patches.len() + i + 1).try_into().unwrap(), 377 Some(proposal),
369 (patches.len() + ahead.len()).try_into().unwrap(), 378 signer,
370 )), 379 term,
371 None,
372 &None,
373 &[],
374 ) 380 )
375 .await 381 .await?
376 .context("failed to make patch event from commit")?; 382 {
377 events.push(new_patch.clone()); 383 events.push(event);
378 parent_patch = new_patch; 384 }
385 } else {
386 for (i, commit) in ahead.iter().enumerate() {
387 let new_patch = generate_patch_event(
388 git_repo,
389 &git_repo.get_root_commit()?,
390 commit,
391 Some(thread_id),
392 signer,
393 repo_ref,
394 Some(parent_patch.id),
395 Some((
396 (patches.len() + i + 1).try_into().unwrap(),
397 (patches.len() + ahead.len()).try_into().unwrap(),
398 )),
399 None,
400 &None,
401 &[],
402 )
403 .await
404 .context("failed to make patch event from commit")?;
405 events.push(new_patch.clone());
406 parent_patch = new_patch;
407 }
379 } 408 }
380 } else { 409 } else {
381 // we shouldn't get here 410 // we shouldn't get here
@@ -400,22 +429,21 @@ async fn process_proposal_refspecs(
400 } 429 }
401 } else { 430 } else {
402 // TODO new proposal / couldn't find exisiting proposal 431 // TODO new proposal / couldn't find exisiting proposal
403 let (_, main_tip) = git_repo.get_main_or_master_branch()?; 432 let (main_branch_name, main_tip) = git_repo.get_main_or_master_branch()?;
404 let (mut ahead, _) = 433 let (mut ahead, _) =
405 git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; 434 git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?;
406 ahead.reverse(); 435 ahead.reverse();
407 for patch in generate_cover_letter_and_patch_events( 436 if ahead.is_empty() {
408 None, 437 bail!(
409 git_repo, 438 "cannot push '{from}' as proposal as branch isn't ahead of '{main_branch_name}'"
410 &ahead, 439 );
411 signer, 440 }
412 repo_ref, 441 for event in generate_patches_or_pr_event_or_pr_updates(
413 &None, 442 git_repo, repo_ref, &ahead, user_ref, None, signer, term,
414 &[],
415 ) 443 )
416 .await? 444 .await?
417 { 445 {
418 events.push(patch); 446 events.push(event);
419 } 447 }
420 } 448 }
421 } 449 }
@@ -423,6 +451,145 @@ async fn process_proposal_refspecs(
423 Ok((events, rejected_proposal_refspecs)) 451 Ok((events, rejected_proposal_refspecs))
424} 452}
425 453
454fn are_commits_too_big_for_patches(git_repo: &Repo, commits: &[Sha1Hash]) -> bool {
455 commits.iter().any(|commit| {
456 if let Ok(patch) = git_repo.make_patch_from_commit(commit, &None) {
457 patch.len()
458 > ((65 // max recomended patch event size specified in nip34 in kb
459 // allownace for nostr event wrapper (id, pubkey, tags, sig)
460 - 1) * 1024)
461 } else {
462 true
463 }
464 })
465}
466
467#[allow(clippy::too_many_lines)]
468async fn generate_patches_or_pr_event_or_pr_updates(
469 git_repo: &Repo,
470 repo_ref: &RepoRef,
471 ahead: &[Sha1Hash],
472 user_ref: &UserRef,
473 root_proposal: Option<&Event>,
474 signer: &Arc<dyn NostrSigner>,
475 term: &Term,
476) -> Result<Vec<Event>> {
477 let mut events: Vec<Event> = vec![];
478 let use_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST))
479 || are_commits_too_big_for_patches(git_repo, ahead);
480
481 if use_pr {
482 let repo_grasps = repo_ref.grasp_servers();
483 let repo_grasp_clone_urls = repo_ref
484 .git_server
485 .iter()
486 .filter(|s| is_grasp_server(s, &repo_grasps));
487
488 let mut unsigned_pr_event: Option<UnsignedEvent> = None;
489 let mut failed_clone_urls = vec![];
490 for clone_url in repo_grasp_clone_urls {
491 let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event {
492 unsigned_pr_event.clone()
493 } else {
494 generate_unsigned_pr_or_update_event(
495 git_repo,
496 repo_ref,
497 &user_ref.public_key,
498 root_proposal,
499 ahead.first().context("no commits to push")?,
500 &[clone_url],
501 &[],
502 )?
503 };
504
505 let refspec = format!(
506 "{}:refs/nostr/{}",
507 ahead.first().unwrap(),
508 draft_pr_event.id()
509 );
510
511 if let Err(error) = push_to_remote_url(git_repo, clone_url, &[refspec], term) {
512 failed_clone_urls.push(clone_url);
513 term.write_line(
514 format!(
515 "push: error sending commit data to {}: {error}",
516 normalize_grasp_server_url(clone_url)?
517 )
518 .as_str(),
519 )?;
520 } else {
521 term.write_line(
522 format!(
523 "push: commit data sent to {}",
524 normalize_grasp_server_url(clone_url)?
525 )
526 .as_str(),
527 )?;
528 unsigned_pr_event = Some(draft_pr_event);
529 }
530 }
531 if unsigned_pr_event.is_none() {
532 bail!(
533 "a commit in your proposal is too big for a nostr patch. The repository doesnt list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request. Soon ngit will support pushing your changes to a different git / grasp git server."
534 );
535
536 // TODO get grasp_default_set servers that aren't in repo_grasps
537 // cycle through until one succeeds TODO create
538 // personal-fork announcement with grasp servers and
539 // push, after a few seconds push ref/nostr/eventid. if
540 // one success break out of for loop and continue
541 }
542 if let Some(unsigned_pr_event) = unsigned_pr_event {
543 let pr_event = sign_draft_event(
544 unsigned_pr_event,
545 signer,
546 if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) {
547 "Pull Request Replacing Original Patch"
548 } else if root_proposal.is_some() {
549 "Pull Request Update"
550 } else {
551 "Pull Request"
552 }
553 .to_string(),
554 )
555 .await?;
556 events.push(pr_event);
557 if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) {
558 events.push(
559 create_close_status_for_original_patch(
560 signer,
561 repo_ref,
562 root_proposal.unwrap(),
563 )
564 .await?,
565 );
566 }
567 } else {
568 bail!(
569 "a commit in your proposal is too big for a nostr patch. tried to use submit as a nostr Pull Request but could not find a grasp server that would accept your changes"
570 );
571 // TODO suggest `ngit send` where user could specify their own clone
572 // url to push to once that feature is added
573 }
574 } else {
575 for patch in generate_cover_letter_and_patch_events(
576 None,
577 git_repo,
578 ahead,
579 signer,
580 repo_ref,
581 &root_proposal.map(|proposal| proposal.id.to_string()),
582 &[],
583 )
584 .await?
585 {
586 events.push(patch);
587 }
588 }
589
590 Ok(events)
591}
592
426fn push_to_remote( 593fn push_to_remote(
427 git_repo: &Repo, 594 git_repo: &Repo,
428 git_server_url: &str, 595 git_server_url: &str,
@@ -1079,7 +1246,7 @@ type MergedProposalsInfo =
1079async fn get_merged_proposals_info( 1246async fn get_merged_proposals_info(
1080 git_repo: &Repo, 1247 git_repo: &Repo,
1081 ahead: &Vec<Sha1Hash>, 1248 ahead: &Vec<Sha1Hash>,
1082 available_patches: &[Event], 1249 available_patches_pr_pr_update: &[Event],
1083) -> Result<MergedProposalsInfo> { 1250) -> Result<MergedProposalsInfo> {
1084 let mut proposals: MergedProposalsInfo = HashMap::new(); 1251 let mut proposals: MergedProposalsInfo = HashMap::new();
1085 1252
@@ -1089,19 +1256,19 @@ async fn get_merged_proposals_info(
1089 // are in ahead 1256 // are in ahead
1090 if commit.parent_count() > 1 { 1257 if commit.parent_count() > 1 {
1091 for parent in commit.parents() { 1258 for parent in commit.parents() {
1092 for patch_event in available_patches 1259 for event in available_patches_pr_pr_update
1093 .iter() 1260 .iter()
1094 .filter(|e| { 1261 .filter(|e| {
1095 e.tags.iter().any(|t| { 1262 e.tags.iter().any(|t| {
1096 t.as_slice().len() > 1 1263 t.as_slice().len() > 1
1097 && t.as_slice()[0].eq("commit") 1264 && (t.as_slice()[0].eq("commit") || t.as_slice()[0].eq("c"))
1098 && t.as_slice()[1].eq(&parent.id().to_string()) 1265 && t.as_slice()[1].eq(&parent.id().to_string())
1099 }) 1266 })
1100 }) 1267 })
1101 .collect::<Vec<&Event>>() 1268 .collect::<Vec<&Event>>()
1102 { 1269 {
1103 if let Ok((proposal_id, revision_id)) = 1270 if let Ok((proposal_id, revision_id)) =
1104 get_proposal_and_revision_root_from_patch(git_repo, patch_event).await 1271 get_proposal_and_revision_root_from_patch(git_repo, event).await
1105 { 1272 {
1106 let (entry_revision_id, merged_patches) = 1273 let (entry_revision_id, merged_patches) =
1107 proposals.entry(proposal_id).or_default(); 1274 proposals.entry(proposal_id).or_default();
@@ -1114,12 +1281,12 @@ async fn get_merged_proposals_info(
1114 } else { 1281 } else {
1115 // three way merge or fast forward merge commits 1282 // three way merge or fast forward merge commits
1116 // note: ahead included commits of three-way merged branches 1283 // note: ahead included commits of three-way merged branches
1117 let mut matching_patches = available_patches 1284 let mut matching_patches = available_patches_pr_pr_update
1118 .iter() 1285 .iter()
1119 .filter(|e| { 1286 .filter(|e| {
1120 e.tags.iter().any(|t| { 1287 e.tags.iter().any(|t| {
1121 t.as_slice().len() > 1 1288 t.as_slice().len() > 1
1122 && t.as_slice()[0].eq("commit") 1289 && (t.as_slice()[0].eq("commit") || t.as_slice()[0].eq("c"))
1123 && t.as_slice()[1].eq(&commit_hash.to_string()) 1290 && t.as_slice()[1].eq(&commit_hash.to_string())
1124 }) 1291 })
1125 }) 1292 })
@@ -1144,7 +1311,7 @@ async fn get_merged_proposals_info(
1144 // applied commits - this is done after so that merged revisions take priority 1311 // applied commits - this is done after so that merged revisions take priority
1145 if matching_patches.is_empty() { 1312 if matching_patches.is_empty() {
1146 let author = git_repo.get_commit_author(commit_hash)?; 1313 let author = git_repo.get_commit_author(commit_hash)?;
1147 matching_patches = available_patches 1314 matching_patches = available_patches_pr_pr_update
1148 .iter() 1315 .iter()
1149 .filter(|e| { 1316 .filter(|e| {
1150 if let Ok(patch_author) = get_patch_author(e) { 1317 if let Ok(patch_author) = get_patch_author(e) {
@@ -1391,6 +1558,62 @@ async fn create_merge_status(
1391 .await 1558 .await
1392} 1559}
1393 1560
1561async fn create_close_status_for_original_patch(
1562 signer: &Arc<dyn NostrSigner>,
1563 repo_ref: &RepoRef,
1564 proposal: &Event,
1565) -> Result<Event> {
1566 let mut public_keys = repo_ref
1567 .maintainers
1568 .iter()
1569 .copied()
1570 .collect::<HashSet<PublicKey>>();
1571 public_keys.insert(proposal.pubkey);
1572
1573 sign_event(
1574 EventBuilder::new(nostr::event::Kind::GitStatusClosed, String::new()).tags(
1575 [
1576 vec![
1577 Tag::custom(
1578 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
1579 vec![
1580 "Git patch closed as forthcoming update is too large. Replacing with Pull Request"
1581 .to_string(),
1582 ],
1583 ),
1584 Tag::from_standardized(nostr::TagStandard::Event {
1585 event_id: proposal.id,
1586 relay_url: repo_ref.relays.first().cloned(),
1587 marker: Some(Marker::Root),
1588 public_key: None,
1589 uppercase: false,
1590 }),
1591 ],
1592 public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(),
1593 repo_ref
1594 .coordinates()
1595 .iter()
1596 .map(|c| {
1597 Tag::from_standardized(TagStandard::Coordinate {
1598 coordinate: c.coordinate.clone(),
1599 relay_url: c.relays.first().cloned(),
1600 uppercase: false,
1601 })
1602 })
1603 .collect::<Vec<Tag>>(),
1604 vec![
1605 Tag::from_standardized(nostr::TagStandard::Reference(
1606 repo_ref.root_commit.to_string(),
1607 )),
1608 ],
1609 ]
1610 .concat(),
1611 ),
1612 signer,
1613 "close status for original patch".to_string(),
1614 )
1615 .await
1616}
1394async fn get_proposal_and_revision_root_from_patch( 1617async fn get_proposal_and_revision_root_from_patch(
1395 git_repo: &Repo, 1618 git_repo: &Repo,
1396 patch: &Event, 1619 patch: &Event,
diff --git a/src/bin/git_remote_nostr/utils.rs b/src/bin/git_remote_nostr/utils.rs
index dc75872..2cb85bf 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};
10use git2::Repository; 10use git2::Repository;
11use ngit::{ 11use ngit::{
12 client::{ 12 client::{
13 get_all_proposal_patch_events_from_cache, get_events_from_local_cache, 13 get_all_proposal_patch_pr_pr_update_events_from_cache, get_events_from_local_cache,
14 get_proposals_and_revisions_from_cache, 14 get_proposals_and_revisions_from_cache,
15 }, 15 },
16 git::{ 16 git::{
@@ -18,7 +18,7 @@ use ngit::{
18 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 18 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
19 }, 19 },
20 git_events::{ 20 git_events::{
21 event_is_revision_root, get_most_recent_patch_with_ancestors, 21 event_is_revision_root, get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status,
22 is_event_proposal_root_for_branch, status_kinds, 22 is_event_proposal_root_for_branch, status_kinds,
23 }, 23 },
24 repo_ref::RepoRef, 24 repo_ref::RepoRef,
@@ -103,7 +103,10 @@ pub async fn get_open_or_draft_proposals(
103 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) 103 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates())
104 .await? 104 .await?
105 .iter() 105 .iter()
106 .filter(|e| !event_is_revision_root(e)) 106 .filter(|e|
107 // If we wanted to treat to list Pull Requests that revise a Patch we would do this:
108 // e.kind.eq(&KIND_PULL_REQUEST) ||
109 !event_is_revision_root(e))
107 .cloned() 110 .cloned()
108 .collect(); 111 .collect();
109 112
@@ -123,32 +126,23 @@ pub async fn get_open_or_draft_proposals(
123 }; 126 };
124 let mut open_or_draft_proposals = HashMap::new(); 127 let mut open_or_draft_proposals = HashMap::new();
125 128
126 for proposal in proposals { 129 for proposal in &proposals {
127 let status = if let Some(e) = statuses 130 let status = get_status(proposal, repo_ref, &statuses, &proposals);
128 .iter()
129 .filter(|e| {
130 status_kinds().contains(&e.kind)
131 && e.tags.iter().any(|t| {
132 t.as_slice().len() > 1 && t.as_slice()[1].eq(&proposal.id.to_string())
133 })
134 })
135 .collect::<Vec<&nostr::Event>>()
136 .first()
137 {
138 e.kind
139 } else {
140 Kind::GitStatusOpen
141 };
142 if [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&status) { 131 if [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&status) {
143 if let Ok(commits_events) = 132 if let Ok(commits_events) = get_all_proposal_patch_pr_pr_update_events_from_cache(
144 get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id) 133 git_repo_path,
145 .await 134 repo_ref,
135 &proposal.id,
136 )
137 .await
146 { 138 {
147 if let Ok(most_recent_proposal_patch_chain) = 139 if let Ok(most_recent_proposal_patch_chain) =
148 get_most_recent_patch_with_ancestors(commits_events.clone()) 140 get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone())
149 { 141 {
150 open_or_draft_proposals 142 open_or_draft_proposals.insert(
151 .insert(proposal.id, (proposal, most_recent_proposal_patch_chain)); 143 proposal.id,
144 (proposal.clone(), most_recent_proposal_patch_chain),
145 );
152 } 146 }
153 } 147 }
154 } 148 }
@@ -165,18 +159,25 @@ pub async fn get_all_proposals(
165 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) 159 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates())
166 .await? 160 .await?
167 .iter() 161 .iter()
168 .filter(|e| !event_is_revision_root(e)) 162 .filter(|e|
163 // If we wanted to treat to list Pull Requests that revise a Patch we would do this:
164 // e.kind.eq(&KIND_PULL_REQUEST) ||
165 !event_is_revision_root(e))
169 .cloned() 166 .cloned()
170 .collect(); 167 .collect();
171 168
172 let mut all_proposals = HashMap::new(); 169 let mut all_proposals = HashMap::new();
173 170
174 for proposal in proposals { 171 for proposal in proposals {
175 if let Ok(commits_events) = 172 if let Ok(commits_events) = get_all_proposal_patch_pr_pr_update_events_from_cache(
176 get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id).await 173 git_repo_path,
174 repo_ref,
175 &proposal.id,
176 )
177 .await
177 { 178 {
178 if let Ok(most_recent_proposal_patch_chain) = 179 if let Ok(most_recent_proposal_patch_chain) =
179 get_most_recent_patch_with_ancestors(commits_events.clone()) 180 get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone())
180 { 181 {
181 all_proposals.insert(proposal.id, (proposal, most_recent_proposal_patch_chain)); 182 all_proposals.insert(proposal.id, (proposal, most_recent_proposal_patch_chain));
182 } 183 }
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs
index 1242e45..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<()> {
229 .map(std::string::ToString::to_string) 229 .map(std::string::ToString::to_string)
230 .collect::<Vec<String>>() 230 .collect::<Vec<String>>()
231 } else { 231 } else {
232 client.get_fallback_relays().clone() 232 client.get_relay_default_set().clone()
233 } 233 }
234 } else { 234 } else {
235 args.relays.clone() 235 args.relays.clone()
@@ -252,14 +252,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
252 args.blossoms.clone() 252 args.blossoms.clone()
253 }; 253 };
254 254
255 let fallback_grasp_servers = 255 let fallback_grasp_servers = client.get_grasp_default_set();
256 if let Ok(Some(s)) = git_repo.get_git_config_item("nostr.grasp-default-set", None) {
257 s.split(';')
258 .filter_map(|url| normalize_grasp_server_url(url).ok()) // Attempt to parse and filter out errors
259 .collect()
260 } else {
261 vec!["relay.ngit.dev".to_string(), "gitnostr.com".to_string()]
262 };
263 256
264 let selected_grasp_servers = if has_server_and_relay_flags { 257 let selected_grasp_servers = if has_server_and_relay_flags {
265 // ignore so a script running `ngit init` can contiue without prompts 258 // ignore so a script running `ngit init` can contiue without prompts
@@ -269,14 +262,13 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
269 repo_ref.as_ref(), 262 repo_ref.as_ref(),
270 &args.relays, 263 &args.relays,
271 &args.clone_url, 264 &args.clone_url,
272 &args.blossoms,
273 &identifier, 265 &identifier,
274 ); 266 );
275 let mut selections: Vec<bool> = vec![true; options.len()]; // Initialize selections based on existing options 267 let mut selections: Vec<bool> = vec![true; options.len()]; // Initialize selections based on existing options
276 let empty = options.is_empty(); 268 let empty = options.is_empty();
277 for fallback in fallback_grasp_servers { 269 for fallback in fallback_grasp_servers {
278 // Check if any option contains the fallback as a substring 270 // Check if any option contains the fallback as a substring
279 if !options.iter().any(|option| option.contains(&fallback)) { 271 if !options.iter().any(|option| option.contains(fallback)) {
280 options.push(fallback.clone()); // Add fallback if not found 272 options.push(fallback.clone()); // Add fallback if not found
281 selections.push(empty); // mark as selected if no existing ngit relay otherwise not 273 selections.push(empty); // mark as selected if no existing ngit relay otherwise not
282 } 274 }
@@ -464,7 +456,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
464 let mut selections: Vec<bool> = vec![true; options.len()]; 456 let mut selections: Vec<bool> = vec![true; options.len()];
465 457
466 // add fallback relays as options 458 // add fallback relays as options
467 for relay in client.get_fallback_relays().clone() { 459 for relay in client.get_relay_default_set().clone() {
468 if !options.iter().any(|r| r.contains(&relay)) 460 if !options.iter().any(|r| r.contains(&relay))
469 && !formatted_selected_grasp_servers 461 && !formatted_selected_grasp_servers
470 .iter() 462 .iter()
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs
index 0330be1..0083c91 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};
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use ngit::{ 4use ngit::{
5 client::{ 5 client::{
6 Params, get_all_proposal_patch_events_from_cache, get_proposals_and_revisions_from_cache, 6 Params, get_all_proposal_patch_pr_pr_update_events_from_cache,
7 get_proposals_and_revisions_from_cache,
7 }, 8 },
8 git_events::{ 9 git_events::{
9 get_commit_id_from_patch, get_most_recent_patch_with_ancestors, status_kinds, tag_value, 10 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch,
11 get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, status_kinds, tag_value,
10 }, 12 },
11}; 13};
12use nostr_sdk::Kind; 14use nostr_sdk::Kind;
@@ -70,26 +72,15 @@ pub async fn launch() -> Result<()> {
70 72
71 let proposals: Vec<nostr::Event> = proposals_and_revisions 73 let proposals: Vec<nostr::Event> = proposals_and_revisions
72 .iter() 74 .iter()
73 .filter(|e| !event_is_revision_root(e)) 75 .filter(|e|
76 // If we wanted to treat to list Pull Requests that revise a Patch we would do this:
77 // e.kind.eq(&KIND_PULL_REQUEST) ||
78 !event_is_revision_root(e))
74 .cloned() 79 .cloned()
75 .collect(); 80 .collect();
76 81
77 for proposal in &proposals { 82 for proposal in &proposals {
78 let status = if let Some(e) = statuses 83 let status = get_status(proposal, &repo_ref, &statuses, &proposals);
79 .iter()
80 .filter(|e| {
81 status_kinds().contains(&e.kind)
82 && e.tags.iter().any(|t| {
83 t.as_slice().len() > 1 && t.as_slice()[1].eq(&proposal.id.to_string())
84 })
85 })
86 .collect::<Vec<&nostr::Event>>()
87 .first()
88 {
89 e.kind
90 } else {
91 Kind::GitStatusOpen
92 };
93 if status.eq(&Kind::GitStatusOpen) { 84 if status.eq(&Kind::GitStatusOpen) {
94 open_proposals.push(proposal); 85 open_proposals.push(proposal);
95 } else if status.eq(&Kind::GitStatusClosed) { 86 } else if status.eq(&Kind::GitStatusClosed) {
@@ -184,21 +175,22 @@ pub async fn launch() -> Result<()> {
184 let cover_letter = event_to_cover_letter(proposals_for_status[selected_index]) 175 let cover_letter = event_to_cover_letter(proposals_for_status[selected_index])
185 .context("failed to extract proposal details from proposal root event")?; 176 .context("failed to extract proposal details from proposal root event")?;
186 177
187 let commits_events: Vec<nostr::Event> = get_all_proposal_patch_events_from_cache( 178 let commits_events: Vec<nostr::Event> =
188 git_repo_path, 179 get_all_proposal_patch_pr_pr_update_events_from_cache(
189 &repo_ref, 180 git_repo_path,
190 &proposals_for_status[selected_index].id, 181 &repo_ref,
191 ) 182 &proposals_for_status[selected_index].id,
192 .await?; 183 )
184 .await?;
193 185
194 let Ok(most_recent_proposal_patch_chain) = 186 let Ok(most_recent_proposal_patch_chain_or_pr_or_pr_update) =
195 get_most_recent_patch_with_ancestors(commits_events.clone()) 187 get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone())
196 else { 188 else {
197 if Interactor::default().confirm( 189 if Interactor::default().confirm(
198 PromptConfirmParms::default() 190 PromptConfirmParms::default()
199 .with_default(true) 191 .with_default(true)
200 .with_prompt( 192 .with_prompt(
201 "failed to find any patches on this proposal. choose another proposal?", 193 "failed to find any PR or patch events on this proposal. choose another proposal?",
202 ), 194 ),
203 )? { 195 )? {
204 continue; 196 continue;
@@ -208,15 +200,60 @@ pub async fn launch() -> Result<()> {
208 // for commit in &most_recent_proposal_patch_chain { 200 // for commit in &most_recent_proposal_patch_chain {
209 // println!("recent_event: {:?}", commit.as_json()); 201 // println!("recent_event: {:?}", commit.as_json());
210 // } 202 // }
203 if most_recent_proposal_patch_chain_or_pr_or_pr_update
204 .iter()
205 .any(|e| [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&e.kind))
206 {
207 match Interactor::default().choice(
208 PromptChoiceParms::default()
209 .with_prompt(
210 "this is new PR event kind which isn't supported in `ngit list` yet",
211 )
212 .with_default(0)
213 .with_choices(
214 if [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&selected_status)
215 && git_repo
216 .get_first_nostr_remote_when_in_ngit_binary()
217 .await
218 .is_ok_and(|r| r.is_some())
219 {
220 vec![
221 format!(
222 "I'll manually checkout the proposal at remote branch '{}'",
223 cover_letter
224 .get_branch_name_with_pr_prefix_and_shorthand_id()
225 .unwrap()
226 ),
227 // TODO fetch oids and follow similar logic for dealing with
228 // conflcts as with patches below
229 "back to proposals".to_string(),
230 ]
231 } else {
232 vec!["back to proposals".to_string()]
233 },
234 ),
235 )? {
236 0 => continue,
237 _ => {
238 bail!("unexpected choice")
239 }
240 };
241 }
211 242
212 let binding_patch_text_ref = format!("{} commits", most_recent_proposal_patch_chain.len()); 243 let binding_patch_text_ref = format!(
213 let patch_text_ref = if most_recent_proposal_patch_chain.len().gt(&1) { 244 "{} commits",
245 most_recent_proposal_patch_chain_or_pr_or_pr_update.len()
246 );
247 let patch_text_ref = if most_recent_proposal_patch_chain_or_pr_or_pr_update
248 .len()
249 .gt(&1)
250 {
214 binding_patch_text_ref.as_str() 251 binding_patch_text_ref.as_str()
215 } else { 252 } else {
216 "1 commit" 253 "1 commit"
217 }; 254 };
218 255
219 let no_support_for_patches_as_branch = most_recent_proposal_patch_chain 256 let no_support_for_patches_as_branch = most_recent_proposal_patch_chain_or_pr_or_pr_update
220 .iter() 257 .iter()
221 .any(|event| !patch_supports_commit_ids(event)); 258 .any(|event| !patch_supports_commit_ids(event));
222 259
@@ -226,16 +263,18 @@ pub async fn launch() -> Result<()> {
226 PromptChoiceParms::default() 263 PromptChoiceParms::default()
227 .with_default(0) 264 .with_default(0)
228 .with_choices(vec![ 265 .with_choices(vec![
229 "learn why 'patch only' proposals can't be checked out".to_string(), 266 "learn why this proposals can't be checked out".to_string(),
230 format!("apply to current branch with `git am`"), 267 format!("apply to current branch with `git am`"),
231 format!("download to ./patches"), 268 format!("download to ./patches"),
232 "back".to_string(), 269 "back".to_string(),
233 ]), 270 ]),
234 )? { 271 )? {
235 0 => { 272 0 => {
236 println!("Some proposals are posted as 'patch only'\n");
237 println!( 273 println!(
238 "they are not anchored against a particular state of the code base like a standard proposal or a GitHub Pull Request can be\n" 274 "Some proposals are posted as patch without listing a parent commit\n"
275 );
276 println!(
277 "they are not anchored against a particular state of the code base like a standard patch or a pull request can be\n"
239 ); 278 );
240 println!( 279 println!(
241 "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" 280 "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"
@@ -244,7 +283,7 @@ pub async fn launch() -> Result<()> {
244 "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" 283 "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"
245 ); 284 );
246 println!( 285 println!(
247 "by default ngit posts proposals that support both the branch and patch model so either workflow can be used" 286 "by default ngit posts proposals with a parent commit so either workflow can be used"
248 ); 287 );
249 Interactor::default().choice( 288 Interactor::default().choice(
250 PromptChoiceParms::default() 289 PromptChoiceParms::default()
@@ -253,8 +292,13 @@ pub async fn launch() -> Result<()> {
253 )?; 292 )?;
254 continue; 293 continue;
255 } 294 }
256 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), 295 1 => {
257 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), 296 launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update)
297 }
298 2 => save_patches_to_dir(
299 most_recent_proposal_patch_chain_or_pr_or_pr_update,
300 &git_repo,
301 ),
258 3 => continue, 302 3 => continue,
259 _ => { 303 _ => {
260 bail!("unexpected choice") 304 bail!("unexpected choice")
@@ -277,9 +321,11 @@ pub async fn launch() -> Result<()> {
277 .eq(&cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?); 321 .eq(&cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?);
278 322
279 let proposal_base_commit = str_to_sha1(&tag_value( 323 let proposal_base_commit = str_to_sha1(&tag_value(
280 most_recent_proposal_patch_chain.last().context( 324 most_recent_proposal_patch_chain_or_pr_or_pr_update
281 "there should be at least one patch as we have already checked for this", 325 .last()
282 )?, 326 .context(
327 "there should be at least one patch as we have already checked for this",
328 )?,
283 "parent-commit", 329 "parent-commit",
284 )?) 330 )?)
285 .context("failed to get valid parent commit id from patch")?; 331 .context("failed to get valid parent commit id from patch")?;
@@ -300,8 +346,8 @@ pub async fn launch() -> Result<()> {
300 ], 346 ],
301 ))? { 347 ))? {
302 0 | 3 => continue, 348 0 | 3 => continue,
303 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), 349 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update),
304 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), 350 2 => save_patches_to_dir(most_recent_proposal_patch_chain_or_pr_or_pr_update, &git_repo),
305 _ => { 351 _ => {
306 bail!("unexpected choice") 352 bail!("unexpected choice")
307 } 353 }
@@ -309,9 +355,13 @@ pub async fn launch() -> Result<()> {
309 } 355 }
310 356
311 let proposal_tip = str_to_sha1( 357 let proposal_tip = str_to_sha1(
312 &get_commit_id_from_patch(most_recent_proposal_patch_chain.first().context( 358 &get_commit_id_from_patch(
313 "there should be at least one patch as we have already checked for this", 359 most_recent_proposal_patch_chain_or_pr_or_pr_update
314 )?) 360 .first()
361 .context(
362 "there should be at least one patch as we have already checked for this",
363 )?,
364 )
315 .context("failed to get valid commit_id from patch")?, 365 .context("failed to get valid commit_id from patch")?,
316 ) 366 )
317 .context("failed to get valid commit_id from patch")?; 367 .context("failed to get valid commit_id from patch")?;
@@ -325,7 +375,7 @@ pub async fn launch() -> Result<()> {
325 .choice(PromptChoiceParms::default().with_default(0).with_choices(vec![ 375 .choice(PromptChoiceParms::default().with_default(0).with_choices(vec![
326 format!( 376 format!(
327 "create and checkout proposal branch ({} ahead {} behind '{main_branch_name}')", 377 "create and checkout proposal branch ({} ahead {} behind '{main_branch_name}')",
328 most_recent_proposal_patch_chain.len(), 378 most_recent_proposal_patch_chain_or_pr_or_pr_update.len(),
329 proposal_behind_main.len(), 379 proposal_behind_main.len(),
330 ), 380 ),
331 format!("apply to current branch with `git am`"), 381 format!("apply to current branch with `git am`"),
@@ -337,7 +387,7 @@ pub async fn launch() -> Result<()> {
337 let _ = git_repo 387 let _ = git_repo
338 .apply_patch_chain( 388 .apply_patch_chain(
339 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 389 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
340 most_recent_proposal_patch_chain, 390 most_recent_proposal_patch_chain_or_pr_or_pr_update,
341 ) 391 )
342 .context("failed to apply patch chain")?; 392 .context("failed to apply patch chain")?;
343 393
@@ -347,8 +397,8 @@ pub async fn launch() -> Result<()> {
347 ); 397 );
348 Ok(()) 398 Ok(())
349 } 399 }
350 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), 400 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update),
351 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), 401 2 => save_patches_to_dir(most_recent_proposal_patch_chain_or_pr_or_pr_update, &git_repo),
352 3 => continue, 402 3 => continue,
353 _ => { 403 _ => {
354 bail!("unexpected choice") 404 bail!("unexpected choice")
@@ -382,7 +432,7 @@ pub async fn launch() -> Result<()> {
382 .with_choices(vec![ 432 .with_choices(vec![
383 format!( 433 format!(
384 "checkout proposal branch ({} ahead {} behind '{main_branch_name}')", 434 "checkout proposal branch ({} ahead {} behind '{main_branch_name}')",
385 most_recent_proposal_patch_chain.len(), 435 most_recent_proposal_patch_chain_or_pr_or_pr_update.len(),
386 proposal_behind_main.len(), 436 proposal_behind_main.len(),
387 ), 437 ),
388 format!("apply to current branch with `git am`"), 438 format!("apply to current branch with `git am`"),
@@ -401,8 +451,13 @@ pub async fn launch() -> Result<()> {
401 ); 451 );
402 Ok(()) 452 Ok(())
403 } 453 }
404 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), 454 1 => {
405 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), 455 launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update)
456 }
457 2 => save_patches_to_dir(
458 most_recent_proposal_patch_chain_or_pr_or_pr_update,
459 &git_repo,
460 ),
406 3 => continue, 461 3 => continue,
407 _ => { 462 _ => {
408 bail!("unexpected choice") 463 bail!("unexpected choice")
@@ -414,11 +469,14 @@ pub async fn launch() -> Result<()> {
414 git_repo.get_commits_ahead_behind(&master_tip, &local_branch_tip)?; 469 git_repo.get_commits_ahead_behind(&master_tip, &local_branch_tip)?;
415 470
416 // new appendments to proposal 471 // new appendments to proposal
417 if let Some(index) = most_recent_proposal_patch_chain.iter().position(|patch| { 472 if let Some(index) = most_recent_proposal_patch_chain_or_pr_or_pr_update
418 get_commit_id_from_patch(patch) 473 .iter()
419 .unwrap_or_default() 474 .position(|patch| {
420 .eq(&local_branch_tip.to_string()) 475 get_commit_id_from_patch(patch)
421 }) { 476 .unwrap_or_default()
477 .eq(&local_branch_tip.to_string())
478 })
479 {
422 return match Interactor::default().choice( 480 return match Interactor::default().choice(
423 PromptChoiceParms::default() 481 PromptChoiceParms::default()
424 .with_default(0) 482 .with_default(0)
@@ -437,7 +495,7 @@ pub async fn launch() -> Result<()> {
437 let _ = git_repo 495 let _ = git_repo
438 .apply_patch_chain( 496 .apply_patch_chain(
439 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 497 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
440 most_recent_proposal_patch_chain, 498 most_recent_proposal_patch_chain_or_pr_or_pr_update,
441 ) 499 )
442 .context("failed to apply patch chain")?; 500 .context("failed to apply patch chain")?;
443 println!( 501 println!(
@@ -448,8 +506,13 @@ pub async fn launch() -> Result<()> {
448 ); 506 );
449 Ok(()) 507 Ok(())
450 } 508 }
451 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), 509 1 => {
452 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), 510 launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update)
511 }
512 2 => save_patches_to_dir(
513 most_recent_proposal_patch_chain_or_pr_or_pr_update,
514 &git_repo,
515 ),
453 3 => continue, 516 3 => continue,
454 _ => { 517 _ => {
455 bail!("unexpected choice") 518 bail!("unexpected choice")
@@ -467,7 +530,7 @@ pub async fn launch() -> Result<()> {
467 }) { 530 }) {
468 println!( 531 println!(
469 "updated proposal available ({} ahead {} behind '{main_branch_name}'). existing version is {} ahead {} behind '{main_branch_name}'", 532 "updated proposal available ({} ahead {} behind '{main_branch_name}'). existing version is {} ahead {} behind '{main_branch_name}'",
470 most_recent_proposal_patch_chain.len(), 533 most_recent_proposal_patch_chain_or_pr_or_pr_update.len(),
471 proposal_behind_main.len(), 534 proposal_behind_main.len(),
472 local_ahead_of_main.len(), 535 local_ahead_of_main.len(),
473 local_beind_main.len(), 536 local_beind_main.len(),
@@ -492,11 +555,11 @@ pub async fn launch() -> Result<()> {
492 git_repo.checkout( 555 git_repo.checkout(
493 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 556 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
494 )?; 557 )?;
495 let chain_length = most_recent_proposal_patch_chain.len(); 558 let chain_length = most_recent_proposal_patch_chain_or_pr_or_pr_update.len();
496 let _ = git_repo 559 let _ = git_repo
497 .apply_patch_chain( 560 .apply_patch_chain(
498 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 561 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
499 most_recent_proposal_patch_chain, 562 most_recent_proposal_patch_chain_or_pr_or_pr_update,
500 ) 563 )
501 .context("failed to apply patch chain")?; 564 .context("failed to apply patch chain")?;
502 println!( 565 println!(
@@ -520,8 +583,13 @@ pub async fn launch() -> Result<()> {
520 ); 583 );
521 Ok(()) 584 Ok(())
522 } 585 }
523 2 => launch_git_am_with_patches(most_recent_proposal_patch_chain), 586 2 => {
524 3 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), 587 launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update)
588 }
589 3 => save_patches_to_dir(
590 most_recent_proposal_patch_chain_or_pr_or_pr_update,
591 &git_repo,
592 ),
525 4 => continue, 593 4 => continue,
526 _ => { 594 _ => {
527 bail!("unexpected choice") 595 bail!("unexpected choice")
@@ -581,7 +649,7 @@ pub async fn launch() -> Result<()> {
581 if git_repo.does_commit_exist(&proposal_tip.to_string())? { 649 if git_repo.does_commit_exist(&proposal_tip.to_string())? {
582 println!( 650 println!(
583 "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}')", 651 "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}')",
584 most_recent_proposal_patch_chain.len(), 652 most_recent_proposal_patch_chain_or_pr_or_pr_update.len(),
585 proposal_behind_main.len(), 653 proposal_behind_main.len(),
586 local_ahead_of_main.len(), 654 local_ahead_of_main.len(),
587 local_beind_main.len(), 655 local_beind_main.len(),
@@ -594,7 +662,7 @@ pub async fn launch() -> Result<()> {
594 "your local proposal branch ({} ahead {} behind '{main_branch_name}') has conflicting changes with the latest published proposal ({} ahead {} behind '{main_branch_name}')", 662 "your local proposal branch ({} ahead {} behind '{main_branch_name}') has conflicting changes with the latest published proposal ({} ahead {} behind '{main_branch_name}')",
595 local_ahead_of_main.len(), 663 local_ahead_of_main.len(),
596 local_beind_main.len(), 664 local_beind_main.len(),
597 most_recent_proposal_patch_chain.len(), 665 most_recent_proposal_patch_chain_or_pr_or_pr_update.len(),
598 proposal_behind_main.len(), 666 proposal_behind_main.len(),
599 ); 667 );
600 668
@@ -639,11 +707,11 @@ pub async fn launch() -> Result<()> {
639 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 707 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
640 &proposal_base_commit.to_string(), 708 &proposal_base_commit.to_string(),
641 )?; 709 )?;
642 let chain_length = most_recent_proposal_patch_chain.len(); 710 let chain_length = most_recent_proposal_patch_chain_or_pr_or_pr_update.len();
643 let _ = git_repo 711 let _ = git_repo
644 .apply_patch_chain( 712 .apply_patch_chain(
645 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 713 &cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
646 most_recent_proposal_patch_chain, 714 most_recent_proposal_patch_chain_or_pr_or_pr_update,
647 ) 715 )
648 .context("failed to apply patch chain")?; 716 .context("failed to apply patch chain")?;
649 717
@@ -658,8 +726,11 @@ pub async fn launch() -> Result<()> {
658 ); 726 );
659 Ok(()) 727 Ok(())
660 } 728 }
661 2 => launch_git_am_with_patches(most_recent_proposal_patch_chain), 729 2 => launch_git_am_with_patches(most_recent_proposal_patch_chain_or_pr_or_pr_update),
662 3 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), 730 3 => save_patches_to_dir(
731 most_recent_proposal_patch_chain_or_pr_or_pr_update,
732 &git_repo,
733 ),
663 4 => continue, 734 4 => continue,
664 _ => { 735 _ => {
665 bail!("unexpected choice") 736 bail!("unexpected choice")