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.rs138
1 files changed, 106 insertions, 32 deletions
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs
index a583ca5..60e129f 100644
--- a/src/bin/ngit/sub_commands/list.rs
+++ b/src/bin/ngit/sub_commands/list.rs
@@ -95,7 +95,13 @@ fn run_git_fetch(remote_name: &str) -> Result<()> {
95} 95}
96 96
97#[allow(clippy::too_many_lines)] 97#[allow(clippy::too_many_lines)]
98pub async fn launch(status: String, json: bool, id: Option<String>, offline: bool) -> Result<()> { 98pub async fn launch(
99 status: String,
100 json: bool,
101 show_comments: bool,
102 id: Option<String>,
103 offline: bool,
104) -> Result<()> {
99 if std::env::var("NGIT_INTERACTIVE_MODE").is_ok() { 105 if std::env::var("NGIT_INTERACTIVE_MODE").is_ok() {
100 return launch_interactive().await; 106 return launch_interactive().await;
101 } 107 }
@@ -203,12 +209,26 @@ pub async fn launch(status: String, json: bool, id: Option<String>, offline: boo
203 if let Some(ref event_id_or_nevent) = id { 209 if let Some(ref event_id_or_nevent) = id {
204 // Resolve the target proposal ID so we can fetch its comments. 210 // Resolve the target proposal ID so we can fetch its comments.
205 let target_id = resolve_event_id(event_id_or_nevent)?; 211 let target_id = resolve_event_id(event_id_or_nevent)?;
206 let comments = get_comments_for_proposal(git_repo_path, &target_id).await?; 212 let comments = if show_comments {
213 get_comments_for_proposal(git_repo_path, &target_id).await?
214 } else {
215 vec![]
216 };
217 // Always fetch the count so we can display it even without --comments.
218 let comment_count = if show_comments {
219 comments.len()
220 } else {
221 get_comments_for_proposal(git_repo_path, &target_id)
222 .await?
223 .len()
224 };
207 return show_proposal_details( 225 return show_proposal_details(
208 &filtered_proposals, 226 &filtered_proposals,
209 &repo_ref, 227 &repo_ref,
210 event_id_or_nevent, 228 event_id_or_nevent,
211 json, 229 json,
230 show_comments,
231 comment_count,
212 &comments, 232 &comments,
213 ); 233 );
214 } 234 }
@@ -353,11 +373,40 @@ fn output_json(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef) -> Resu
353 Ok(()) 373 Ok(())
354} 374}
355 375
376/// Extract the parent comment ID from a NIP-22 comment event.
377/// Returns `Some(id)` when the lowercase `e` tag differs from the root `E` tag
378/// (i.e. the comment is a reply to another comment, not a top-level comment).
379fn comment_reply_to(comment: &nostr::Event) -> Option<nostr::EventId> {
380 let root_id = comment.tags.iter().find_map(|t| {
381 let s = t.as_slice();
382 if s.len() >= 2 && s[0].eq("E") {
383 nostr::EventId::parse(&s[1]).ok()
384 } else {
385 None
386 }
387 })?;
388 comment.tags.iter().find_map(|t| {
389 let s = t.as_slice();
390 if s.len() >= 2 && s[0].eq("e") {
391 let parent_id = nostr::EventId::parse(&s[1]).ok()?;
392 if parent_id == root_id {
393 None
394 } else {
395 Some(parent_id)
396 }
397 } else {
398 None
399 }
400 })
401}
402
356fn show_proposal_details( 403fn show_proposal_details(
357 proposals: &[(&nostr::Event, Kind)], 404 proposals: &[(&nostr::Event, Kind)],
358 _repo_ref: &RepoRef, 405 _repo_ref: &RepoRef,
359 event_id_or_nevent: &str, 406 event_id_or_nevent: &str,
360 json: bool, 407 json: bool,
408 show_comments: bool,
409 comment_count: usize,
361 comments: &[nostr::Event], 410 comments: &[nostr::Event],
362) -> Result<()> { 411) -> Result<()> {
363 use nostr::ToBech32; 412 use nostr::ToBech32;
@@ -373,26 +422,41 @@ fn show_proposal_details(
373 .context("failed to extract proposal details from proposal root event")?; 422 .context("failed to extract proposal details from proposal root event")?;
374 423
375 if json { 424 if json {
376 let comments_json: Vec<serde_json::Value> = comments 425 let json_output = if show_comments {
377 .iter() 426 let comments_json: Vec<serde_json::Value> = comments
378 .map(|c| { 427 .iter()
379 serde_json::json!({ 428 .map(|c| {
380 "id": c.id.to_string(), 429 let reply_to = comment_reply_to(c).map(|id| id.to_string());
381 "author": c.pubkey.to_bech32().unwrap_or_default(), 430 serde_json::json!({
382 "created_at": c.created_at.as_secs(), 431 "id": c.id.to_string(),
383 "body": c.content, 432 "author": c.pubkey.to_bech32().unwrap_or_default(),
433 "created_at": c.created_at.as_secs(),
434 "reply_to": reply_to,
435 "body": c.content,
436 })
384 }) 437 })
438 .collect();
439 serde_json::json!({
440 "id": proposal.id.to_string(),
441 "status": status_kind_to_str(*status_kind),
442 "title": cover_letter.title,
443 "author": proposal.pubkey.to_bech32().unwrap_or_default(),
444 "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
445 "comment_count": comment_count,
446 "comments": comments_json,
447 "description": cover_letter.description,
385 }) 448 })
386 .collect(); 449 } else {
387 let json_output = serde_json::json!({ 450 serde_json::json!({
388 "id": proposal.id.to_string(), 451 "id": proposal.id.to_string(),
389 "status": status_kind_to_str(*status_kind), 452 "status": status_kind_to_str(*status_kind),
390 "title": cover_letter.title, 453 "title": cover_letter.title,
391 "author": proposal.pubkey.to_bech32().unwrap_or_default(), 454 "author": proposal.pubkey.to_bech32().unwrap_or_default(),
392 "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 455 "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
393 "comments": comments_json, 456 "comment_count": comment_count,
394 "description": cover_letter.description, 457 "description": cover_letter.description,
395 }); 458 })
459 };
396 println!("{}", serde_json::to_string_pretty(&json_output)?); 460 println!("{}", serde_json::to_string_pretty(&json_output)?);
397 return Ok(()); 461 return Ok(());
398 } 462 }
@@ -416,21 +480,31 @@ fn show_proposal_details(
416 } 480 }
417 } 481 }
418 482
419 if comments.is_empty() { 483 if show_comments {
420 println!("Comments: 0"); 484 if comments.is_empty() {
421 } else { 485 println!("Comments: 0");
422 println!(); 486 } else {
423 println!("Comments ({}):", comments.len());
424 let dim = console::Style::new().color256(247);
425 for comment in comments {
426 let author = comment.pubkey.to_bech32().unwrap_or_default();
427 let ts = chrono_timestamp(comment.created_at.as_secs());
428 println!(); 487 println!();
429 println!("{}", dim.apply_to(format!(" {author} {ts}"))); 488 println!("Comments ({comment_count}):");
430 for line in comment.content.lines() { 489 let dim = console::Style::new().color256(247);
431 println!(" {line}"); 490 for comment in comments {
491 let author = comment.pubkey.to_bech32().unwrap_or_default();
492 let ts = chrono_timestamp(comment.created_at.as_secs());
493 println!();
494 if let Some(parent_id) = comment_reply_to(comment) {
495 println!(
496 "{}",
497 dim.apply_to(format!(" ↳ reply to {}", &parent_id.to_hex()[..8]))
498 );
499 }
500 println!("{}", dim.apply_to(format!(" {author} {ts}")));
501 for line in comment.content.lines() {
502 println!(" {line}");
503 }
432 } 504 }
433 } 505 }
506 } else {
507 println!("Comments: {comment_count} (use --comments to view)");
434 } 508 }
435 509
436 println!(); 510 println!();