diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/git_remote_nostr/fetch.rs | 105 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/list.rs | 56 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/push.rs | 333 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/utils.rs | 59 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/init.rs | 16 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/list.rs | 211 | ||||
| -rw-r--r-- | src/lib/client.rs | 164 | ||||
| -rw-r--r-- | src/lib/git/mod.rs | 20 | ||||
| -rw-r--r-- | src/lib/git_events.rs | 321 | ||||
| -rw-r--r-- | src/lib/login/fresh.rs | 4 | ||||
| -rw-r--r-- | src/lib/repo_ref.rs | 23 |
11 files changed, 1033 insertions, 279 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 @@ | |||
| 1 | use core::str; | 1 | use core::str; |
| 2 | use std::{ | 2 | use 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 | ||
| 129 | async 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 | |||
| 88 | pub fn make_commits_for_proposal( | 159 | pub 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 | ||
| 131 | async fn fetch_open_or_draft_proposals( | 202 | async 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 | |||
| 12 | use console::Term; | 12 | use console::Term; |
| 13 | use git::{RepoActions, sha1_to_oid}; | 13 | use git::{RepoActions, sha1_to_oid}; |
| 14 | use git_events::{ | 14 | use 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 | }; |
| 17 | use git2::{Oid, Repository}; | 18 | use git2::{Oid, Repository}; |
| 18 | use ngit::{ | 19 | use 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 | }; |
| 31 | use nostr::nips::nip10::Marker; | 32 | use nostr::{event::UnsignedEvent, nips::nip10::Marker}; |
| 32 | use nostr_sdk::{ | 33 | use 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 | ||
| 177 | async fn create_and_publish_events( | 178 | async 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 | ||
| 454 | fn 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)] | ||
| 468 | async 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 | |||
| 426 | fn push_to_remote( | 593 | fn 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 = | |||
| 1079 | async fn get_merged_proposals_info( | 1246 | async 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 | ||
| 1561 | async 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 | } | ||
| 1394 | async fn get_proposal_and_revision_root_from_patch( | 1617 | async 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}; | |||
| 10 | use git2::Repository; | 10 | use git2::Repository; |
| 11 | use ngit::{ | 11 | use 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}; | |||
| 3 | use anyhow::{Context, Result, bail}; | 3 | use anyhow::{Context, Result, bail}; |
| 4 | use ngit::{ | 4 | use 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 | }; |
| 12 | use nostr_sdk::Kind; | 14 | use 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") |
diff --git a/src/lib/client.rs b/src/lib/client.rs index e808bea..6f28cff 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs | |||
| @@ -31,6 +31,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressState, P | |||
| 31 | use mockall::*; | 31 | use mockall::*; |
| 32 | use nostr::{ | 32 | use nostr::{ |
| 33 | Event, | 33 | Event, |
| 34 | event::{TagKind, TagStandard, UnsignedEvent}, | ||
| 34 | filter::Alphabet, | 35 | filter::Alphabet, |
| 35 | nips::{nip01::Coordinate, nip19::Nip19Coordinate}, | 36 | nips::{nip01::Coordinate, nip19::Nip19Coordinate}, |
| 36 | signer::SignerBackend, | 37 | signer::SignerBackend, |
| @@ -47,20 +48,23 @@ use crate::{ | |||
| 47 | get_dirs, | 48 | get_dirs, |
| 48 | git::{Repo, RepoActions, get_git_config_item}, | 49 | git::{Repo, RepoActions, get_git_config_item}, |
| 49 | git_events::{ | 50 | git_events::{ |
| 50 | event_is_cover_letter, event_is_patch_set_root, event_is_revision_root, status_kinds, | 51 | KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_cover_letter, |
| 52 | event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update, | ||
| 53 | status_kinds, | ||
| 51 | }, | 54 | }, |
| 52 | login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, | 55 | login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, |
| 53 | repo_ref::RepoRef, | 56 | repo_ref::{RepoRef, normalize_grasp_server_url}, |
| 54 | repo_state::RepoState, | 57 | repo_state::RepoState, |
| 55 | }; | 58 | }; |
| 56 | 59 | ||
| 57 | #[allow(clippy::struct_field_names)] | 60 | #[allow(clippy::struct_field_names)] |
| 58 | pub struct Client { | 61 | pub struct Client { |
| 59 | client: nostr_sdk::Client, | 62 | client: nostr_sdk::Client, |
| 60 | fallback_relays: Vec<String>, | 63 | relay_default_set: Vec<String>, |
| 61 | more_fallback_relays: Vec<String>, | 64 | more_fallback_relays: Vec<String>, |
| 62 | blaster_relays: Vec<String>, | 65 | blaster_relays: Vec<String>, |
| 63 | fallback_signer_relays: Vec<String>, | 66 | fallback_signer_relays: Vec<String>, |
| 67 | grasp_default_set: Vec<String>, | ||
| 64 | relays_not_to_retry: Arc<RwLock<HashMap<RelayUrl, String>>>, | 68 | relays_not_to_retry: Arc<RwLock<HashMap<RelayUrl, String>>>, |
| 65 | } | 69 | } |
| 66 | 70 | ||
| @@ -94,10 +98,11 @@ pub trait Connect { | |||
| 94 | async fn set_signer(&mut self, signer: Arc<dyn NostrSigner>); | 98 | async fn set_signer(&mut self, signer: Arc<dyn NostrSigner>); |
| 95 | async fn connect(&self, relay_url: &RelayUrl) -> Result<()>; | 99 | async fn connect(&self, relay_url: &RelayUrl) -> Result<()>; |
| 96 | async fn disconnect(&self) -> Result<()>; | 100 | async fn disconnect(&self) -> Result<()>; |
| 97 | fn get_fallback_relays(&self) -> &Vec<String>; | 101 | fn get_relay_default_set(&self) -> &Vec<String>; |
| 98 | fn get_more_fallback_relays(&self) -> &Vec<String>; | 102 | fn get_more_fallback_relays(&self) -> &Vec<String>; |
| 99 | fn get_blaster_relays(&self) -> &Vec<String>; | 103 | fn get_blaster_relays(&self) -> &Vec<String>; |
| 100 | fn get_fallback_signer_relays(&self) -> &Vec<String>; | 104 | fn get_fallback_signer_relays(&self) -> &Vec<String>; |
| 105 | fn get_grasp_default_set(&self) -> &Vec<String>; | ||
| 101 | async fn send_event_to<'a>( | 106 | async fn send_event_to<'a>( |
| 102 | &self, | 107 | &self, |
| 103 | git_repo_path: Option<&'a Path>, | 108 | git_repo_path: Option<&'a Path>, |
| @@ -147,10 +152,11 @@ impl Connect for Client { | |||
| 147 | .opts(Options::new().relay_limits(RelayLimits::disable())) | 152 | .opts(Options::new().relay_limits(RelayLimits::disable())) |
| 148 | .build() | 153 | .build() |
| 149 | }, | 154 | }, |
| 150 | fallback_relays: opts.fallback_relays, | 155 | relay_default_set: opts.relay_default_set, |
| 151 | more_fallback_relays: opts.more_fallback_relays, | 156 | more_fallback_relays: opts.more_fallback_relays, |
| 152 | blaster_relays: opts.blaster_relays, | 157 | blaster_relays: opts.blaster_relays, |
| 153 | fallback_signer_relays: opts.fallback_signer_relays, | 158 | fallback_signer_relays: opts.fallback_signer_relays, |
| 159 | grasp_default_set: opts.grasp_default_set, | ||
| 154 | relays_not_to_retry: Arc::new(RwLock::new(HashMap::new())), | 160 | relays_not_to_retry: Arc::new(RwLock::new(HashMap::new())), |
| 155 | } | 161 | } |
| 156 | } | 162 | } |
| @@ -189,8 +195,8 @@ impl Connect for Client { | |||
| 189 | Ok(()) | 195 | Ok(()) |
| 190 | } | 196 | } |
| 191 | 197 | ||
| 192 | fn get_fallback_relays(&self) -> &Vec<String> { | 198 | fn get_relay_default_set(&self) -> &Vec<String> { |
| 193 | &self.fallback_relays | 199 | &self.relay_default_set |
| 194 | } | 200 | } |
| 195 | 201 | ||
| 196 | fn get_more_fallback_relays(&self) -> &Vec<String> { | 202 | fn get_more_fallback_relays(&self) -> &Vec<String> { |
| @@ -205,6 +211,10 @@ impl Connect for Client { | |||
| 205 | &self.fallback_signer_relays | 211 | &self.fallback_signer_relays |
| 206 | } | 212 | } |
| 207 | 213 | ||
| 214 | fn get_grasp_default_set(&self) -> &Vec<String> { | ||
| 215 | &self.grasp_default_set | ||
| 216 | } | ||
| 217 | |||
| 208 | async fn send_event_to<'a>( | 218 | async fn send_event_to<'a>( |
| 209 | &self, | 219 | &self, |
| 210 | git_repo_path: Option<&'a Path>, | 220 | git_repo_path: Option<&'a Path>, |
| @@ -335,8 +345,8 @@ impl Connect for Client { | |||
| 335 | trusted_maintainer_coordinate: Option<&'a Nip19Coordinate>, | 345 | trusted_maintainer_coordinate: Option<&'a Nip19Coordinate>, |
| 336 | user_profiles: &HashSet<PublicKey>, | 346 | user_profiles: &HashSet<PublicKey>, |
| 337 | ) -> Result<(Vec<Result<FetchReport>>, MultiProgress)> { | 347 | ) -> Result<(Vec<Result<FetchReport>>, MultiProgress)> { |
| 338 | let fallback_relays = &self | 348 | let relay_default_set = &self |
| 339 | .fallback_relays | 349 | .relay_default_set |
| 340 | .iter() | 350 | .iter() |
| 341 | .filter_map(|r| RelayUrl::parse(r).ok()) | 351 | .filter_map(|r| RelayUrl::parse(r).ok()) |
| 342 | .collect::<HashSet<RelayUrl>>(); | 352 | .collect::<HashSet<RelayUrl>>(); |
| @@ -345,7 +355,7 @@ impl Connect for Client { | |||
| 345 | git_repo_path, | 355 | git_repo_path, |
| 346 | trusted_maintainer_coordinate, | 356 | trusted_maintainer_coordinate, |
| 347 | user_profiles, | 357 | user_profiles, |
| 348 | fallback_relays.clone(), | 358 | relay_default_set.clone(), |
| 349 | ) | 359 | ) |
| 350 | .await?; | 360 | .await?; |
| 351 | 361 | ||
| @@ -685,17 +695,18 @@ async fn get_events_of( | |||
| 685 | 695 | ||
| 686 | pub struct Params { | 696 | pub struct Params { |
| 687 | pub keys: Option<nostr::Keys>, | 697 | pub keys: Option<nostr::Keys>, |
| 688 | pub fallback_relays: Vec<String>, | 698 | pub relay_default_set: Vec<String>, |
| 689 | pub more_fallback_relays: Vec<String>, | 699 | pub more_fallback_relays: Vec<String>, |
| 690 | pub blaster_relays: Vec<String>, | 700 | pub blaster_relays: Vec<String>, |
| 691 | pub fallback_signer_relays: Vec<String>, | 701 | pub fallback_signer_relays: Vec<String>, |
| 702 | pub grasp_default_set: Vec<String>, | ||
| 692 | } | 703 | } |
| 693 | 704 | ||
| 694 | impl Default for Params { | 705 | impl Default for Params { |
| 695 | fn default() -> Self { | 706 | fn default() -> Self { |
| 696 | Params { | 707 | Params { |
| 697 | keys: None, | 708 | keys: None, |
| 698 | fallback_relays: if std::env::var("NGITTEST").is_ok() { | 709 | relay_default_set: if std::env::var("NGITTEST").is_ok() { |
| 699 | vec![ | 710 | vec![ |
| 700 | "ws://localhost:8051".to_string(), | 711 | "ws://localhost:8051".to_string(), |
| 701 | "ws://localhost:8052".to_string(), | 712 | "ws://localhost:8052".to_string(), |
| @@ -731,6 +742,11 @@ impl Default for Params { | |||
| 731 | } else { | 742 | } else { |
| 732 | vec!["wss://relay.nsec.app".to_string()] | 743 | vec!["wss://relay.nsec.app".to_string()] |
| 733 | }, | 744 | }, |
| 745 | grasp_default_set: if std::env::var("NGITTEST").is_ok() { | ||
| 746 | vec![] | ||
| 747 | } else { | ||
| 748 | vec!["relay.ngit.dev".to_string(), "gitnostr.com".to_string()] | ||
| 749 | }, | ||
| 734 | } | 750 | } |
| 735 | } | 751 | } |
| 736 | } | 752 | } |
| @@ -749,7 +765,7 @@ impl Params { | |||
| 749 | .collect(); | 765 | .collect(); |
| 750 | // elsewhere it is assumed this isn't empty | 766 | // elsewhere it is assumed this isn't empty |
| 751 | if !new_default_relays.is_empty() { | 767 | if !new_default_relays.is_empty() { |
| 752 | params.fallback_relays = new_default_relays; | 768 | params.relay_default_set = new_default_relays; |
| 753 | } | 769 | } |
| 754 | } | 770 | } |
| 755 | if let Ok(Some(relay_blasters)) = | 771 | if let Ok(Some(relay_blasters)) = |
| @@ -770,6 +786,17 @@ impl Params { | |||
| 770 | .map(|relay_url| relay_url.to_string()) // Convert RelayUrl back to String | 786 | .map(|relay_url| relay_url.to_string()) // Convert RelayUrl back to String |
| 771 | .collect(); | 787 | .collect(); |
| 772 | } | 788 | } |
| 789 | if let Ok(Some(grasp_default_servers)) = | ||
| 790 | get_git_config_item(git_repo, "nostr.grasp-default-set") | ||
| 791 | { | ||
| 792 | let new_default_grasp_servers: Vec<String> = grasp_default_servers | ||
| 793 | .split(';') | ||
| 794 | .filter_map(|url| normalize_grasp_server_url(url).ok()) // Attempt to parse and filter out errors | ||
| 795 | .collect(); | ||
| 796 | if !new_default_grasp_servers.is_empty() { | ||
| 797 | params.grasp_default_set = new_default_grasp_servers; | ||
| 798 | } | ||
| 799 | } | ||
| 773 | } | 800 | } |
| 774 | params | 801 | params |
| 775 | } | 802 | } |
| @@ -811,6 +838,30 @@ pub async fn sign_event( | |||
| 811 | } | 838 | } |
| 812 | } | 839 | } |
| 813 | 840 | ||
| 841 | pub async fn sign_draft_event( | ||
| 842 | draft_event: UnsignedEvent, | ||
| 843 | signer: &Arc<dyn NostrSigner>, | ||
| 844 | description: String, | ||
| 845 | ) -> Result<nostr::Event> { | ||
| 846 | if signer.backend() == SignerBackend::NostrConnect { | ||
| 847 | let term = console::Term::stderr(); | ||
| 848 | term.write_line(&format!( | ||
| 849 | "signing event ({description}) with remote signer..." | ||
| 850 | ))?; | ||
| 851 | let event = signer | ||
| 852 | .sign_event(draft_event) | ||
| 853 | .await | ||
| 854 | .context("failed to sign event")?; | ||
| 855 | term.clear_last_lines(1)?; | ||
| 856 | Ok(event) | ||
| 857 | } else { | ||
| 858 | signer | ||
| 859 | .sign_event(draft_event) | ||
| 860 | .await | ||
| 861 | .context("failed to sign event") | ||
| 862 | } | ||
| 863 | } | ||
| 864 | |||
| 814 | pub async fn fetch_public_key(signer: &Arc<dyn NostrSigner>) -> Result<nostr::PublicKey> { | 865 | pub async fn fetch_public_key(signer: &Arc<dyn NostrSigner>) -> Result<nostr::PublicKey> { |
| 815 | if signer.backend() == SignerBackend::NostrConnect { | 866 | if signer.backend() == SignerBackend::NostrConnect { |
| 816 | let term = console::Term::stderr(); | 867 | let term = console::Term::stderr(); |
| @@ -1459,7 +1510,7 @@ async fn process_fetched_events( | |||
| 1459 | report.updated_state = Some((event.created_at, event.id)); | 1510 | report.updated_state = Some((event.created_at, event.id)); |
| 1460 | } | 1511 | } |
| 1461 | } | 1512 | } |
| 1462 | } else if event_is_patch_set_root(event) { | 1513 | } else if event_is_patch_set_root(event) || event.kind.eq(&KIND_PULL_REQUEST) { |
| 1463 | fresh_proposal_roots.insert(event.id); | 1514 | fresh_proposal_roots.insert(event.id); |
| 1464 | report.proposals.insert(event.id); | 1515 | report.proposals.insert(event.id); |
| 1465 | if !request.contributors.contains(&event.pubkey) | 1516 | if !request.contributors.contains(&event.pubkey) |
| @@ -1487,12 +1538,23 @@ async fn process_fetched_events( | |||
| 1487 | } | 1538 | } |
| 1488 | for event in &events { | 1539 | for event in &events { |
| 1489 | if !request.existing_events.contains(&event.id) | 1540 | if !request.existing_events.contains(&event.id) |
| 1490 | && !event | 1541 | && (!event |
| 1491 | .tags | 1542 | .tags |
| 1492 | .event_ids() | 1543 | .event_ids() |
| 1493 | .any(|id| report.proposals.contains(id)) | 1544 | .any(|id| report.proposals.contains(id)) |
| 1545 | || event | ||
| 1546 | .tags | ||
| 1547 | .filter_standardized(TagKind::Custom(std::borrow::Cow::Borrowed("E"))) | ||
| 1548 | .filter_map(|t| match t { | ||
| 1549 | TagStandard::Event { event_id, .. } => Some(event_id), | ||
| 1550 | TagStandard::EventReport(event_id, ..) => Some(event_id), | ||
| 1551 | _ => None, | ||
| 1552 | }) | ||
| 1553 | .any(|id| report.proposals.contains(id))) | ||
| 1494 | { | 1554 | { |
| 1495 | if event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event) { | 1555 | if (event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event)) |
| 1556 | || event.kind.eq(&KIND_PULL_REQUEST_UPDATE) | ||
| 1557 | { | ||
| 1496 | report.commits.insert(event.id); | 1558 | report.commits.insert(event.id); |
| 1497 | } else if status_kinds().contains(&event.kind) { | 1559 | } else if status_kinds().contains(&event.kind) { |
| 1498 | report.statuses.insert(event.id); | 1560 | report.statuses.insert(event.id); |
| @@ -1570,7 +1632,7 @@ pub fn get_fetch_filters( | |||
| 1570 | get_filter_state_events(repo_coordinates), | 1632 | get_filter_state_events(repo_coordinates), |
| 1571 | get_filter_repo_events(repo_coordinates), | 1633 | get_filter_repo_events(repo_coordinates), |
| 1572 | nostr::Filter::default() | 1634 | nostr::Filter::default() |
| 1573 | .kinds(vec![Kind::GitPatch, Kind::EventDeletion]) | 1635 | .kinds(vec![Kind::GitPatch, Kind::EventDeletion, KIND_PULL_REQUEST]) |
| 1574 | .custom_tags( | 1636 | .custom_tags( |
| 1575 | SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), | 1637 | SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), |
| 1576 | repo_coordinates | 1638 | repo_coordinates |
| @@ -1584,15 +1646,29 @@ pub fn get_fetch_filters( | |||
| 1584 | vec![] | 1646 | vec![] |
| 1585 | } else { | 1647 | } else { |
| 1586 | vec![ | 1648 | vec![ |
| 1587 | nostr::Filter::default() | 1649 | nostr::Filter::default().events(proposal_ids.clone()).kinds( |
| 1588 | .events(proposal_ids.clone()) | 1650 | [ |
| 1589 | .kinds([vec![Kind::GitPatch, Kind::EventDeletion], status_kinds()].concat()), | 1651 | vec![ |
| 1652 | Kind::GitPatch, | ||
| 1653 | Kind::EventDeletion, | ||
| 1654 | KIND_PULL_REQUEST_UPDATE, | ||
| 1655 | ], | ||
| 1656 | status_kinds(), | ||
| 1657 | ] | ||
| 1658 | .concat(), | ||
| 1659 | ), | ||
| 1590 | nostr::Filter::default() | 1660 | nostr::Filter::default() |
| 1591 | .custom_tags( | 1661 | .custom_tags( |
| 1592 | SingleLetterTag::uppercase(Alphabet::E), | 1662 | SingleLetterTag::uppercase(Alphabet::E), |
| 1593 | proposal_ids.clone(), | 1663 | proposal_ids.clone(), |
| 1594 | ) | 1664 | ) |
| 1595 | .kinds([vec![Kind::GitPatch, Kind::EventDeletion], status_kinds()].concat()), | 1665 | .kinds( |
| 1666 | [ | ||
| 1667 | vec![Kind::EventDeletion, KIND_PULL_REQUEST_UPDATE], | ||
| 1668 | status_kinds(), | ||
| 1669 | ] | ||
| 1670 | .concat(), | ||
| 1671 | ), | ||
| 1596 | ] | 1672 | ] |
| 1597 | }, | 1673 | }, |
| 1598 | if required_profiles.is_empty() { | 1674 | if required_profiles.is_empty() { |
| @@ -1784,7 +1860,7 @@ pub async fn get_proposals_and_revisions_from_cache( | |||
| 1784 | git_repo_path, | 1860 | git_repo_path, |
| 1785 | vec![ | 1861 | vec![ |
| 1786 | nostr::Filter::default() | 1862 | nostr::Filter::default() |
| 1787 | .kind(nostr::Kind::GitPatch) | 1863 | .kinds([nostr::Kind::GitPatch, KIND_PULL_REQUEST]) |
| 1788 | .custom_tags( | 1864 | .custom_tags( |
| 1789 | nostr::SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), | 1865 | nostr::SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), |
| 1790 | repo_coordinates | 1866 | repo_coordinates |
| @@ -1796,7 +1872,8 @@ pub async fn get_proposals_and_revisions_from_cache( | |||
| 1796 | ) | 1872 | ) |
| 1797 | .await? | 1873 | .await? |
| 1798 | .iter() | 1874 | .iter() |
| 1799 | .filter(|e| event_is_patch_set_root(e)) | 1875 | .filter(|e| event_is_patch_set_root(e) || e.kind.eq(&KIND_PULL_REQUEST)) |
| 1876 | .filter(|e| e.kind.eq(&Kind::GitPatch) || event_is_valid_pr_or_pr_update(e)) | ||
| 1800 | .cloned() | 1877 | .cloned() |
| 1801 | .collect::<Vec<nostr::Event>>(); | 1878 | .collect::<Vec<nostr::Event>>(); |
| 1802 | proposals.sort_by_key(|e| e.created_at); | 1879 | proposals.sort_by_key(|e| e.created_at); |
| @@ -1804,7 +1881,7 @@ pub async fn get_proposals_and_revisions_from_cache( | |||
| 1804 | Ok(proposals) | 1881 | Ok(proposals) |
| 1805 | } | 1882 | } |
| 1806 | 1883 | ||
| 1807 | pub async fn get_all_proposal_patch_events_from_cache( | 1884 | pub async fn get_all_proposal_patch_pr_pr_update_events_from_cache( |
| 1808 | git_repo_path: &Path, | 1885 | git_repo_path: &Path, |
| 1809 | repo_ref: &RepoRef, | 1886 | repo_ref: &RepoRef, |
| 1810 | proposal_id: &nostr::EventId, | 1887 | proposal_id: &nostr::EventId, |
| @@ -1813,10 +1890,21 @@ pub async fn get_all_proposal_patch_events_from_cache( | |||
| 1813 | git_repo_path, | 1890 | git_repo_path, |
| 1814 | vec![ | 1891 | vec![ |
| 1815 | nostr::Filter::default() | 1892 | nostr::Filter::default() |
| 1816 | .kind(nostr::Kind::GitPatch) | 1893 | .kinds([ |
| 1894 | nostr::Kind::GitPatch, | ||
| 1895 | KIND_PULL_REQUEST, | ||
| 1896 | KIND_PULL_REQUEST_UPDATE, | ||
| 1897 | ]) | ||
| 1817 | .event(*proposal_id), | 1898 | .event(*proposal_id), |
| 1818 | nostr::Filter::default() | 1899 | nostr::Filter::default() |
| 1819 | .kind(nostr::Kind::GitPatch) | 1900 | .kinds([ |
| 1901 | nostr::Kind::GitPatch, | ||
| 1902 | KIND_PULL_REQUEST, | ||
| 1903 | KIND_PULL_REQUEST_UPDATE, | ||
| 1904 | ]) | ||
| 1905 | .custom_tag(SingleLetterTag::uppercase(Alphabet::E), *proposal_id), | ||
| 1906 | nostr::Filter::default() | ||
| 1907 | .kinds([nostr::Kind::GitPatch, KIND_PULL_REQUEST]) | ||
| 1820 | .id(*proposal_id), | 1908 | .id(*proposal_id), |
| 1821 | ], | 1909 | ], |
| 1822 | ) | 1910 | ) |
| @@ -1836,7 +1924,11 @@ pub async fn get_all_proposal_patch_events_from_cache( | |||
| 1836 | .iter() | 1924 | .iter() |
| 1837 | .copied() | 1925 | .copied() |
| 1838 | .collect(); | 1926 | .collect(); |
| 1839 | commit_events.retain(|e| permissioned_users.contains(&e.pubkey)); | 1927 | |
| 1928 | commit_events.retain(|e| { | ||
| 1929 | permissioned_users.contains(&e.pubkey) | ||
| 1930 | && (e.kind.eq(&Kind::GitPatch) || event_is_valid_pr_or_pr_update(e)) | ||
| 1931 | }); | ||
| 1840 | 1932 | ||
| 1841 | let revision_roots: HashSet<nostr::EventId> = commit_events | 1933 | let revision_roots: HashSet<nostr::EventId> = commit_events |
| 1842 | .iter() | 1934 | .iter() |
| @@ -1849,8 +1941,20 @@ pub async fn get_all_proposal_patch_events_from_cache( | |||
| 1849 | git_repo_path, | 1941 | git_repo_path, |
| 1850 | vec![ | 1942 | vec![ |
| 1851 | nostr::Filter::default() | 1943 | nostr::Filter::default() |
| 1852 | .kind(nostr::Kind::GitPatch) | 1944 | .kinds([ |
| 1853 | .events(revision_roots) | 1945 | nostr::Kind::GitPatch, |
| 1946 | KIND_PULL_REQUEST, | ||
| 1947 | KIND_PULL_REQUEST_UPDATE, | ||
| 1948 | ]) | ||
| 1949 | .events(revision_roots.clone()) | ||
| 1950 | .authors(permissioned_users.clone()), | ||
| 1951 | nostr::Filter::default() | ||
| 1952 | .kinds([ | ||
| 1953 | nostr::Kind::GitPatch, | ||
| 1954 | KIND_PULL_REQUEST, | ||
| 1955 | KIND_PULL_REQUEST_UPDATE, | ||
| 1956 | ]) | ||
| 1957 | .custom_tags(SingleLetterTag::uppercase(Alphabet::E), revision_roots) | ||
| 1854 | .authors(permissioned_users.clone()), | 1958 | .authors(permissioned_users.clone()), |
| 1855 | ], | 1959 | ], |
| 1856 | ) | 1960 | ) |
| @@ -1891,7 +1995,7 @@ pub async fn send_events( | |||
| 1891 | silent: bool, | 1995 | silent: bool, |
| 1892 | ) -> Result<()> { | 1996 | ) -> Result<()> { |
| 1893 | let fallback = [ | 1997 | let fallback = [ |
| 1894 | client.get_fallback_relays().clone(), | 1998 | client.get_relay_default_set().clone(), |
| 1895 | if events.iter().any(|e| e.kind.eq(&Kind::GitRepoAnnouncement)) { | 1999 | if events.iter().any(|e| e.kind.eq(&Kind::GitRepoAnnouncement)) { |
| 1896 | client.get_blaster_relays().clone() | 2000 | client.get_blaster_relays().clone() |
| 1897 | } else { | 2001 | } else { |
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index d4bf2f5..b275b49 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs | |||
| @@ -10,6 +10,7 @@ use nostr_sdk::{ | |||
| 10 | Tags, | 10 | Tags, |
| 11 | hashes::{Hash, sha1::Hash as Sha1Hash}, | 11 | hashes::{Hash, sha1::Hash as Sha1Hash}, |
| 12 | }; | 12 | }; |
| 13 | use nostr_url::NostrUrlDecoded; | ||
| 13 | 14 | ||
| 14 | use crate::git_events::{get_commit_id_from_patch, tag_value}; | 15 | use crate::git_events::{get_commit_id_from_patch, tag_value}; |
| 15 | pub mod identify_ahead_behind; | 16 | pub mod identify_ahead_behind; |
| @@ -92,6 +93,10 @@ pub trait RepoActions { | |||
| 92 | fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>; | 93 | fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>; |
| 93 | fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>; | 94 | fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>; |
| 94 | fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool>; | 95 | fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool>; |
| 96 | #[allow(async_fn_in_trait)] | ||
| 97 | async fn get_first_nostr_remote_when_in_ngit_binary( | ||
| 98 | &self, | ||
| 99 | ) -> Result<Option<(String, NostrUrlDecoded)>>; | ||
| 95 | } | 100 | } |
| 96 | 101 | ||
| 97 | impl RepoActions for Repo { | 102 | impl RepoActions for Repo { |
| @@ -796,6 +801,21 @@ impl RepoActions for Repo { | |||
| 796 | Ok(true) | 801 | Ok(true) |
| 797 | } | 802 | } |
| 798 | } | 803 | } |
| 804 | |||
| 805 | async fn get_first_nostr_remote_when_in_ngit_binary( | ||
| 806 | &self, | ||
| 807 | ) -> Result<Option<(String, NostrUrlDecoded)>> { | ||
| 808 | for remote_name in self.git_repo.remotes()?.iter().flatten() { | ||
| 809 | if let Some(remote_url) = self.git_repo.find_remote(remote_name)?.url() { | ||
| 810 | if let Ok(nostr_url_decoded) = | ||
| 811 | NostrUrlDecoded::parse_and_resolve(remote_url, &Some(self)).await | ||
| 812 | { | ||
| 813 | return Ok(Some((remote_name.to_string(), nostr_url_decoded))); | ||
| 814 | } | ||
| 815 | } | ||
| 816 | } | ||
| 817 | Ok(None) | ||
| 818 | } | ||
| 799 | } | 819 | } |
| 800 | 820 | ||
| 801 | fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { | 821 | fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { |
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 69406c1..2e1f215 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs | |||
| @@ -1,7 +1,10 @@ | |||
| 1 | use std::{str::FromStr, sync::Arc}; | 1 | use std::{str::FromStr, sync::Arc}; |
| 2 | 2 | ||
| 3 | use anyhow::{Context, Result, bail}; | 3 | use anyhow::{Context, Result, bail}; |
| 4 | use nostr::nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}; | 4 | use nostr::{ |
| 5 | event::UnsignedEvent, | ||
| 6 | nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}, | ||
| 7 | }; | ||
| 5 | use nostr_sdk::{ | 8 | use nostr_sdk::{ |
| 6 | Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind, | 9 | Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind, |
| 7 | TagStandard, hashes::sha1::Hash as Sha1Hash, | 10 | TagStandard, hashes::sha1::Hash as Sha1Hash, |
| @@ -58,6 +61,9 @@ pub fn status_kinds() -> Vec<Kind> { | |||
| 58 | ] | 61 | ] |
| 59 | } | 62 | } |
| 60 | 63 | ||
| 64 | pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); | ||
| 65 | pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); | ||
| 66 | |||
| 61 | pub fn event_is_patch_set_root(event: &Event) -> bool { | 67 | pub fn event_is_patch_set_root(event: &Event) -> bool { |
| 62 | event.kind.eq(&Kind::GitPatch) | 68 | event.kind.eq(&Kind::GitPatch) |
| 63 | && event | 69 | && event |
| @@ -67,11 +73,16 @@ pub fn event_is_patch_set_root(event: &Event) -> bool { | |||
| 67 | } | 73 | } |
| 68 | 74 | ||
| 69 | pub fn event_is_revision_root(event: &Event) -> bool { | 75 | pub fn event_is_revision_root(event: &Event) -> bool { |
| 70 | event.kind.eq(&Kind::GitPatch) | 76 | (event.kind.eq(&Kind::GitPatch) |
| 71 | && event | 77 | && event |
| 72 | .tags | 78 | .tags |
| 73 | .iter() | 79 | .iter() |
| 74 | .any(|t| t.as_slice().len() > 1 && t.as_slice()[1].eq("revision-root")) | 80 | .any(|t| t.as_slice().len() > 1 && t.as_slice()[1].eq("revision-root"))) |
| 81 | || (event.kind.eq(&KIND_PULL_REQUEST) | ||
| 82 | && event | ||
| 83 | .tags | ||
| 84 | .iter() | ||
| 85 | .any(|t| t.as_slice().len() > 1 && t.as_slice()[0].eq("e"))) | ||
| 75 | } | 86 | } |
| 76 | 87 | ||
| 77 | pub fn patch_supports_commit_ids(event: &Event) -> bool { | 88 | pub fn patch_supports_commit_ids(event: &Event) -> bool { |
| @@ -82,6 +93,19 @@ pub fn patch_supports_commit_ids(event: &Event) -> bool { | |||
| 82 | .any(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq("commit-pgp-sig")) | 93 | .any(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq("commit-pgp-sig")) |
| 83 | } | 94 | } |
| 84 | 95 | ||
| 96 | pub fn event_is_valid_pr_or_pr_update(event: &Event) -> bool { | ||
| 97 | [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind) | ||
| 98 | && event.tags.iter().any(|t| { | ||
| 99 | t.as_slice().len().gt(&1) | ||
| 100 | && t.as_slice()[0].eq("c") | ||
| 101 | && git2::Oid::from_str(&t.as_slice()[1]).is_ok() | ||
| 102 | }) | ||
| 103 | && event | ||
| 104 | .tags | ||
| 105 | .iter() | ||
| 106 | .any(|t| t.as_slice().len().gt(&1) && t.as_slice()[0].eq("clone")) | ||
| 107 | } | ||
| 108 | |||
| 85 | #[allow(clippy::too_many_arguments)] | 109 | #[allow(clippy::too_many_arguments)] |
| 86 | #[allow(clippy::too_many_lines)] | 110 | #[allow(clippy::too_many_lines)] |
| 87 | pub async fn generate_patch_event( | 111 | pub async fn generate_patch_event( |
| @@ -326,6 +350,180 @@ pub fn event_tag_from_nip19_or_hex( | |||
| 326 | } | 350 | } |
| 327 | } | 351 | } |
| 328 | 352 | ||
| 353 | pub fn generate_unsigned_pr_or_update_event( | ||
| 354 | git_repo: &Repo, | ||
| 355 | repo_ref: &RepoRef, | ||
| 356 | signing_public_key: &PublicKey, | ||
| 357 | root_proposal: Option<&Event>, | ||
| 358 | commit: &Sha1Hash, | ||
| 359 | clone_url_hint: &[&str], | ||
| 360 | mentions: &[nostr::Tag], | ||
| 361 | ) -> Result<UnsignedEvent> { | ||
| 362 | let root_patch_cover_letter = if let Some(root_proposal) = root_proposal { | ||
| 363 | if root_proposal.kind.eq(&Kind::GitPatch) { | ||
| 364 | Some(event_to_cover_letter(root_proposal)?) | ||
| 365 | } else { | ||
| 366 | None | ||
| 367 | } | ||
| 368 | } else { | ||
| 369 | None | ||
| 370 | }; | ||
| 371 | |||
| 372 | let title = if let Some(cl) = &root_patch_cover_letter { | ||
| 373 | cl.title.clone() | ||
| 374 | } else { | ||
| 375 | git_repo.get_commit_message_summary(commit)? | ||
| 376 | }; | ||
| 377 | |||
| 378 | let description = if let Some(cl) = &root_patch_cover_letter { | ||
| 379 | cl.description.clone() | ||
| 380 | } else { | ||
| 381 | let mut description = git_repo.get_commit_message(commit)?.trim().to_string(); | ||
| 382 | if let Some(remaining_description) = description.strip_prefix(&title) { | ||
| 383 | description = remaining_description.trim().to_string(); | ||
| 384 | } | ||
| 385 | description | ||
| 386 | }; | ||
| 387 | |||
| 388 | let root_commit = git_repo | ||
| 389 | .get_root_commit() | ||
| 390 | .context("failed to get root commit of the repository")?; | ||
| 391 | |||
| 392 | let pr_update_specific_tags = |root_proposal: &Event| { | ||
| 393 | vec![ | ||
| 394 | Tag::custom( | ||
| 395 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), | ||
| 396 | vec![format!("git Pull Request Update")], | ||
| 397 | ), | ||
| 398 | Tag::custom( | ||
| 399 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("E")), | ||
| 400 | vec![root_proposal.id], | ||
| 401 | ), | ||
| 402 | Tag::custom( | ||
| 403 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("P")), | ||
| 404 | vec![root_proposal.pubkey], | ||
| 405 | ), | ||
| 406 | ] | ||
| 407 | }; | ||
| 408 | let pr_specific_tags = || { | ||
| 409 | [ | ||
| 410 | vec![ | ||
| 411 | Tag::from_standardized(TagStandard::Subject(title.clone())), | ||
| 412 | Tag::custom( | ||
| 413 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), | ||
| 414 | vec![format!("git Pull Request: {}", title.clone())], | ||
| 415 | ), | ||
| 416 | ], | ||
| 417 | if let Some(cl) = &root_patch_cover_letter { | ||
| 418 | vec![ | ||
| 419 | Tag::custom( | ||
| 420 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("e")), | ||
| 421 | vec![root_proposal.unwrap().id], | ||
| 422 | ), | ||
| 423 | Tag::custom( | ||
| 424 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), | ||
| 425 | vec![cl.branch_name_without_id_or_prefix.clone()], | ||
| 426 | ), | ||
| 427 | Tag::public_key(root_proposal.unwrap().pubkey), | ||
| 428 | ] | ||
| 429 | } else if let Some(branch_name_tag) = | ||
| 430 | make_branch_name_tag_from_check_out_branch(git_repo) | ||
| 431 | { | ||
| 432 | vec![branch_name_tag] | ||
| 433 | } else { | ||
| 434 | vec![] | ||
| 435 | }, | ||
| 436 | ] | ||
| 437 | .concat() | ||
| 438 | }; | ||
| 439 | |||
| 440 | Ok( | ||
| 441 | if root_proposal.is_some() && root_patch_cover_letter.is_none() { | ||
| 442 | EventBuilder::new(KIND_PULL_REQUEST_UPDATE, "") | ||
| 443 | } else { | ||
| 444 | EventBuilder::new(KIND_PULL_REQUEST, description) | ||
| 445 | } | ||
| 446 | .tags( | ||
| 447 | [ | ||
| 448 | repo_ref | ||
| 449 | .maintainers | ||
| 450 | .iter() | ||
| 451 | .map(|m| { | ||
| 452 | Tag::from_standardized(TagStandard::Coordinate { | ||
| 453 | coordinate: Coordinate { | ||
| 454 | kind: nostr::Kind::GitRepoAnnouncement, | ||
| 455 | public_key: *m, | ||
| 456 | identifier: repo_ref.identifier.to_string(), | ||
| 457 | }, | ||
| 458 | relay_url: repo_ref.relays.first().cloned(), | ||
| 459 | uppercase: false, | ||
| 460 | }) | ||
| 461 | }) | ||
| 462 | .collect::<Vec<Tag>>(), | ||
| 463 | mentions.to_vec(), | ||
| 464 | if let Some(root_proposal) = root_proposal { | ||
| 465 | if root_patch_cover_letter.is_none() { | ||
| 466 | pr_update_specific_tags(root_proposal) | ||
| 467 | } else { | ||
| 468 | pr_specific_tags() | ||
| 469 | } | ||
| 470 | } else { | ||
| 471 | pr_specific_tags() | ||
| 472 | }, | ||
| 473 | vec![ | ||
| 474 | Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), | ||
| 475 | Tag::custom( | ||
| 476 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")), | ||
| 477 | vec![format!("{commit}")], | ||
| 478 | ), | ||
| 479 | Tag::custom( | ||
| 480 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")), | ||
| 481 | clone_url_hint | ||
| 482 | .iter() | ||
| 483 | .map(|s| s.to_string()) | ||
| 484 | .collect::<Vec<String>>(), | ||
| 485 | ), | ||
| 486 | ], | ||
| 487 | repo_ref | ||
| 488 | .maintainers | ||
| 489 | .iter() | ||
| 490 | .map(|pk| Tag::public_key(*pk)) | ||
| 491 | .collect(), | ||
| 492 | ] | ||
| 493 | .concat(), | ||
| 494 | ) | ||
| 495 | .build(*signing_public_key), | ||
| 496 | ) | ||
| 497 | } | ||
| 498 | |||
| 499 | fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option<Tag> { | ||
| 500 | if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { | ||
| 501 | if !branch_name.eq("main") | ||
| 502 | && !branch_name.eq("master") | ||
| 503 | && !branch_name.eq("origin/main") | ||
| 504 | && !branch_name.eq("origin/master") | ||
| 505 | { | ||
| 506 | Some(Tag::custom( | ||
| 507 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), | ||
| 508 | vec![ | ||
| 509 | if let Some(branch_name) = branch_name.strip_prefix("pr/") { | ||
| 510 | branch_name.to_string() | ||
| 511 | } else { | ||
| 512 | branch_name | ||
| 513 | } | ||
| 514 | .chars() | ||
| 515 | .take(60) | ||
| 516 | .collect::<String>(), | ||
| 517 | ], | ||
| 518 | )) | ||
| 519 | } else { | ||
| 520 | None | ||
| 521 | } | ||
| 522 | } else { | ||
| 523 | None | ||
| 524 | } | ||
| 525 | } | ||
| 526 | |||
| 329 | #[allow(clippy::too_many_lines)] | 527 | #[allow(clippy::too_many_lines)] |
| 330 | pub async fn generate_cover_letter_and_patch_events( | 528 | pub async fn generate_cover_letter_and_patch_events( |
| 331 | cover_letter_title_description: Option<(String, String)>, | 529 | cover_letter_title_description: Option<(String, String)>, |
| @@ -388,24 +586,8 @@ pub async fn generate_cover_letter_and_patch_events( | |||
| 388 | // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding | 586 | // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding |
| 389 | // a change like this, or the removal of this tag will require the actual branch name to be tracked | 587 | // a change like this, or the removal of this tag will require the actual branch name to be tracked |
| 390 | // so pulling and pushing still work | 588 | // so pulling and pushing still work |
| 391 | if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { | 589 | if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) { |
| 392 | if !branch_name.eq("main") | 590 | vec![branch_name_tag] |
| 393 | && !branch_name.eq("master") | ||
| 394 | && !branch_name.eq("origin/main") | ||
| 395 | && !branch_name.eq("origin/master") | ||
| 396 | { | ||
| 397 | vec![ | ||
| 398 | Tag::custom( | ||
| 399 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), | ||
| 400 | vec![if let Some(branch_name) = branch_name.strip_prefix("pr/") { | ||
| 401 | branch_name.to_string() | ||
| 402 | } else { | ||
| 403 | branch_name | ||
| 404 | }.chars().take(60).collect::<String>()], | ||
| 405 | ), | ||
| 406 | ] | ||
| 407 | } | ||
| 408 | else { vec![] } | ||
| 409 | } else { | 591 | } else { |
| 410 | vec![] | 592 | vec![] |
| 411 | }, | 593 | }, |
| @@ -531,13 +713,22 @@ pub fn commit_msg_from_patch_oneliner(patch: &nostr::Event) -> Result<String> { | |||
| 531 | } | 713 | } |
| 532 | 714 | ||
| 533 | pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { | 715 | pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { |
| 534 | if !event_is_patch_set_root(event) { | 716 | if !event.kind.eq(&KIND_PULL_REQUEST) && !event_is_patch_set_root(event) { |
| 535 | bail!("event is not a patch set root event (root patch or cover letter)") | 717 | bail!("event is not a patch set root event (root patch or cover letter)") |
| 536 | } | 718 | } |
| 537 | 719 | ||
| 538 | let title = commit_msg_from_patch_oneliner(event)?; | 720 | let title = if event.kind.eq(&KIND_PULL_REQUEST) { |
| 539 | let full = commit_msg_from_patch(event)?; | 721 | tag_value(event, "subject").unwrap_or("untitled".to_owned()) |
| 540 | let description = full[title.len()..].trim().to_string(); | 722 | } else { |
| 723 | commit_msg_from_patch_oneliner(event)? | ||
| 724 | }; | ||
| 725 | let description = if event.kind.eq(&KIND_PULL_REQUEST) { | ||
| 726 | event.content.clone() | ||
| 727 | } else { | ||
| 728 | commit_msg_from_patch(event)?[title.len()..] | ||
| 729 | .trim() | ||
| 730 | .to_string() | ||
| 731 | }; | ||
| 541 | 732 | ||
| 542 | Ok(CoverLetter { | 733 | Ok(CoverLetter { |
| 543 | title: title.clone(), | 734 | title: title.clone(), |
| @@ -569,25 +760,25 @@ fn safe_branch_name_for_pr(s: &str) -> String { | |||
| 569 | .collect() | 760 | .collect() |
| 570 | } | 761 | } |
| 571 | 762 | ||
| 572 | pub fn get_most_recent_patch_with_ancestors( | 763 | pub fn get_pr_tip_event_or_most_recent_patch_with_ancestors( |
| 573 | mut patches: Vec<nostr::Event>, | 764 | mut proposal_events: Vec<nostr::Event>, |
| 574 | ) -> Result<Vec<nostr::Event>> { | 765 | ) -> Result<Vec<nostr::Event>> { |
| 575 | patches.sort_by_key(|e| e.created_at); | 766 | proposal_events.sort_by_key(|e| e.created_at); |
| 576 | 767 | ||
| 577 | let youngest_patch = patches.last().context("no patches found")?; | 768 | let youngest = proposal_events.last().context("no proposal events found")?; |
| 578 | 769 | ||
| 579 | let patches_with_youngest_created_at: Vec<&nostr::Event> = patches | 770 | let events_with_youngest_created_at: Vec<&nostr::Event> = proposal_events |
| 580 | .iter() | 771 | .iter() |
| 581 | .filter(|p| p.created_at.eq(&youngest_patch.created_at)) | 772 | .filter(|p| p.created_at.eq(&youngest.created_at)) |
| 582 | .collect(); | 773 | .collect(); |
| 583 | 774 | ||
| 584 | let mut res = vec![]; | 775 | let mut res = vec![]; |
| 585 | 776 | ||
| 586 | let mut event_id_to_search = patches_with_youngest_created_at | 777 | let mut event_id_to_search = events_with_youngest_created_at |
| 587 | .clone() | 778 | .clone() |
| 588 | .iter() | 779 | .iter() |
| 589 | .find(|p| { | 780 | .find(|p| { |
| 590 | !patches_with_youngest_created_at.iter().any(|p2| { | 781 | !events_with_youngest_created_at.iter().any(|p2| { |
| 591 | if let Ok(reply_to) = get_event_parent_id(p2) { | 782 | if let Ok(reply_to) = get_event_parent_id(p2) { |
| 592 | reply_to.eq(&p.id.to_string()) | 783 | reply_to.eq(&p.id.to_string()) |
| 593 | } else { | 784 | } else { |
| @@ -595,16 +786,18 @@ pub fn get_most_recent_patch_with_ancestors( | |||
| 595 | } | 786 | } |
| 596 | }) | 787 | }) |
| 597 | }) | 788 | }) |
| 598 | .context("failed to find patches_with_youngest_created_at")? | 789 | .context("failed to find events_with_youngest_created_at")? |
| 599 | .id | 790 | .id |
| 600 | .to_string(); | 791 | .to_string(); |
| 601 | 792 | ||
| 602 | while let Some(event) = patches | 793 | while let Some(event) = proposal_events |
| 603 | .iter() | 794 | .iter() |
| 604 | .find(|e| e.id.to_string().eq(&event_id_to_search)) | 795 | .find(|e| e.id.to_string().eq(&event_id_to_search)) |
| 605 | { | 796 | { |
| 606 | res.push(event.clone()); | 797 | res.push(event.clone()); |
| 607 | if event_is_patch_set_root(event) { | 798 | if [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind) |
| 799 | || event_is_patch_set_root(event) | ||
| 800 | { | ||
| 608 | break; | 801 | break; |
| 609 | } | 802 | } |
| 610 | event_id_to_search = get_event_parent_id(event).unwrap_or_default(); | 803 | event_id_to_search = get_event_parent_id(event).unwrap_or_default(); |
| @@ -642,7 +835,61 @@ pub fn is_event_proposal_root_for_branch( | |||
| 642 | || cl | 835 | || cl |
| 643 | .get_branch_name_with_pr_prefix_and_shorthand_id() | 836 | .get_branch_name_with_pr_prefix_and_shorthand_id() |
| 644 | .is_ok_and(|s| s.eq(&branch_name)) | 837 | .is_ok_and(|s| s.eq(&branch_name)) |
| 645 | }) && !event_is_revision_root(e)) | 838 | }) && ( |
| 839 | // If we wanted to treat to list Pull Requests that revise a Patch we would do this: | ||
| 840 | // Note: whilst this the the case elsewhere event_is_revision_root is used, there is more to | ||
| 841 | // think about here? | ||
| 842 | // e.kind.eq(&KIND_PULL_REQUEST) || | ||
| 843 | !event_is_revision_root(e) | ||
| 844 | )) | ||
| 845 | } | ||
| 846 | |||
| 847 | pub fn get_status( | ||
| 848 | proposal: &Event, | ||
| 849 | repo_ref: &RepoRef, | ||
| 850 | all_status_in_repo: &[Event], | ||
| 851 | all_pr_roots_in_repo: &[Event], | ||
| 852 | ) -> Kind { | ||
| 853 | let get_direct_status = |proposal: &Event| { | ||
| 854 | if let Some(e) = all_status_in_repo | ||
| 855 | .iter() | ||
| 856 | .filter(|e| { | ||
| 857 | status_kinds().contains(&e.kind) | ||
| 858 | && e.tags.iter().any(|t| { | ||
| 859 | t.as_slice().len() > 1 && t.as_slice()[1].eq(&proposal.id.to_string()) | ||
| 860 | }) | ||
| 861 | && (proposal.pubkey.eq(&e.pubkey) || repo_ref.maintainers.contains(&e.pubkey)) | ||
| 862 | }) | ||
| 863 | .collect::<Vec<&nostr::Event>>() | ||
| 864 | .first() | ||
| 865 | { | ||
| 866 | e.kind | ||
| 867 | } else { | ||
| 868 | Kind::GitStatusOpen | ||
| 869 | } | ||
| 870 | }; | ||
| 871 | let is_proposal_pr_revision_of_patch = |proposal: &Event, patch: &Event| { | ||
| 872 | proposal.kind.eq(&KIND_PULL_REQUEST) | ||
| 873 | && proposal.tags.clone().into_iter().any(|t| { | ||
| 874 | t.as_slice().len() > 1 | ||
| 875 | && t.as_slice()[0].eq("e") | ||
| 876 | && t.as_slice()[1].eq(&patch.id.to_string()) | ||
| 877 | }) | ||
| 878 | }; | ||
| 879 | |||
| 880 | let direct_status = get_direct_status(proposal); | ||
| 881 | if direct_status.eq(&Kind::GitStatusClosed) && proposal.kind.eq(&Kind::GitPatch) { | ||
| 882 | if let Some(pr_revision) = all_pr_roots_in_repo | ||
| 883 | .iter() | ||
| 884 | .find(|p| is_proposal_pr_revision_of_patch(p, proposal)) | ||
| 885 | { | ||
| 886 | get_direct_status(pr_revision) | ||
| 887 | } else { | ||
| 888 | direct_status | ||
| 889 | } | ||
| 890 | } else { | ||
| 891 | direct_status | ||
| 892 | } | ||
| 646 | } | 893 | } |
| 647 | 894 | ||
| 648 | #[cfg(test)] | 895 | #[cfg(test)] |
diff --git a/src/lib/login/fresh.rs b/src/lib/login/fresh.rs index a169177..358045a 100644 --- a/src/lib/login/fresh.rs +++ b/src/lib/login/fresh.rs | |||
| @@ -728,7 +728,7 @@ async fn signup( | |||
| 728 | EventBuilder::metadata(&Metadata::new().name(name)).sign_with_keys(&keys)?; | 728 | EventBuilder::metadata(&Metadata::new().name(name)).sign_with_keys(&keys)?; |
| 729 | let relay_list = EventBuilder::relay_list( | 729 | let relay_list = EventBuilder::relay_list( |
| 730 | client | 730 | client |
| 731 | .get_fallback_relays() | 731 | .get_relay_default_set() |
| 732 | .iter() | 732 | .iter() |
| 733 | .map(|s| (RelayUrl::parse(s).unwrap(), None)), | 733 | .map(|s| (RelayUrl::parse(s).unwrap(), None)), |
| 734 | ) | 734 | ) |
| @@ -738,7 +738,7 @@ async fn signup( | |||
| 738 | client, | 738 | client, |
| 739 | None, | 739 | None, |
| 740 | vec![profile, relay_list], | 740 | vec![profile, relay_list], |
| 741 | client.get_fallback_relays().clone(), | 741 | client.get_relay_default_set().clone(), |
| 742 | vec![], | 742 | vec![], |
| 743 | true, | 743 | true, |
| 744 | false, | 744 | false, |
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs index 0236e34..bca4a3b 100644 --- a/src/lib/repo_ref.rs +++ b/src/lib/repo_ref.rs | |||
| @@ -309,7 +309,7 @@ impl RepoRef { | |||
| 309 | } | 309 | } |
| 310 | 310 | ||
| 311 | pub fn grasp_servers(&self) -> Vec<String> { | 311 | pub fn grasp_servers(&self) -> Vec<String> { |
| 312 | detect_existing_grasp_servers(Some(self), &[], &[], &[], &self.identifier) | 312 | detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier) |
| 313 | } | 313 | } |
| 314 | } | 314 | } |
| 315 | 315 | ||
| @@ -593,7 +593,6 @@ pub fn detect_existing_grasp_servers( | |||
| 593 | repo_ref: Option<&RepoRef>, | 593 | repo_ref: Option<&RepoRef>, |
| 594 | args_relays: &[String], | 594 | args_relays: &[String], |
| 595 | args_clone_url: &[String], | 595 | args_clone_url: &[String], |
| 596 | args_blossoms: &[String], | ||
| 597 | identifier: &str, | 596 | identifier: &str, |
| 598 | ) -> Vec<String> { | 597 | ) -> Vec<String> { |
| 599 | // Collect clone URLs from arguments or repo_ref | 598 | // Collect clone URLs from arguments or repo_ref |
| @@ -617,18 +616,6 @@ pub fn detect_existing_grasp_servers( | |||
| 617 | Vec::new() | 616 | Vec::new() |
| 618 | }; | 617 | }; |
| 619 | 618 | ||
| 620 | // Collect blossom server URLs from arguments or repo_ref | ||
| 621 | let blossoms: Vec<Url> = if !args_blossoms.is_empty() { | ||
| 622 | args_blossoms | ||
| 623 | .iter() | ||
| 624 | .filter_map(|r| Url::parse(r).ok()) | ||
| 625 | .collect() | ||
| 626 | } else if let Some(repo) = repo_ref { | ||
| 627 | repo.blossoms.clone() | ||
| 628 | } else { | ||
| 629 | Vec::new() | ||
| 630 | }; | ||
| 631 | |||
| 632 | let mut existing_grasp_servers = Vec::new(); | 619 | let mut existing_grasp_servers = Vec::new(); |
| 633 | for url in &clone_urls { | 620 | for url in &clone_urls { |
| 634 | let Ok(formatted_as_grasp_server_url) = normalize_grasp_server_url(url) else { | 621 | let Ok(formatted_as_grasp_server_url) = normalize_grasp_server_url(url) else { |
| @@ -655,14 +642,6 @@ pub fn detect_existing_grasp_servers( | |||
| 655 | continue; | 642 | continue; |
| 656 | } | 643 | } |
| 657 | 644 | ||
| 658 | let matches_blossoms = blossoms.iter().any(|r| { | ||
| 659 | normalize_grasp_server_url(r.as_str()) | ||
| 660 | .is_ok_and(|r| r.eq(&formatted_as_grasp_server_url)) | ||
| 661 | }); | ||
| 662 | if !matches_blossoms { | ||
| 663 | continue; | ||
| 664 | } | ||
| 665 | |||
| 666 | existing_grasp_servers.push(formatted_as_grasp_server_url); | 645 | existing_grasp_servers.push(formatted_as_grasp_server_url); |
| 667 | } | 646 | } |
| 668 | existing_grasp_servers | 647 | existing_grasp_servers |