upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/git_remote_nostr
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/git_remote_nostr')
-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
4 files changed, 447 insertions, 106 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 }