upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/sub_commands/list.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/sub_commands/list.rs')
-rw-r--r--src/sub_commands/list.rs262
1 files changed, 116 insertions, 146 deletions
diff --git a/src/sub_commands/list.rs b/src/sub_commands/list.rs
index d3f583f..2ae4cfb 100644
--- a/src/sub_commands/list.rs
+++ b/src/sub_commands/list.rs
@@ -1,17 +1,19 @@
1use std::{io::Write, ops::Add}; 1use std::{collections::HashSet, io::Write, ops::Add, path::Path};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
4use nostr::nips::nip01::Coordinate;
5use nostr_sdk::PublicKey;
4 6
5use super::send::event_is_patch_set_root; 7use super::send::event_is_patch_set_root;
6#[cfg(not(test))]
7use crate::client::Client;
8#[cfg(test)] 8#[cfg(test)]
9use crate::client::MockConnect; 9use crate::client::MockConnect;
10#[cfg(not(test))]
11use crate::client::{Client, Connect};
10use crate::{ 12use crate::{
11 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, 13 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms},
12 client::Connect, 14 client::{fetching_with_report, get_events_from_cache, get_repo_ref_from_cache},
13 git::{str_to_sha1, Repo, RepoActions}, 15 git::{str_to_sha1, Repo, RepoActions},
14 repo_ref::{self, RepoRef, REPO_REF_KIND}, 16 repo_ref::{get_repo_coordinates, RepoRef},
15 sub_commands::send::{ 17 sub_commands::send::{
16 commit_msg_from_patch_oneliner, event_is_cover_letter, event_is_revision_root, 18 commit_msg_from_patch_oneliner, event_is_cover_letter, event_is_revision_root,
17 event_to_cover_letter, patch_supports_commit_ids, PATCH_KIND, 19 event_to_cover_letter, patch_supports_commit_ids, PATCH_KIND,
@@ -21,10 +23,7 @@ use crate::{
21#[allow(clippy::too_many_lines)] 23#[allow(clippy::too_many_lines)]
22pub async fn launch() -> Result<()> { 24pub async fn launch() -> Result<()> {
23 let git_repo = Repo::discover().context("cannot find a git repository")?; 25 let git_repo = Repo::discover().context("cannot find a git repository")?;
24 26 let git_repo_path = git_repo.get_path()?;
25 let root_commit = git_repo
26 .get_root_commit()
27 .context("failed to get root commit of the repository")?;
28 27
29 // TODO: check for empty repo 28 // TODO: check for empty repo
30 // TODO: check for existing maintaiers file 29 // TODO: check for existing maintaiers file
@@ -35,36 +34,47 @@ pub async fn launch() -> Result<()> {
35 #[cfg(test)] 34 #[cfg(test)]
36 let client = <MockConnect as std::default::Default>::default(); 35 let client = <MockConnect as std::default::Default>::default();
37 36
38 let repo_ref = repo_ref::fetch( 37 let repo_coordinates = get_repo_coordinates(&git_repo, &client).await?;
39 &git_repo,
40 root_commit.to_string(),
41 &client,
42 client.get_fallback_relays().clone(),
43 true,
44 )
45 .await?;
46 38
47 println!("finding proposals..."); 39 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
48 40
49 let proposal_events_and_revisions_and_statuses: Vec<nostr::Event> = 41 let repo_ref = get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?;
50 find_proposal_and_status_events(&client, &repo_ref, &root_commit.to_string()).await?;
51 42
52 if proposal_events_and_revisions_and_statuses.is_empty() { 43 let proposals_and_revisions: Vec<nostr::Event> =
44 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()).await?;
45 if proposals_and_revisions.is_empty() {
53 println!("no proposals found... create one? try `ngit send`"); 46 println!("no proposals found... create one? try `ngit send`");
54 return Ok(()); 47 return Ok(());
55 } 48 }
56 let proposal_events: Vec<&nostr::Event> = proposal_events_and_revisions_and_statuses 49
57 .iter() 50 let statuses: Vec<nostr::Event> = {
58 .filter(|e| !event_is_revision_root(e) && e.kind().as_u16().eq(&PATCH_KIND)) 51 let mut statuses = get_events_from_cache(
59 .collect::<Vec<&nostr::Event>>(); 52 git_repo_path,
53 vec![
54 nostr::Filter::default()
55 .kinds(status_kinds().clone())
56 .events(proposals_and_revisions.iter().map(nostr::Event::id)),
57 ],
58 )
59 .await?;
60 statuses.sort_by_key(|e| e.created_at);
61 statuses.reverse();
62 statuses
63 };
60 64
61 let mut open_proposals: Vec<&nostr::Event> = vec![]; 65 let mut open_proposals: Vec<&nostr::Event> = vec![];
62 let mut draft_proposals: Vec<&nostr::Event> = vec![]; 66 let mut draft_proposals: Vec<&nostr::Event> = vec![];
63 let mut closed_proposals: Vec<&nostr::Event> = vec![]; 67 let mut closed_proposals: Vec<&nostr::Event> = vec![];
64 let mut applied_proposals: Vec<&nostr::Event> = vec![]; 68 let mut applied_proposals: Vec<&nostr::Event> = vec![];
65 69
66 for proposal in &proposal_events { 70 let proposals: Vec<nostr::Event> = proposals_and_revisions
67 let status = if let Some(e) = proposal_events_and_revisions_and_statuses 71 .iter()
72 .filter(|e| !event_is_revision_root(e))
73 .cloned()
74 .collect();
75
76 for proposal in &proposals {
77 let status = if let Some(e) = statuses
68 .iter() 78 .iter()
69 .filter(|e| { 79 .filter(|e| {
70 status_kinds().contains(&e.kind()) 80 status_kinds().contains(&e.kind())
@@ -89,11 +99,6 @@ pub async fn launch() -> Result<()> {
89 } 99 }
90 } 100 }
91 101
92 if proposal_events_and_revisions_and_statuses.is_empty() {
93 println!("no open proposals found... create one? try `ngit send`");
94 return Ok(());
95 }
96
97 let mut selected_status = STATUS_KIND_OPEN; 102 let mut selected_status = STATUS_KIND_OPEN;
98 103
99 loop { 104 loop {
@@ -106,10 +111,10 @@ pub async fn launch() -> Result<()> {
106 } else if selected_status == STATUS_KIND_APPLIED { 111 } else if selected_status == STATUS_KIND_APPLIED {
107 &applied_proposals 112 &applied_proposals
108 } else { 113 } else {
109 &proposal_events 114 &open_proposals
110 }; 115 };
111 116
112 let prompt = if proposal_events.len().eq(&open_proposals.len()) { 117 let prompt = if proposals.len().eq(&open_proposals.len()) {
113 "all proposals" 118 "all proposals"
114 } else if selected_status == STATUS_KIND_OPEN { 119 } else if selected_status == STATUS_KIND_OPEN {
115 if open_proposals.is_empty() { 120 if open_proposals.is_empty() {
@@ -176,31 +181,12 @@ pub async fn launch() -> Result<()> {
176 let cover_letter = event_to_cover_letter(proposals_for_status[selected_index]) 181 let cover_letter = event_to_cover_letter(proposals_for_status[selected_index])
177 .context("cannot extract proposal details from proposal root event")?; 182 .context("cannot extract proposal details from proposal root event")?;
178 183
179 println!("finding commits..."); 184 let commits_events: Vec<nostr::Event> = get_all_proposal_patch_events_from_cache(
180 185 git_repo_path,
181 let commits_events: Vec<nostr::Event> = find_commits_for_proposal_root_events(
182 &client,
183 &[
184 vec![proposals_for_status[selected_index]],
185 proposal_events_and_revisions_and_statuses
186 .iter()
187 .filter(|e| {
188 e.kind.as_u16().eq(&PATCH_KIND)
189 && e.tags.iter().any(|t| {
190 t.as_vec().len().gt(&1)
191 && t.as_vec()[1]
192 .eq(&proposals_for_status[selected_index].id.to_string())
193 })
194 })
195 .collect::<Vec<&nostr::Event>>(),
196 ]
197 .concat(),
198 &repo_ref, 186 &repo_ref,
187 &proposals_for_status[selected_index].id(),
199 ) 188 )
200 .await?; 189 .await?;
201 // for commit in &commits_events {
202 // println!("cevent: {:?}", commit.as_json());
203 // }
204 190
205 let Ok(most_recent_proposal_patch_chain) = 191 let Ok(most_recent_proposal_patch_chain) =
206 get_most_recent_patch_with_ancestors(commits_events.clone()) 192 get_most_recent_patch_with_ancestors(commits_events.clone())
@@ -829,109 +815,93 @@ pub fn status_kinds() -> Vec<nostr::Kind> {
829 ] 815 ]
830} 816}
831 817
832pub async fn find_proposal_and_status_events( 818pub async fn get_proposals_and_revisions_from_cache(
833 #[cfg(test)] client: &crate::client::MockConnect, 819 git_repo_path: &Path,
834 #[cfg(not(test))] client: &Client, 820 repo_coordinates: HashSet<Coordinate>,
835 repo_ref: &RepoRef,
836 root_commit: &str,
837) -> Result<Vec<nostr::Event>> { 821) -> Result<Vec<nostr::Event>> {
838 let repo_tags_filter = nostr::Filter::default().custom_tag( 822 let mut proposals = get_events_from_cache(
839 nostr::SingleLetterTag::lowercase(nostr::Alphabet::A), 823 git_repo_path,
840 repo_ref 824 vec![
841 .maintainers 825 nostr::Filter::default()
842 .iter() 826 .kind(nostr::Kind::Custom(PATCH_KIND))
843 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)), 827 .custom_tag(
844 ); 828 nostr::SingleLetterTag::lowercase(nostr_sdk::Alphabet::A),
845 let mut proposals = client 829 repo_coordinates
846 .get_events( 830 .iter()
847 repo_ref.relays.clone(), 831 .map(std::string::ToString::to_string)
848 vec![ 832 .collect::<Vec<String>>(),
849 repo_tags_filter 833 ),
850 .clone() 834 ],
851 .kind(nostr::Kind::Custom(PATCH_KIND)) 835 )
852 .custom_tag( 836 .await?
853 nostr::SingleLetterTag::lowercase(nostr::Alphabet::T), 837 .iter()
854 vec!["root"], 838 .filter(|e| event_is_patch_set_root(e))
855 ), 839 .cloned()
856 // also pick up proposals from the same repo but no target at our maintainers repo 840 .collect::<Vec<nostr::Event>>();
857 // events
858 nostr::Filter::default()
859 .reference(root_commit)
860 .kind(nostr::Kind::Custom(PATCH_KIND))
861 .custom_tag(
862 nostr::SingleLetterTag::lowercase(nostr::Alphabet::T),
863 vec!["root"],
864 ),
865 repo_tags_filter.clone().kinds(status_kinds().clone()),
866 nostr::Filter::default()
867 .reference(root_commit)
868 .kinds(status_kinds().clone()),
869 ],
870 )
871 .await
872 .context("cannot get proposal events")?
873 .iter()
874 .filter(|e| {
875 (event_is_patch_set_root(e) || status_kinds().contains(&e.kind()))
876 && (e
877 .tags
878 .iter()
879 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(root_commit))
880 || e.tags.iter().any(|t| {
881 t.as_vec().len() > 1
882 && repo_ref
883 .maintainers
884 .iter()
885 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier))
886 .any(|d| t.as_vec()[1].eq(&d))
887 }))
888 })
889 .map(std::borrow::ToOwned::to_owned)
890 .collect::<Vec<nostr::Event>>();
891 proposals.sort_by_key(|e| e.created_at); 841 proposals.sort_by_key(|e| e.created_at);
892 proposals.reverse(); 842 proposals.reverse();
893 Ok(proposals) 843 Ok(proposals)
894} 844}
895 845
896pub async fn find_commits_for_proposal_root_events( 846pub async fn get_all_proposal_patch_events_from_cache(
897 #[cfg(test)] client: &crate::client::MockConnect, 847 git_repo_path: &Path,
898 #[cfg(not(test))] client: &Client,
899 proposal_root_events: &Vec<&nostr::Event>,
900 repo_ref: &RepoRef, 848 repo_ref: &RepoRef,
849 proposal_id: &nostr::EventId,
901) -> Result<Vec<nostr::Event>> { 850) -> Result<Vec<nostr::Event>> {
902 let mut patch_events: Vec<nostr::Event> = client 851 let mut commit_events = get_events_from_cache(
903 .get_events( 852 git_repo_path,
904 repo_ref.relays.clone(), 853 vec![
854 nostr::Filter::default()
855 .kind(nostr::Kind::Custom(PATCH_KIND))
856 .event(*proposal_id),
857 nostr::Filter::default()
858 .kind(nostr::Kind::Custom(PATCH_KIND))
859 .id(*proposal_id),
860 ],
861 )
862 .await?;
863
864 let permissioned_users: HashSet<PublicKey> = [
865 repo_ref.maintainers.clone(),
866 vec![
867 commit_events
868 .iter()
869 .find(|e| e.id().eq(proposal_id))
870 .context("proposal not in cache")?
871 .author(),
872 ],
873 ]
874 .concat()
875 .iter()
876 .copied()
877 .collect();
878 commit_events.retain(|e| permissioned_users.contains(e.author_ref()));
879
880 let revision_roots: HashSet<nostr::EventId> = commit_events
881 .iter()
882 .filter(|e| event_is_revision_root(e))
883 .map(nostr::Event::id)
884 .collect();
885
886 if !revision_roots.is_empty() {
887 for event in get_events_from_cache(
888 git_repo_path,
905 vec![ 889 vec![
906 nostr::Filter::default() 890 nostr::Filter::default()
907 .kind(nostr::Kind::Custom(PATCH_KIND)) 891 .kind(nostr::Kind::Custom(PATCH_KIND))
908 .events( 892 .events(revision_roots)
909 proposal_root_events 893 .authors(permissioned_users.clone()),
910 .iter()
911 .map(|e| e.id)
912 .collect::<Vec<nostr::EventId>>(),
913 ),
914 ], 894 ],
915 ) 895 )
916 .await 896 .await?
917 .context("cannot fetch patch events")? 897 {
918 .iter() 898 commit_events.push(event);
919 .filter(|e| {
920 e.kind.as_u16() == PATCH_KIND
921 && e.tags.iter().any(|t| {
922 t.as_vec().len() > 2
923 && proposal_root_events
924 .iter()
925 .any(|e| t.as_vec()[1].eq(&e.id.to_string()))
926 })
927 })
928 .map(std::borrow::ToOwned::to_owned)
929 .collect();
930
931 for e in proposal_root_events {
932 if !event_is_cover_letter(e) && !patch_events.iter().any(|e2| e2.id.eq(&e.id)) {
933 patch_events.push(e.to_owned().clone());
934 } 899 }
935 } 900 }
936 Ok(patch_events) 901
902 Ok(commit_events
903 .iter()
904 .filter(|e| !event_is_cover_letter(e) && permissioned_users.contains(e.author_ref()))
905 .cloned()
906 .collect())
937} 907}