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:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-03-04 15:37:17 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-03-04 15:37:17 +0000
commit293ef01e141846f7de5af2c8c6be9d6c694083fd (patch)
treed4ee20d9a89fe622fcfa058e600fdd25e56eab91 /src/bin/ngit/sub_commands/list.rs
parentb4c7b5e24d05aef878e155a1bedc22de54609fbb (diff)
standardise on --label; add label filter and display to pr list/view
- rename --hashtag (comma-separated) to --label (repeatable) on issue list, matching the --label flag already used on issue create - add --label filter to pr list with the same OR semantics (matching GitHub) - display labels column in pr list table and Labels: line in pr view - include labels array in all JSON outputs (list and view for both issue and pr) - rename internal 'hashtags' -> 'labels' throughout issue_list.rs and list.rs
Diffstat (limited to 'src/bin/ngit/sub_commands/list.rs')
-rw-r--r--src/bin/ngit/sub_commands/list.rs93
1 files changed, 83 insertions, 10 deletions
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs
index 60e129f..547c051 100644
--- a/src/bin/ngit/sub_commands/list.rs
+++ b/src/bin/ngit/sub_commands/list.rs
@@ -97,6 +97,7 @@ fn run_git_fetch(remote_name: &str) -> Result<()> {
97#[allow(clippy::too_many_lines)] 97#[allow(clippy::too_many_lines)]
98pub async fn launch( 98pub async fn launch(
99 status: String, 99 status: String,
100 labels: Vec<String>,
100 json: bool, 101 json: bool,
101 show_comments: bool, 102 show_comments: bool,
102 id: Option<String>, 103 id: Option<String>,
@@ -187,6 +188,9 @@ pub async fn launch(
187 188
188 let status_filter: HashSet<&str> = status.split(',').map(str::trim).collect(); 189 let status_filter: HashSet<&str> = status.split(',').map(str::trim).collect();
189 190
191 // OR filter: proposal must have at least one of the requested labels.
192 let label_filter: HashSet<String> = labels.iter().map(|l| l.trim().to_lowercase()).collect();
193
190 let filtered_proposals: Vec<(&nostr::Event, Kind)> = proposals 194 let filtered_proposals: Vec<(&nostr::Event, Kind)> = proposals
191 .iter() 195 .iter()
192 .filter_map(|p| { 196 .filter_map(|p| {
@@ -198,11 +202,24 @@ pub async fn launch(
198 Kind::GitStatusApplied => "applied", 202 Kind::GitStatusApplied => "applied",
199 _ => "unknown", 203 _ => "unknown",
200 }; 204 };
201 if status_filter.contains(status_str) || status_filter.contains("unknown") { 205 if !status_filter.contains(status_str) && !status_filter.contains("unknown") {
202 Some((p, status_kind)) 206 return None;
203 } else {
204 None
205 } 207 }
208 if !label_filter.is_empty() {
209 let proposal_labels: HashSet<String> = p
210 .tags
211 .iter()
212 .filter(|t| {
213 let s = t.as_slice();
214 s.len() >= 2 && s[0].eq("t")
215 })
216 .map(|t| t.as_slice()[1].to_lowercase())
217 .collect();
218 if !label_filter.iter().any(|l| proposal_labels.contains(l)) {
219 return None;
220 }
221 }
222 Some((p, status_kind))
206 }) 223 })
207 .collect(); 224 .collect();
208 225
@@ -236,7 +253,7 @@ pub async fn launch(
236 if json { 253 if json {
237 output_json(&filtered_proposals, &repo_ref)?; 254 output_json(&filtered_proposals, &repo_ref)?;
238 } else { 255 } else {
239 output_table(&filtered_proposals, &repo_ref, &status); 256 output_table(&filtered_proposals, &repo_ref, &status, &label_filter);
240 } 257 }
241 258
242 Ok(()) 259 Ok(())
@@ -299,13 +316,18 @@ fn status_kind_to_str(kind: Kind) -> &'static str {
299 } 316 }
300} 317}
301 318
302fn output_table(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef, status_filter: &str) { 319fn output_table(
320 proposals: &[(&nostr::Event, Kind)],
321 _repo_ref: &RepoRef,
322 status_filter: &str,
323 label_filter: &HashSet<String>,
324) {
303 if proposals.is_empty() { 325 if proposals.is_empty() {
304 println!("No proposals found matching status: {status_filter}"); 326 println!("No proposals found matching status: {status_filter}");
305 return; 327 return;
306 } 328 }
307 329
308 println!("{:<66} {:<8} TITLE", "ID", "STATUS"); 330 println!("{:<66} {:<8} TITLE LABELS", "ID", "STATUS");
309 for (proposal, status_kind) in proposals { 331 for (proposal, status_kind) in proposals {
310 let id = proposal.id.to_string(); 332 let id = proposal.id.to_string();
311 let status = status_kind_to_str(*status_kind); 333 let status = status_kind_to_str(*status_kind);
@@ -316,11 +338,31 @@ fn output_table(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef, status
316 } else { 338 } else {
317 proposal.id.to_string() 339 proposal.id.to_string()
318 }; 340 };
319 println!("{id:<66} {status:<8} {title}"); 341 let labels_str: String = proposal
342 .tags
343 .iter()
344 .filter(|t| {
345 let s = t.as_slice();
346 s.len() >= 2 && s[0].eq("t")
347 })
348 .map(|t| format!("#{}", t.as_slice()[1]))
349 .collect::<Vec<_>>()
350 .join(" ");
351 if labels_str.is_empty() {
352 println!("{id:<66} {status:<8} {title}");
353 } else {
354 println!("{id:<66} {status:<8} {title} {labels_str}");
355 }
320 } 356 }
321 357
322 println!(); 358 println!();
323 println!("--status {status_filter}"); 359 print!("--status {status_filter}");
360 if !label_filter.is_empty() {
361 for l in label_filter {
362 print!(" --label {l}");
363 }
364 }
365 println!();
324 println!( 366 println!(
325 "{}", 367 "{}",
326 console::style("To view: ngit pr view <id>").yellow() 368 console::style("To view: ngit pr view <id>").yellow()
@@ -359,12 +401,22 @@ fn output_json(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef) -> Resu
359 String::new(), 401 String::new(),
360 ) 402 )
361 }; 403 };
404 let labels: Vec<String> = proposal
405 .tags
406 .iter()
407 .filter(|t| {
408 let s = t.as_slice();
409 s.len() >= 2 && s[0].eq("t")
410 })
411 .map(|t| t.as_slice()[1].clone())
412 .collect();
362 serde_json::json!({ 413 serde_json::json!({
363 "id": id, 414 "id": id,
364 "status": status, 415 "status": status,
365 "title": title, 416 "title": title,
366 "author": author, 417 "author": author,
367 "branch": branch 418 "branch": branch,
419 "labels": labels,
368 }) 420 })
369 }) 421 })
370 .collect(); 422 .collect();
@@ -400,6 +452,7 @@ fn comment_reply_to(comment: &nostr::Event) -> Option<nostr::EventId> {
400 }) 452 })
401} 453}
402 454
455#[allow(clippy::too_many_lines)]
403fn show_proposal_details( 456fn show_proposal_details(
404 proposals: &[(&nostr::Event, Kind)], 457 proposals: &[(&nostr::Event, Kind)],
405 _repo_ref: &RepoRef, 458 _repo_ref: &RepoRef,
@@ -421,6 +474,16 @@ fn show_proposal_details(
421 let cover_letter = event_to_cover_letter(proposal) 474 let cover_letter = event_to_cover_letter(proposal)
422 .context("failed to extract proposal details from proposal root event")?; 475 .context("failed to extract proposal details from proposal root event")?;
423 476
477 let proposal_labels: Vec<String> = proposal
478 .tags
479 .iter()
480 .filter(|t| {
481 let s = t.as_slice();
482 s.len() >= 2 && s[0].eq("t")
483 })
484 .map(|t| t.as_slice()[1].clone())
485 .collect();
486
424 if json { 487 if json {
425 let json_output = if show_comments { 488 let json_output = if show_comments {
426 let comments_json: Vec<serde_json::Value> = comments 489 let comments_json: Vec<serde_json::Value> = comments
@@ -442,6 +505,7 @@ fn show_proposal_details(
442 "title": cover_letter.title, 505 "title": cover_letter.title,
443 "author": proposal.pubkey.to_bech32().unwrap_or_default(), 506 "author": proposal.pubkey.to_bech32().unwrap_or_default(),
444 "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 507 "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
508 "labels": proposal_labels,
445 "comment_count": comment_count, 509 "comment_count": comment_count,
446 "comments": comments_json, 510 "comments": comments_json,
447 "description": cover_letter.description, 511 "description": cover_letter.description,
@@ -453,6 +517,7 @@ fn show_proposal_details(
453 "title": cover_letter.title, 517 "title": cover_letter.title,
454 "author": proposal.pubkey.to_bech32().unwrap_or_default(), 518 "author": proposal.pubkey.to_bech32().unwrap_or_default(),
455 "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, 519 "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?,
520 "labels": proposal_labels,
456 "comment_count": comment_count, 521 "comment_count": comment_count,
457 "description": cover_letter.description, 522 "description": cover_letter.description,
458 }) 523 })
@@ -471,6 +536,14 @@ fn show_proposal_details(
471 "Branch: {}", 536 "Branch: {}",
472 cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()? 537 cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?
473 ); 538 );
539 if !proposal_labels.is_empty() {
540 let labels_str = proposal_labels
541 .iter()
542 .map(|l| format!("#{l}"))
543 .collect::<Vec<_>>()
544 .join(" ");
545 println!("Labels: {labels_str}");
546 }
474 547
475 if !cover_letter.description.is_empty() { 548 if !cover_letter.description.is_empty() {
476 println!(); 549 println!();