upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/list.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit/sub_commands/list.rs')
-rw-r--r--src/bin/ngit/sub_commands/list.rs245
1 files changed, 244 insertions, 1 deletions
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs
index 981307d..7873fae 100644
--- a/src/bin/ngit/sub_commands/list.rs
+++ b/src/bin/ngit/sub_commands/list.rs
@@ -14,6 +14,8 @@ use ngit::{
14 repo_ref::{RepoRef, is_grasp_server_in_list}, 14 repo_ref::{RepoRef, is_grasp_server_in_list},
15}; 15};
16use nostr::filter::{Alphabet, SingleLetterTag}; 16use nostr::filter::{Alphabet, SingleLetterTag};
17use nostr::nips::nip19::Nip19;
18use nostr::{FromBech32, ToBech32};
17use nostr_sdk::Kind; 19use nostr_sdk::Kind;
18 20
19use crate::{ 21use crate::{
@@ -30,7 +32,248 @@ use crate::{
30}; 32};
31 33
32#[allow(clippy::too_many_lines)] 34#[allow(clippy::too_many_lines)]
33pub async fn launch() -> Result<()> { 35pub async fn launch(status: String, json: bool, id: Option<String>) -> Result<()> {
36 if std::env::var("NGIT_INTERACTIVE_MODE").is_ok() {
37 return launch_interactive().await;
38 }
39
40 let git_repo = Repo::discover().context("failed to find a git repository")?;
41 let git_repo_path = git_repo.get_path()?;
42
43 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
44
45 let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
46
47 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
48
49 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
50
51 let proposals_and_revisions: Vec<nostr::Event> =
52 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()).await?;
53 if proposals_and_revisions.is_empty() {
54 println!("no proposals found... create one? try `ngit send`");
55 return Ok(());
56 }
57
58 let statuses: Vec<nostr::Event> = {
59 let mut statuses = get_events_from_local_cache(
60 git_repo_path,
61 vec![
62 nostr::Filter::default()
63 .kinds(status_kinds().clone())
64 .events(proposals_and_revisions.iter().map(|e| e.id)),
65 nostr::Filter::default()
66 .custom_tags(
67 SingleLetterTag::uppercase(Alphabet::E),
68 proposals_and_revisions.iter().map(|e| e.id),
69 )
70 .kinds(status_kinds().clone()),
71 ],
72 )
73 .await?;
74 statuses.sort_by_key(|e| e.created_at);
75 statuses.reverse();
76 statuses
77 };
78
79 let mut open_proposals: Vec<&nostr::Event> = vec![];
80 let mut draft_proposals: Vec<&nostr::Event> = vec![];
81 let mut closed_proposals: Vec<&nostr::Event> = vec![];
82 let mut applied_proposals: Vec<&nostr::Event> = vec![];
83
84 let proposals: Vec<nostr::Event> = proposals_and_revisions
85 .iter()
86 .filter(|e| !event_is_revision_root(e))
87 .cloned()
88 .collect();
89
90 for proposal in &proposals {
91 let status_kind = get_status(proposal, &repo_ref, &statuses, &proposals);
92 if status_kind.eq(&Kind::GitStatusOpen) {
93 open_proposals.push(proposal);
94 } else if status_kind.eq(&Kind::GitStatusClosed) {
95 closed_proposals.push(proposal);
96 } else if status_kind.eq(&Kind::GitStatusDraft) {
97 draft_proposals.push(proposal);
98 } else if status_kind.eq(&Kind::GitStatusApplied) {
99 applied_proposals.push(proposal);
100 }
101 }
102
103 let status_filter: HashSet<&str> = status.split(',').map(str::trim).collect();
104
105 let filtered_proposals: Vec<(&nostr::Event, Kind)> = proposals
106 .iter()
107 .filter_map(|p| {
108 let status_kind = get_status(p, &repo_ref, &statuses, &proposals);
109 let status_str = match status_kind {
110 Kind::GitStatusOpen => "open",
111 Kind::GitStatusDraft => "draft",
112 Kind::GitStatusClosed => "closed",
113 Kind::GitStatusApplied => "applied",
114 _ => "unknown",
115 };
116 if status_filter.contains(status_str) || status_filter.contains("unknown") {
117 Some((p, status_kind))
118 } else {
119 None
120 }
121 })
122 .collect();
123
124 if let Some(ref event_id_or_nevent) = id {
125 return show_proposal_details(&filtered_proposals, &repo_ref, event_id_or_nevent, json);
126 }
127
128 if json {
129 output_json(&filtered_proposals, &repo_ref)?;
130 } else {
131 output_table(&filtered_proposals, &repo_ref);
132 }
133
134 Ok(())
135}
136
137fn status_kind_to_str(kind: Kind) -> &'static str {
138 match kind {
139 Kind::GitStatusOpen => "open",
140 Kind::GitStatusDraft => "draft",
141 Kind::GitStatusClosed => "closed",
142 Kind::GitStatusApplied => "applied",
143 _ => "unknown",
144 }
145}
146
147fn output_table(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef) {
148 if proposals.is_empty() {
149 println!("No proposals found matching the filter.");
150 return;
151 }
152
153 println!("{:<8} {:<8} TITLE", "ID", "STATUS");
154 for (proposal, status_kind) in proposals {
155 let id = &proposal.id.to_string()[..7];
156 let status = status_kind_to_str(*status_kind);
157 let title = if let Ok(cl) = event_to_cover_letter(proposal) {
158 cl.title
159 } else if let Ok(msg) = tag_value(proposal, "description") {
160 msg.split('\n').collect::<Vec<&str>>()[0].to_string()
161 } else {
162 proposal.id.to_string()
163 };
164 println!("{id:<8} {status:<8} {title}");
165 }
166
167 println!();
168 println!("To checkout: ngit checkout <id>");
169 println!("To apply: ngit apply <id>");
170}
171
172fn output_json(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef) -> Result<()> {
173 let json_output: Vec<serde_json::Value> = proposals
174 .iter()
175 .map(|(proposal, status_kind)| {
176 let id = proposal.id.to_string();
177 let status = status_kind_to_str(*status_kind).to_string();
178 let (title, author, branch) = if let Ok(cl) = event_to_cover_letter(proposal) {
179 (
180 cl.title.clone(),
181 proposal.pubkey.to_bech32().unwrap_or_default(),
182 cl.get_branch_name_with_pr_prefix_and_shorthand_id()
183 .unwrap_or_default(),
184 )
185 } else {
186 let title = tag_value(proposal, "description").map_or_else(
187 |_| proposal.id.to_string(),
188 |d| d.split('\n').collect::<Vec<&str>>()[0].to_string(),
189 );
190 (
191 title,
192 proposal.pubkey.to_bech32().unwrap_or_default(),
193 String::new(),
194 )
195 };
196 serde_json::json!({
197 "id": id,
198 "status": status,
199 "title": title,
200 "author": author,
201 "branch": branch
202 })
203 })
204 .collect();
205
206 println!("{}", serde_json::to_string_pretty(&json_output)?);
207 Ok(())
208}
209
210fn show_proposal_details(
211 proposals: &[(&nostr::Event, Kind)],
212 _repo_ref: &RepoRef,
213 event_id_or_nevent: &str,
214 json: bool,
215) -> Result<()> {
216 let target_id = if event_id_or_nevent.starts_with("nevent") {
217 let nip19 = Nip19::from_bech32(event_id_or_nevent)
218 .context("failed to parse nevent")?;
219 match nip19 {
220 Nip19::EventId(id) => id,
221 Nip19::Event(event) => event.event_id,
222 _ => bail!("invalid nevent format"),
223 }
224 } else {
225 nostr::EventId::from_hex(event_id_or_nevent).context("failed to parse event id")?
226 };
227
228 let (proposal, status_kind) = proposals
229 .iter()
230 .find(|(p, _)| p.id == target_id)
231 .context("proposal not found")?;
232
233 let cover_letter = event_to_cover_letter(proposal)
234 .context("failed to extract proposal details from proposal root event")?;
235
236 if json {
237 let json_output = serde_json::json!({
238 "id": proposal.id.to_string(),
239 "status": status_kind_to_str(*status_kind),
240 "title": cover_letter.title,
241 "author": proposal.pubkey.to_bech32().unwrap_or_default(),
242 "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
243 "description": cover_letter.description,
244 });
245 println!("{}", serde_json::to_string_pretty(&json_output)?);
246 return Ok(());
247 }
248
249 println!("Title: {}", cover_letter.title);
250 println!(
251 "Author: {}",
252 proposal.pubkey.to_bech32().unwrap_or_default()
253 );
254 println!("Status: {}", status_kind_to_str(*status_kind));
255 println!(
256 "Branch: {}",
257 cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?
258 );
259
260 if !cover_letter.description.is_empty() {
261 println!();
262 println!("Description:");
263 for line in cover_letter.description.lines() {
264 println!(" {line}");
265 }
266 }
267
268 println!();
269 println!("To checkout: ngit checkout {}", &proposal.id.to_string()[..7]);
270 println!("To apply: ngit apply {}", &proposal.id.to_string()[..7]);
271
272 Ok(())
273}
274
275#[allow(clippy::too_many_lines)]
276async fn launch_interactive() -> Result<()> {
34 let git_repo = Repo::discover().context("failed to find a git repository")?; 277 let git_repo = Repo::discover().context("failed to find a git repository")?;
35 let git_repo_path = git_repo.get_path()?; 278 let git_repo_path = git_repo.get_path()?;
36 279