upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/cli_interactor.rs105
-rw-r--r--src/lib/client.rs54
-rw-r--r--src/lib/git/mod.rs14
-rw-r--r--src/lib/git_events.rs11
-rw-r--r--src/lib/list.rs4
-rw-r--r--src/lib/login/user.rs77
-rw-r--r--src/lib/push.rs270
-rw-r--r--src/lib/repo_ref.rs87
-rw-r--r--src/lib/utils.rs36
9 files changed, 597 insertions, 61 deletions
diff --git a/src/lib/cli_interactor.rs b/src/lib/cli_interactor.rs
index 8fca81d..e944bf9 100644
--- a/src/lib/cli_interactor.rs
+++ b/src/lib/cli_interactor.rs
@@ -1,5 +1,8 @@
1use anyhow::{Context, Result}; 1use anyhow::{Context, Result};
2use dialoguer::{Confirm, Input, Password, theme::ColorfulTheme}; 2use dialoguer::{
3 Confirm, Input, Password,
4 theme::{ColorfulTheme, Theme},
5};
3use indicatif::TermLike; 6use indicatif::TermLike;
4#[cfg(test)] 7#[cfg(test)]
5use mockall::*; 8use mockall::*;
@@ -236,6 +239,106 @@ impl PromptMultiChoiceParms {
236 } 239 }
237} 240}
238 241
242pub fn multi_select_with_custom_value<F>(
243 prompt: &str,
244 custom_choice_prompt: &str,
245 mut choices: Vec<String>,
246 mut defaults: Vec<bool>,
247 validate_choice: F,
248) -> Result<Vec<String>>
249where
250 F: Fn(&str) -> Result<String>,
251{
252 let mut selected_choices = vec![];
253
254 // Loop to allow users to add more choices
255 loop {
256 // Add 'add another' option at the end of the choices
257 let mut current_choices = choices.clone();
258 current_choices.push(if current_choices.is_empty() {
259 "add".to_string()
260 } else {
261 "add another".to_string()
262 });
263
264 // Create default selections based on the provided defaults
265 let mut current_defaults = defaults.clone();
266 current_defaults.push(current_choices.len() == 1); // 'add another' should not be selected by default
267
268 // Prompt for selections
269 let selected_indices: Vec<usize> = Interactor::default().multi_choice(
270 PromptMultiChoiceParms::default()
271 .with_prompt(prompt)
272 .dont_report()
273 .with_choices(current_choices.clone())
274 .with_defaults(current_defaults),
275 )?;
276
277 // Collect selected choices
278 selected_choices.clear(); // Clear previous selections to update
279 for &index in &selected_indices {
280 if index < choices.len() {
281 // Exclude 'add another' option
282 selected_choices.push(choices[index].clone());
283 }
284 }
285
286 // Check if 'add another' was selected
287 if selected_indices.contains(&(choices.len())) {
288 // Last index is 'add another'
289 let mut new_choice: String;
290 loop {
291 new_choice = Interactor::default().input(
292 PromptInputParms::default()
293 .with_prompt(custom_choice_prompt)
294 .dont_report()
295 .optional(),
296 )?;
297
298 if new_choice.is_empty() {
299 break;
300 }
301 // Validate the new choice
302 match validate_choice(&new_choice) {
303 Ok(valid_choice) => {
304 new_choice = valid_choice; // Use the fixed version of the input
305 break; // Valid choice, exit the loop
306 }
307 Err(err) => {
308 // Inform the user about the validation error
309 println!("Error: {err}");
310 }
311 }
312 }
313
314 // Add the new choice to the choices vector
315 if !new_choice.is_empty() {
316 choices.push(new_choice.clone()); // Add new choice to the end of the list
317 selected_choices.push(new_choice); // Automatically select the new choice
318 defaults.push(true); // Set the new choice as selected by default
319 }
320 } else {
321 // Exit the loop if 'add another' was not selected
322 break;
323 }
324 }
325
326 Ok(selected_choices)
327}
328
329pub fn show_multi_input_prompt_success(label: &str, values: &[String]) {
330 let values_str: Vec<&str> = values.iter().map(std::string::String::as_str).collect();
331 eprintln!("{}", {
332 let mut s = String::new();
333 let _ = ColorfulTheme::default().format_multi_select_prompt_selection(
334 &mut s,
335 label,
336 &values_str,
337 );
338 s
339 });
340}
341
239#[derive(Debug, Default)] 342#[derive(Debug, Default)]
240pub struct Printer { 343pub struct Printer {
241 printed_lines: Vec<String>, 344 printed_lines: Vec<String>,
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 3fcfba4..0984b19 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -53,7 +53,7 @@ use crate::{
53 get_dirs, 53 get_dirs,
54 git::{Repo, RepoActions, get_git_config_item}, 54 git::{Repo, RepoActions, get_git_config_item},
55 git_events::{ 55 git_events::{
56 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_cover_letter, 56 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, KIND_USER_GRASP_LIST, event_is_cover_letter,
57 event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update, 57 event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update,
58 status_kinds, 58 status_kinds,
59 }, 59 },
@@ -241,7 +241,7 @@ impl Connect for Client {
241 if let Some(git_repo_path) = git_repo_path { 241 if let Some(git_repo_path) = git_repo_path {
242 save_event_in_local_cache(git_repo_path, &event).await?; 242 save_event_in_local_cache(git_repo_path, &event).await?;
243 } 243 }
244 if event.kind.eq(&Kind::GitRepoAnnouncement) { 244 if [Kind::GitRepoAnnouncement, KIND_USER_GRASP_LIST].contains(&event.kind) {
245 save_event_in_global_cache(git_repo_path, &event).await?; 245 save_event_in_global_cache(git_repo_path, &event).await?;
246 } 246 }
247 Ok(event.id) 247 Ok(event.id)
@@ -1308,17 +1308,21 @@ async fn create_relays_request(
1308 user_profiles.insert(current_user); 1308 user_profiles.insert(current_user);
1309 } 1309 }
1310 } 1310 }
1311 let mut map: HashMap<PublicKey, (Timestamp, Timestamp)> = HashMap::new(); 1311 let mut map: HashMap<PublicKey, (Timestamp, Timestamp, Timestamp)> = HashMap::new();
1312 for public_key in &user_profiles { 1312 for public_key in &user_profiles {
1313 if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await { 1313 if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await {
1314 map.insert( 1314 map.insert(
1315 public_key.to_owned(), 1315 public_key.to_owned(),
1316 (user_ref.metadata.created_at, user_ref.relays.created_at), 1316 (
1317 user_ref.metadata.created_at,
1318 user_ref.relays.created_at,
1319 user_ref.grasp_list.created_at,
1320 ),
1317 ); 1321 );
1318 } else { 1322 } else {
1319 map.insert( 1323 map.insert(
1320 public_key.to_owned(), 1324 public_key.to_owned(),
1321 (Timestamp::from(0), Timestamp::from(0)), 1325 (Timestamp::from(0), Timestamp::from(0), Timestamp::from(0)),
1322 ); 1326 );
1323 } 1327 }
1324 } 1328 }
@@ -1545,16 +1549,22 @@ async fn process_fetched_events(
1545 { 1549 {
1546 fresh_profiles.insert(event.pubkey); 1550 fresh_profiles.insert(event.pubkey);
1547 } 1551 }
1548 } else if [Kind::RelayList, Kind::Metadata].contains(&event.kind) { 1552 } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind)
1553 {
1549 if request.missing_contributor_profiles.contains(&event.pubkey) { 1554 if request.missing_contributor_profiles.contains(&event.pubkey) {
1550 report.contributor_profiles.insert(event.pubkey); 1555 report.contributor_profiles.insert(event.pubkey);
1551 } else if let Some((_, (metadata_timestamp, relay_list_timestamp))) = request 1556 } else if let Some((
1557 _,
1558 (metadata_timestamp, relay_list_timestamp, grasp_list_timestamp),
1559 )) = request
1552 .profiles_to_fetch_from_user_relays 1560 .profiles_to_fetch_from_user_relays
1553 .get_key_value(&event.pubkey) 1561 .get_key_value(&event.pubkey)
1554 { 1562 {
1555 if (Kind::Metadata.eq(&event.kind) && event.created_at.gt(metadata_timestamp)) 1563 if (Kind::Metadata.eq(&event.kind) && event.created_at.gt(metadata_timestamp))
1556 || (Kind::RelayList.eq(&event.kind) 1564 || (Kind::RelayList.eq(&event.kind)
1557 && event.created_at.gt(relay_list_timestamp)) 1565 && event.created_at.gt(relay_list_timestamp))
1566 || (KIND_USER_GRASP_LIST.eq(&event.kind)
1567 && event.created_at.gt(grasp_list_timestamp))
1558 { 1568 {
1559 report.profile_updates.insert(event.pubkey); 1569 report.profile_updates.insert(event.pubkey);
1560 } 1570 }
@@ -1716,35 +1726,21 @@ pub fn get_filter_repo_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> no
1716 .map(|c| c.identifier.clone()) 1726 .map(|c| c.identifier.clone())
1717 .collect::<Vec<String>>(), 1727 .collect::<Vec<String>>(),
1718 ) 1728 )
1719 .authors(
1720 repo_coordinates
1721 .iter()
1722 .map(|c| c.public_key)
1723 .collect::<Vec<PublicKey>>(),
1724 )
1725} 1729}
1726 1730
1727pub static STATE_KIND: nostr::Kind = Kind::Custom(30618); 1731pub static STATE_KIND: nostr::Kind = Kind::Custom(30618);
1728pub fn get_filter_state_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> nostr::Filter { 1732pub fn get_filter_state_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> nostr::Filter {
1729 nostr::Filter::default() 1733 nostr::Filter::default().kind(STATE_KIND).identifiers(
1730 .kind(STATE_KIND) 1734 repo_coordinates
1731 .identifiers( 1735 .iter()
1732 repo_coordinates 1736 .map(|c| c.identifier.clone())
1733 .iter() 1737 .collect::<Vec<String>>(),
1734 .map(|c| c.identifier.clone()) 1738 )
1735 .collect::<Vec<String>>(),
1736 )
1737 .authors(
1738 repo_coordinates
1739 .iter()
1740 .map(|c| c.public_key)
1741 .collect::<Vec<PublicKey>>(),
1742 )
1743} 1739}
1744 1740
1745pub fn get_filter_contributor_profiles(contributors: HashSet<PublicKey>) -> nostr::Filter { 1741pub fn get_filter_contributor_profiles(contributors: HashSet<PublicKey>) -> nostr::Filter {
1746 nostr::Filter::default() 1742 nostr::Filter::default()
1747 .kinds(vec![Kind::Metadata, Kind::RelayList]) 1743 .kinds(vec![Kind::Metadata, Kind::RelayList, KIND_USER_GRASP_LIST])
1748 .authors(contributors) 1744 .authors(contributors)
1749} 1745}
1750 1746
@@ -1848,7 +1844,7 @@ pub struct FetchRequest {
1848 contributors: HashSet<PublicKey>, 1844 contributors: HashSet<PublicKey>,
1849 missing_contributor_profiles: HashSet<PublicKey>, 1845 missing_contributor_profiles: HashSet<PublicKey>,
1850 existing_events: HashSet<EventId>, 1846 existing_events: HashSet<EventId>,
1851 profiles_to_fetch_from_user_relays: HashMap<PublicKey, (Timestamp, Timestamp)>, 1847 profiles_to_fetch_from_user_relays: HashMap<PublicKey, (Timestamp, Timestamp, Timestamp)>,
1852 user_relays_for_profiles: HashSet<RelayUrl>, 1848 user_relays_for_profiles: HashSet<RelayUrl>,
1853} 1849}
1854 1850
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs
index b275b49..3d5297f 100644
--- a/src/lib/git/mod.rs
+++ b/src/lib/git/mod.rs
@@ -75,6 +75,7 @@ pub trait RepoActions {
75 commit: &Sha1Hash, 75 commit: &Sha1Hash,
76 series_count: &Option<(u64, u64)>, 76 series_count: &Option<(u64, u64)>,
77 ) -> Result<String>; 77 ) -> Result<String>;
78 fn are_commits_too_big_for_patches(&self, commits: &[Sha1Hash]) -> bool;
78 fn extract_commit_pgp_signature(&self, commit: &Sha1Hash) -> Result<String>; 79 fn extract_commit_pgp_signature(&self, commit: &Sha1Hash) -> Result<String>;
79 fn checkout(&self, ref_name: &str) -> Result<Sha1Hash>; 80 fn checkout(&self, ref_name: &str) -> Result<Sha1Hash>;
80 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()>; 81 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()>;
@@ -380,6 +381,19 @@ impl RepoActions for Repo {
380 .to_owned()) 381 .to_owned())
381 } 382 }
382 383
384 fn are_commits_too_big_for_patches(&self, commits: &[Sha1Hash]) -> bool {
385 commits.iter().any(|commit| {
386 if let Ok(patch) = self.make_patch_from_commit(commit, &None) {
387 patch.len()
388 > ((65 // max recomended patch event size specified in nip34 in kb
389 // allownace for nostr event wrapper (id, pubkey, tags, sig)
390 - 1) * 1024)
391 } else {
392 true
393 }
394 })
395 }
396
383 fn extract_commit_pgp_signature(&self, commit: &Sha1Hash) -> Result<String> { 397 fn extract_commit_pgp_signature(&self, commit: &Sha1Hash) -> Result<String> {
384 let oid = Oid::from_bytes(commit.as_byte_array()).context(format!( 398 let oid = Oid::from_bytes(commit.as_byte_array()).context(format!(
385 "failed to convert commit_id format for {}", 399 "failed to convert commit_id format for {}",
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs
index 8ba5419..5ea630a 100644
--- a/src/lib/git_events.rs
+++ b/src/lib/git_events.rs
@@ -63,6 +63,7 @@ pub fn status_kinds() -> Vec<Kind> {
63 63
64pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); 64pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618);
65pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); 65pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619);
66pub const KIND_USER_GRASP_LIST: Kind = Kind::Custom(10317);
66 67
67pub fn event_is_patch_set_root(event: &Event) -> bool { 68pub fn event_is_patch_set_root(event: &Event) -> bool {
68 event.kind.eq(&Kind::GitPatch) 69 event.kind.eq(&Kind::GitPatch)
@@ -376,11 +377,13 @@ pub fn event_tag_from_nip19_or_hex(
376 } 377 }
377} 378}
378 379
380#[allow(clippy::too_many_arguments)]
379pub fn generate_unsigned_pr_or_update_event( 381pub fn generate_unsigned_pr_or_update_event(
380 git_repo: &Repo, 382 git_repo: &Repo,
381 repo_ref: &RepoRef, 383 repo_ref: &RepoRef,
382 signing_public_key: &PublicKey, 384 signing_public_key: &PublicKey,
383 root_proposal: Option<&Event>, 385 root_proposal: Option<&Event>,
386 title_description_overide: &Option<(String, String)>,
384 commit: &Sha1Hash, 387 commit: &Sha1Hash,
385 clone_url_hint: &[&str], 388 clone_url_hint: &[&str],
386 mentions: &[nostr::Tag], 389 mentions: &[nostr::Tag],
@@ -395,13 +398,17 @@ pub fn generate_unsigned_pr_or_update_event(
395 None 398 None
396 }; 399 };
397 400
398 let title = if let Some(cl) = &root_patch_cover_letter { 401 let title = if let Some((title, _)) = &title_description_overide {
402 title.clone()
403 } else if let Some(cl) = &root_patch_cover_letter {
399 cl.title.clone() 404 cl.title.clone()
400 } else { 405 } else {
401 git_repo.get_commit_message_summary(commit)? 406 git_repo.get_commit_message_summary(commit)?
402 }; 407 };
403 408
404 let description = if let Some(cl) = &root_patch_cover_letter { 409 let description = if let Some((_, description)) = &title_description_overide {
410 description.clone()
411 } else if let Some(cl) = &root_patch_cover_letter {
405 cl.description.clone() 412 cl.description.clone()
406 } else { 413 } else {
407 let mut description = git_repo.get_commit_message(commit)?.trim().to_string(); 414 let mut description = git_repo.get_commit_message(commit)?.trim().to_string();
diff --git a/src/lib/list.rs b/src/lib/list.rs
index b940546..b867858 100644
--- a/src/lib/list.rs
+++ b/src/lib/list.rs
@@ -9,7 +9,7 @@ use crate::{
9 Repo, RepoActions, 9 Repo, RepoActions,
10 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 10 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
11 }, 11 },
12 repo_ref::is_grasp_server, 12 repo_ref::is_grasp_server_in_list,
13 utils::{Direction, get_read_protocols_to_try, join_with_and, set_protocol_preference}, 13 utils::{Direction, get_read_protocols_to_try, join_with_and, set_protocol_preference},
14}; 14};
15 15
@@ -23,7 +23,7 @@ pub fn list_from_remotes(
23 let mut remote_states = HashMap::new(); 23 let mut remote_states = HashMap::new();
24 let mut errors = HashMap::new(); 24 let mut errors = HashMap::new();
25 for url in git_servers { 25 for url in git_servers {
26 let is_grasp_server = is_grasp_server(url, grasp_servers); 26 let is_grasp_server = is_grasp_server_in_list(url, grasp_servers);
27 match list_from_remote(term, git_repo, url, decoded_nostr_url, is_grasp_server) { 27 match list_from_remote(term, git_repo, url, decoded_nostr_url, is_grasp_server) {
28 Err(error) => { 28 Err(error) => {
29 errors.insert(url, error); 29 errors.insert(url, error);
diff --git a/src/lib/login/user.rs b/src/lib/login/user.rs
index 071cb25..0b702ef 100644
--- a/src/lib/login/user.rs
+++ b/src/lib/login/user.rs
@@ -1,7 +1,7 @@
1use std::{collections::HashSet, path::Path}; 1use std::{collections::HashSet, path::Path, sync::Arc};
2 2
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use nostr::PublicKey; 4use nostr::{PublicKey, Url, event::Tag, signer::NostrSigner};
5use nostr_sdk::{Alphabet, JsonUtil, Kind, SingleLetterTag, Timestamp, ToBech32}; 5use nostr_sdk::{Alphabet, JsonUtil, Kind, SingleLetterTag, Timestamp, ToBech32};
6use serde::{self, Deserialize, Serialize}; 6use serde::{self, Deserialize, Serialize};
7 7
@@ -9,13 +9,17 @@ use serde::{self, Deserialize, Serialize};
9use crate::client::Client; 9use crate::client::Client;
10#[cfg(test)] 10#[cfg(test)]
11use crate::client::MockConnect; 11use crate::client::MockConnect;
12use crate::client::{Connect, get_event_from_global_cache}; 12use crate::{
13 client::{Connect, get_event_from_global_cache, sign_event},
14 git_events::KIND_USER_GRASP_LIST,
15};
13 16
14#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 17#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
15pub struct UserRef { 18pub struct UserRef {
16 pub public_key: PublicKey, 19 pub public_key: PublicKey,
17 pub metadata: UserMetadata, 20 pub metadata: UserMetadata,
18 pub relays: UserRelays, 21 pub relays: UserRelays,
22 pub grasp_list: UserGraspList,
19} 23}
20 24
21#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 25#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
@@ -49,6 +53,35 @@ impl UserRelays {
49} 53}
50 54
51#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 55#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
56pub struct UserGraspList {
57 pub urls: Vec<Url>,
58 pub created_at: Timestamp,
59}
60
61impl UserGraspList {
62 pub async fn to_event(&mut self, signer: &Arc<dyn NostrSigner>) -> Result<nostr::Event> {
63 let event = sign_event(
64 nostr_sdk::EventBuilder::new(KIND_USER_GRASP_LIST, "").tags(
65 self.urls
66 .iter()
67 .map(|url| {
68 Tag::custom(
69 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("g")),
70 vec![url.to_string()],
71 )
72 })
73 .collect::<Vec<_>>(),
74 ),
75 signer,
76 "user grasp list".to_string(),
77 )
78 .await?;
79 self.created_at = event.created_at;
80 Ok(event)
81 }
82}
83
84#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
52pub struct UserRelayRef { 85pub struct UserRelayRef {
53 pub url: String, 86 pub url: String,
54 pub read: bool, 87 pub read: bool,
@@ -84,6 +117,7 @@ pub async fn get_user_details(
84 public_key: public_key.to_owned(), 117 public_key: public_key.to_owned(),
85 metadata: extract_user_metadata(public_key, &[])?, 118 metadata: extract_user_metadata(public_key, &[])?,
86 relays: extract_user_relays(public_key, &[]), 119 relays: extract_user_relays(public_key, &[]),
120 grasp_list: extract_user_grasp_list(public_key, &[]),
87 }; 121 };
88 if cache_only { 122 if cache_only {
89 Ok(empty) 123 Ok(empty)
@@ -117,6 +151,9 @@ pub async fn get_user_ref_from_cache(
117 nostr::Filter::default() 151 nostr::Filter::default()
118 .author(*public_key) 152 .author(*public_key)
119 .kind(Kind::RelayList), 153 .kind(Kind::RelayList),
154 nostr::Filter::default()
155 .author(*public_key)
156 .kind(KIND_USER_GRASP_LIST),
120 ]; 157 ];
121 158
122 let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?; 159 let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?;
@@ -128,6 +165,7 @@ pub async fn get_user_ref_from_cache(
128 public_key: public_key.to_owned(), 165 public_key: public_key.to_owned(),
129 metadata: extract_user_metadata(public_key, &events)?, 166 metadata: extract_user_metadata(public_key, &events)?,
130 relays: extract_user_relays(public_key, &events), 167 relays: extract_user_relays(public_key, &events),
168 grasp_list: extract_user_grasp_list(public_key, &events),
131 }) 169 })
132} 170}
133 171
@@ -215,3 +253,36 @@ pub fn extract_user_relays(public_key: &nostr::PublicKey, events: &[nostr::Event
215 }, 253 },
216 } 254 }
217} 255}
256
257pub fn extract_user_grasp_list(
258 public_key: &nostr::PublicKey,
259 events: &[nostr::Event],
260) -> UserGraspList {
261 let event = events
262 .iter()
263 .filter(|e| e.kind.eq(&KIND_USER_GRASP_LIST) && e.pubkey.eq(public_key))
264 .max_by_key(|e| e.created_at);
265
266 UserGraspList {
267 urls: if let Some(event) = event {
268 event
269 .tags
270 .iter()
271 .filter_map(|t| {
272 if t.as_slice().len() > 1 && t.as_slice()[0] == "g" {
273 Url::parse(&t.as_slice()[1]).ok()
274 } else {
275 None
276 }
277 })
278 .collect()
279 } else {
280 vec![]
281 },
282 created_at: if let Some(event) = event {
283 event.created_at
284 } else {
285 Timestamp::from(0)
286 },
287 }
288}
diff --git a/src/lib/push.rs b/src/lib/push.rs
index 0d0ec93..8cb0212 100644
--- a/src/lib/push.rs
+++ b/src/lib/push.rs
@@ -1,25 +1,38 @@
1use std::{ 1use std::{
2 collections::{HashMap, HashSet},
2 sync::{Arc, Mutex}, 3 sync::{Arc, Mutex},
3 time::Instant, 4 time::Instant,
4}; 5};
5 6
6use anyhow::{Result, anyhow}; 7use anyhow::{Context, Result, anyhow};
7use auth_git2::GitAuthenticator; 8use auth_git2::GitAuthenticator;
8use console::Term; 9use console::Term;
10use nostr::{
11 event::{Event, EventBuilder, Kind, Tag, TagStandard, UnsignedEvent},
12 hashes::sha1::Hash as Sha1Hash,
13 key::PublicKey,
14 nips::nip10::Marker,
15 signer::NostrSigner,
16};
9 17
10use crate::{ 18use crate::{
11 cli_interactor::count_lines_per_msg_vec, 19 cli_interactor::count_lines_per_msg_vec,
20 client::{sign_draft_event, sign_event},
12 git::{ 21 git::{
13 Repo, 22 Repo, RepoActions,
14 nostr_url::{CloneUrl, NostrUrlDecoded}, 23 nostr_url::{CloneUrl, NostrUrlDecoded},
15 oid_to_shorthand_string, 24 oid_to_shorthand_string,
16 }, 25 },
26 git_events::generate_unsigned_pr_or_update_event,
27 login::user::UserRef,
28 repo_ref::{RepoRef, is_grasp_server_clone_url, normalize_grasp_server_url},
17 utils::{ 29 utils::{
18 Direction, get_short_git_server_name, get_write_protocols_to_try, join_with_and, 30 Direction, get_short_git_server_name, get_write_protocols_to_try, join_with_and,
19 set_protocol_preference, 31 set_protocol_preference,
20 }, 32 },
21}; 33};
22 34
35// returns a HashMap of refs responded to and any related cancellation reasons
23pub fn push_to_remote( 36pub fn push_to_remote(
24 git_repo: &Repo, 37 git_repo: &Repo,
25 git_server_url: &str, 38 git_server_url: &str,
@@ -27,35 +40,65 @@ pub fn push_to_remote(
27 remote_refspecs: &[String], 40 remote_refspecs: &[String],
28 term: &Term, 41 term: &Term,
29 is_grasp_server: bool, 42 is_grasp_server: bool,
30) -> Result<()> { 43) -> Result<HashMap<String, Option<String>>> {
31 let server_url = git_server_url.parse::<CloneUrl>()?; 44 let server_url = git_server_url.parse::<CloneUrl>()?;
32 let protocols_to_attempt = 45 let protocols_to_attempt =
33 get_write_protocols_to_try(git_repo, &server_url, decoded_nostr_url, is_grasp_server); 46 get_write_protocols_to_try(git_repo, &server_url, decoded_nostr_url, is_grasp_server);
34 47
35 let mut failed_protocols = vec![]; 48 let mut failed_protocols = vec![];
36 let mut success = false; 49 let mut success = false;
50 let mut ref_updates = HashMap::new();
37 51
38 for protocol in &protocols_to_attempt { 52 for protocol in &protocols_to_attempt {
39 term.write_line(format!("push: {} over {protocol}...", server_url.short_name(),).as_str())?; 53 term.write_line(format!("push: {} over {protocol}...", server_url.short_name(),).as_str())?;
40 54
41 let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; 55 let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?;
42 56
43 if let Err(error) = push_to_remote_url(git_repo, &formatted_url, remote_refspecs, term) { 57 match push_to_remote_url(git_repo, &formatted_url, remote_refspecs, term) {
44 term.write_line( 58 Err(error) => {
45 format!("push: {formatted_url} failed over {protocol}: {error}").as_str(), 59 term.write_line(
46 )?; 60 format!("push: {formatted_url} failed over {protocol}: {error}").as_str(),
47 failed_protocols.push(protocol); 61 )?;
48 } else { 62 failed_protocols.push(protocol);
49 success = true; 63 }
50 if !failed_protocols.is_empty() { 64 Ok(ref_updates_on_protocol) => {
51 term.write_line(format!("push: succeeded over {protocol}").as_str())?; 65 success = true;
52 let _ = set_protocol_preference(git_repo, protocol, &server_url, &Direction::Push); 66 if ref_updates_on_protocol
67 .values()
68 .all(|error| error.is_none())
69 {
70 if !failed_protocols.is_empty() {
71 term.write_line(format!("push: succeeded over {protocol}").as_str())?;
72 let _ = set_protocol_preference(
73 git_repo,
74 protocol,
75 &server_url,
76 &Direction::Push,
77 );
78 }
79 break;
80 } else {
81 term.write_line(
82 format!(
83 "push: {formatted_url} with {protocol} complete but {}ref{} not accepted:",
84 if remote_refspecs.len() != failed_protocols.len() { "some " } else {""},
85 if remote_refspecs.len() == 1 { "s"} else {""},
86 ).as_str(),
87 )?;
88 for (git_ref, error) in &ref_updates_on_protocol {
89 if let Some(error) = error {
90 term.write_line(format!("push: - {git_ref}: {error}").as_str())?;
91 }
92 }
93 // TODO do we want to report on the refs that weren't responded to?
94 ref_updates = ref_updates_on_protocol;
95 }
96 break;
53 } 97 }
54 break;
55 } 98 }
56 } 99 }
57 if success { 100 if success {
58 Ok(()) 101 Ok(ref_updates)
59 } else { 102 } else {
60 let error = anyhow!( 103 let error = anyhow!(
61 "{} failed over {}{}", 104 "{} failed over {}{}",
@@ -72,12 +115,13 @@ pub fn push_to_remote(
72 } 115 }
73} 116}
74 117
118// returns HashMaps of refspecs responded to and any failure message
75pub fn push_to_remote_url( 119pub fn push_to_remote_url(
76 git_repo: &Repo, 120 git_repo: &Repo,
77 git_server_url: &str, 121 git_server_url: &str,
78 remote_refspecs: &[String], 122 remote_refspecs: &[String],
79 term: &Term, 123 term: &Term,
80) -> Result<()> { 124) -> Result<HashMap<String, Option<String>>> {
81 let git_config = git_repo.git_repo.config()?; 125 let git_config = git_repo.git_repo.config()?;
82 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; 126 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?;
83 let auth = GitAuthenticator::default(); 127 let auth = GitAuthenticator::default();
@@ -91,6 +135,9 @@ pub fn push_to_remote_url(
91 let push_reporter = Arc::clone(&push_reporter); 135 let push_reporter = Arc::clone(&push_reporter);
92 move |name, error| { 136 move |name, error| {
93 let mut reporter = push_reporter.lock().unwrap(); 137 let mut reporter = push_reporter.lock().unwrap();
138 reporter
139 .ref_updates
140 .insert(name.to_string(), error.map(|s| s.to_string()));
94 if let Some(error) = error { 141 if let Some(error) = error {
95 let existing_lines = reporter.count_all_existing_lines(); 142 let existing_lines = reporter.count_all_existing_lines();
96 reporter.update_reference_errors.push(format!( 143 reporter.update_reference_errors.push(format!(
@@ -115,7 +162,11 @@ pub fn push_to_remote_url(
115 .unwrap_or("") 162 .unwrap_or("")
116 .replace("refs/heads/", "") 163 .replace("refs/heads/", "")
117 .replace("refs/tags/", "tags/"); 164 .replace("refs/tags/", "tags/");
118 let msg = if update.dst().is_zero() { 165 let msg = if let Some(Some(_)) =
166 reporter.ref_updates.get(update.dst_refname().unwrap_or(""))
167 {
168 format!("push: - [failed] {dst_refname}")
169 } else if update.dst().is_zero() {
119 format!("push: - [delete] {dst_refname}") 170 format!("push: - [delete] {dst_refname}")
120 } else if update.src().is_zero() { 171 } else if update.src().is_zero() {
121 if update.dst_refname().unwrap_or("").contains("refs/tags") { 172 if update.dst_refname().unwrap_or("").contains("refs/tags") {
@@ -174,7 +225,8 @@ pub fn push_to_remote_url(
174 push_options.remote_callbacks(remote_callbacks); 225 push_options.remote_callbacks(remote_callbacks);
175 git_server_remote.push(remote_refspecs, Some(&mut push_options))?; 226 git_server_remote.push(remote_refspecs, Some(&mut push_options))?;
176 let _ = git_server_remote.disconnect(); 227 let _ = git_server_remote.disconnect();
177 Ok(()) 228 let reporter = push_reporter.lock().unwrap();
229 Ok(reporter.ref_updates.clone())
178} 230}
179 231
180#[allow(clippy::cast_precision_loss)] 232#[allow(clippy::cast_precision_loss)]
@@ -223,6 +275,7 @@ pub struct PushReporter<'a> {
223 negotiation: Vec<String>, 275 negotiation: Vec<String>,
224 transfer_progress_msgs: Vec<String>, 276 transfer_progress_msgs: Vec<String>,
225 update_reference_errors: Vec<String>, 277 update_reference_errors: Vec<String>,
278 ref_updates: HashMap<String, Option<String>>,
226 term: &'a console::Term, 279 term: &'a console::Term,
227 start_time: Option<Instant>, 280 start_time: Option<Instant>,
228 end_time: Option<Instant>, 281 end_time: Option<Instant>,
@@ -234,6 +287,7 @@ impl<'a> PushReporter<'a> {
234 negotiation: vec![], 287 negotiation: vec![],
235 transfer_progress_msgs: vec![], 288 transfer_progress_msgs: vec![],
236 update_reference_errors: vec![], 289 update_reference_errors: vec![],
290 ref_updates: HashMap::new(),
237 term, 291 term,
238 start_time: None, 292 start_time: None,
239 end_time: None, 293 end_time: None,
@@ -308,3 +362,183 @@ impl<'a> PushReporter<'a> {
308 } 362 }
309 } 363 }
310} 364}
365
366#[allow(clippy::too_many_arguments)]
367pub async fn push_refs_and_generate_pr_or_pr_update_event(
368 git_repo: &Repo,
369 repo_ref: &RepoRef,
370 tip: &Sha1Hash,
371 user_ref: &UserRef,
372 root_proposal: Option<&Event>,
373 title_description_overide: &Option<(String, String)>,
374 servers: &[String],
375 git_ref: Option<String>,
376 signer: &Arc<dyn NostrSigner>,
377 term: &Term,
378) -> Result<(Option<Vec<Event>>, Vec<(String, Result<()>)>)> {
379 let mut responses: Vec<(String, Result<()>)> = vec![];
380
381 let mut unsigned_pr_event: Option<UnsignedEvent> = None;
382 for clone_url in servers {
383 let mut draft_pr_event = if let Some(ref unsigned_pr_event) = unsigned_pr_event {
384 unsigned_pr_event.clone()
385 } else {
386 generate_unsigned_pr_or_update_event(
387 git_repo,
388 repo_ref,
389 &user_ref.public_key,
390 root_proposal,
391 title_description_overide,
392 tip,
393 &[clone_url],
394 &[],
395 )?
396 };
397
398 let git_ref_used = git_ref
399 .clone()
400 .unwrap_or("refs/nostr/<event-id>".to_string())
401 .replace("<event-id>", &draft_pr_event.id().to_string());
402
403 let refspec = format!("{tip}:{git_ref_used}");
404
405 let res = if is_grasp_server_clone_url(clone_url) {
406 push_to_remote_url(git_repo, clone_url, &[refspec], term)
407 } else {
408 // anticipated only when pushing to user's own repo or a personal-fork with
409 // non-grasp git servers. this is used to extract prefered protocols / ssh
410 // details from nostr url
411 let decoded_nostr_url = {
412 if let Ok(Some((_, decoded_nostr_url))) = git_repo
413 .get_first_nostr_remote_when_in_ngit_binary()
414 .await.context("failed to list git remotes")
415 .context("no `nostr://` remote detected. `ngit sync` must be run from a repo with a nostr remote") {
416 decoded_nostr_url
417 } else {
418 repo_ref.to_nostr_git_url(&Some(git_repo))
419 }
420 };
421 push_to_remote(
422 git_repo,
423 clone_url,
424 &decoded_nostr_url,
425 &[refspec],
426 term,
427 false,
428 )
429 };
430
431 match res {
432 Err(error) => {
433 let normalized_url = normalize_grasp_server_url(clone_url)?;
434 term.write_line(&format!(
435 "push: error sending commit data to {normalized_url}: {error}"
436 ))?;
437 responses.push((clone_url.clone(), Err(anyhow!(error))));
438 }
439 Ok(ref_updates) => {
440 let normalized_url = normalize_grasp_server_url(clone_url)?;
441 if let Some((_, Some(error))) = ref_updates.iter().next() {
442 term.write_line(&format!(
443 "push: error sending commit data to {normalized_url}: {error}"
444 ))?;
445 responses.push((clone_url.clone(), Err(anyhow!(error.clone()))));
446 } else {
447 responses.push((clone_url.clone(), Ok(())));
448 term.write_line(&format!("push: commit data sent to {normalized_url}"))?;
449 unsigned_pr_event = Some(draft_pr_event);
450 }
451 }
452 }
453 }
454 if let Some(unsigned_pr_event) = unsigned_pr_event {
455 let pr_event = sign_draft_event(
456 unsigned_pr_event,
457 signer,
458 if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) {
459 "Pull Request Replacing Original Patch"
460 } else if root_proposal.is_some() {
461 "Pull Request Update"
462 } else {
463 "Pull Request"
464 }
465 .to_string(),
466 )
467 .await?;
468 if root_proposal.is_some_and(|proposal| proposal.kind.eq(&Kind::GitPatch)) {
469 Ok((
470 Some(vec![
471 pr_event,
472 create_close_status_for_original_patch(
473 signer,
474 repo_ref,
475 root_proposal.unwrap(),
476 )
477 .await?,
478 ]),
479 responses,
480 ))
481 } else {
482 Ok((Some(vec![pr_event]), responses))
483 }
484 } else {
485 Ok((None, responses))
486 }
487}
488
489async fn create_close_status_for_original_patch(
490 signer: &Arc<dyn NostrSigner>,
491 repo_ref: &RepoRef,
492 proposal: &Event,
493) -> Result<Event> {
494 let mut public_keys = repo_ref
495 .maintainers
496 .iter()
497 .copied()
498 .collect::<HashSet<PublicKey>>();
499 public_keys.insert(proposal.pubkey);
500
501 sign_event(
502 EventBuilder::new(nostr::event::Kind::GitStatusClosed, String::new()).tags(
503 [
504 vec![
505 Tag::custom(
506 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
507 vec![
508 "Git patch closed as forthcoming update is too large. Replacing with Pull Request"
509 .to_string(),
510 ],
511 ),
512 Tag::from_standardized(nostr::TagStandard::Event {
513 event_id: proposal.id,
514 relay_url: repo_ref.relays.first().cloned(),
515 marker: Some(Marker::Root),
516 public_key: None,
517 uppercase: false,
518 }),
519 ],
520 public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(),
521 repo_ref
522 .coordinates()
523 .iter()
524 .map(|c| {
525 Tag::from_standardized(TagStandard::Coordinate {
526 coordinate: c.coordinate.clone(),
527 relay_url: c.relays.first().cloned(),
528 uppercase: false,
529 })
530 })
531 .collect::<Vec<Tag>>(),
532 vec![
533 Tag::from_standardized(nostr::TagStandard::Reference(
534 repo_ref.root_commit.to_string(),
535 )),
536 ],
537 ]
538 .concat(),
539 ),
540 signer,
541 "close status for original patch".to_string(),
542 )
543 .await
544}
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs
index bca4a3b..b2bd381 100644
--- a/src/lib/repo_ref.rs
+++ b/src/lib/repo_ref.rs
@@ -40,6 +40,7 @@ pub struct RepoRef {
40 pub web: Vec<String>, 40 pub web: Vec<String>,
41 pub relays: Vec<RelayUrl>, 41 pub relays: Vec<RelayUrl>,
42 pub blossoms: Vec<Url>, 42 pub blossoms: Vec<Url>,
43 pub hashtags: Vec<String>,
43 pub maintainers: Vec<PublicKey>, 44 pub maintainers: Vec<PublicKey>,
44 pub trusted_maintainer: PublicKey, 45 pub trusted_maintainer: PublicKey,
45 // set to None if not known 46 // set to None if not known
@@ -71,6 +72,7 @@ impl TryFrom<(nostr::Event, Option<PublicKey>)> for RepoRef {
71 web: Vec::new(), 72 web: Vec::new(),
72 relays: Vec::new(), 73 relays: Vec::new(),
73 blossoms: Vec::new(), 74 blossoms: Vec::new(),
75 hashtags: Vec::new(),
74 maintainers: Vec::new(), 76 maintainers: Vec::new(),
75 trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey), 77 trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey),
76 maintainers_without_annoucnement: None, 78 maintainers_without_annoucnement: None,
@@ -118,6 +120,7 @@ impl TryFrom<(nostr::Event, Option<PublicKey>)> for RepoRef {
118 } 120 }
119 } 121 }
120 } 122 }
123 [t, hashtag, ..] if t == "t" => r.hashtags.push(hashtag.clone()),
121 [t, blossoms @ ..] if t == "blossoms" => { 124 [t, blossoms @ ..] if t == "blossoms" => {
122 for b in blossoms { 125 for b in blossoms {
123 if let Ok(b) = Url::parse(b) { 126 if let Ok(b) = Url::parse(b) {
@@ -217,6 +220,15 @@ impl RepoRef {
217 vec![format!("git repository: {}", self.name.clone())], 220 vec![format!("git repository: {}", self.name.clone())],
218 ), 221 ),
219 ], 222 ],
223 self.hashtags
224 .iter()
225 .map(|h| {
226 Tag::custom(
227 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("t")),
228 vec![h.clone()],
229 )
230 })
231 .collect(),
220 if self.blossoms.is_empty() { 232 if self.blossoms.is_empty() {
221 vec![] 233 vec![]
222 } else { 234 } else {
@@ -311,6 +323,29 @@ impl RepoRef {
311 pub fn grasp_servers(&self) -> Vec<String> { 323 pub fn grasp_servers(&self) -> Vec<String> {
312 detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier) 324 detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier)
313 } 325 }
326
327 // returns false if already present so didn't need adding
328 pub fn add_grasp_server(&mut self, clone_url: &str) -> Result<bool> {
329 if !is_grasp_server_clone_url(clone_url) {
330 bail!("invalid grasp server clone url. does not end with .git");
331 }
332
333 let relay_url = RelayUrl::parse(
334 &format_grasp_server_url_as_relay_url(clone_url)
335 .context("invalid grasp server clone url")?,
336 )
337 .context("invalid grasp server clone url")?;
338
339 if !self.relays.contains(&relay_url) {
340 self.relays.push(relay_url);
341 }
342 if !self.git_server.contains(&clone_url.to_string()) {
343 self.git_server.push(clone_url.to_string());
344 Ok(true)
345 } else {
346 Ok(false)
347 }
348 }
314} 349}
315 350
316pub async fn get_repo_coordinates_when_remote_unknown( 351pub async fn get_repo_coordinates_when_remote_unknown(
@@ -448,7 +483,7 @@ async fn get_repo_coordinate_from_user_prompt(
448 println!( 483 println!(
449 "{}", 484 "{}",
450 dim.apply_to( 485 dim.apply_to(
451 "hint: https://gitworkshop.dev/repos lists repositories and their nostr address" 486 "hint: https://gitworkshop.dev/search lists repositories and their nostr address"
452 ), 487 ),
453 ); 488 );
454 let git_repo_path = git_repo.get_path()?; 489 let git_repo_path = git_repo.get_path()?;
@@ -699,13 +734,54 @@ pub fn extract_npub(s: &str) -> Result<&str> {
699 } 734 }
700} 735}
701 736
702pub fn is_grasp_server(url: &str, grasp_servers: &[String]) -> bool { 737pub fn is_grasp_server_in_list(url: &str, grasp_servers: &[String]) -> bool {
703 if !grasp_servers.is_empty() { 738 if !grasp_servers.is_empty() {
704 if let Ok(n) = normalize_grasp_server_url(url) { 739 if let Ok(url) = normalize_grasp_server_url(url) {
705 return grasp_servers.contains(&n); 740 grasp_servers.iter().any(|s| {
741 if let Ok(s) = normalize_grasp_server_url(s) {
742 s == url
743 } else {
744 false
745 }
746 })
747 } else {
748 false
706 } 749 }
750 } else {
751 false
752 }
753}
754
755pub fn is_grasp_server_clone_url(url: &str) -> bool {
756 extract_npub(url).is_ok()
757 && (url.ends_with(".git") || url.ends_with(".git/"))
758 && url.starts_with("http")
759}
760
761pub fn format_grasp_server_url_as_relay_url(url: &str) -> Result<String> {
762 let grasp_server_url = normalize_grasp_server_url(url)?;
763 if grasp_server_url.contains("http://") {
764 return Ok(grasp_server_url.replace("http://", "ws://"));
707 } 765 }
708 false 766 Ok(format!("wss://{grasp_server_url}"))
767}
768
769pub fn format_grasp_server_url_as_clone_url(
770 grasp_server: &str,
771 public_key: &PublicKey,
772 identifier: &str,
773) -> Result<String> {
774 let grasp_server_url = normalize_grasp_server_url(grasp_server)?;
775
776 let prefix = if grasp_server_url.contains("http://") {
777 ""
778 } else {
779 "https://"
780 };
781 Ok(format!(
782 "{prefix}{grasp_server_url}/{}/{identifier}.git",
783 public_key.to_bech32()?
784 ))
709} 785}
710 786
711#[cfg(test)] 787#[cfg(test)]
@@ -730,6 +806,7 @@ mod tests {
730 RelayUrl::parse("ws://relay2.io").unwrap(), 806 RelayUrl::parse("ws://relay2.io").unwrap(),
731 ], 807 ],
732 blossoms: vec![], 808 blossoms: vec![],
809 hashtags: vec![],
733 trusted_maintainer: TEST_KEY_1_KEYS.public_key(), 810 trusted_maintainer: TEST_KEY_1_KEYS.public_key(),
734 maintainers_without_annoucnement: None, 811 maintainers_without_annoucnement: None,
735 maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()], 812 maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()],
diff --git a/src/lib/utils.rs b/src/lib/utils.rs
index 431757f..431a14f 100644
--- a/src/lib/utils.rs
+++ b/src/lib/utils.rs
@@ -3,11 +3,13 @@ use std::{
3 collections::HashMap, 3 collections::HashMap,
4 fmt, 4 fmt,
5 io::{self, Stdin}, 5 io::{self, Stdin},
6 path::Path,
6 str::FromStr, 7 str::FromStr,
7}; 8};
8 9
9use anyhow::{Context, Result, bail}; 10use anyhow::{Context, Result, bail};
10use git2::Repository; 11use git2::Repository;
12use nostr::nips::nip19::ToBech32;
11use nostr_sdk::{Event, EventId, Kind, PublicKey, Url}; 13use nostr_sdk::{Event, EventId, Kind, PublicKey, Url};
12 14
13use crate::{ 15use crate::{
@@ -20,7 +22,8 @@ use crate::{
20 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 22 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
21 }, 23 },
22 git_events::{ 24 git_events::{
23 event_is_revision_root, get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, 25 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_revision_root,
26 get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status,
24 is_event_proposal_root_for_branch, status_kinds, 27 is_event_proposal_root_for_branch, status_kinds,
25 }, 28 },
26 repo_ref::RepoRef, 29 repo_ref::RepoRef,
@@ -187,6 +190,37 @@ pub async fn get_all_proposals(
187 Ok(all_proposals) 190 Ok(all_proposals)
188} 191}
189 192
193pub async fn proposal_tip_is_pr_or_pr_update(
194 git_repo_path: &Path,
195 repo_ref: &RepoRef,
196 proposal_id: &EventId,
197) -> Result<bool> {
198 let commits_events =
199 get_all_proposal_patch_pr_pr_update_events_from_cache(git_repo_path, repo_ref, proposal_id)
200 .await
201 .context(format!(
202 "cannot get existing proposal events for {}",
203 proposal_id.to_bech32()?
204 ))?;
205 let most_recent_proposal_patch_chain = get_pr_tip_event_or_most_recent_patch_with_ancestors(
206 commits_events.clone(),
207 )
208 .context(format!(
209 "cannot find tip from proposal events for {}",
210 proposal_id.to_bech32()?,
211 ))?;
212
213 Ok([KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(
214 &most_recent_proposal_patch_chain
215 .first()
216 .context(format!(
217 "cannot find any proposal events for {}",
218 proposal_id.to_bech32()?
219 ))?
220 .kind,
221 ))
222}
223
190pub fn find_proposal_and_patches_by_branch_name<'a>( 224pub fn find_proposal_and_patches_by_branch_name<'a>(
191 refstr: &'a str, 225 refstr: &'a str,
192 proposals: &'a HashMap<EventId, (Event, Vec<Event>)>, 226 proposals: &'a HashMap<EventId, (Event, Vec<Event>)>,