diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-03-04 15:37:17 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-03-04 15:37:17 +0000 |
| commit | 293ef01e141846f7de5af2c8c6be9d6c694083fd (patch) | |
| tree | d4ee20d9a89fe622fcfa058e600fdd25e56eab91 /src/bin | |
| parent | b4c7b5e24d05aef878e155a1bedc22de54609fbb (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')
| -rw-r--r-- | src/bin/ngit/cli.rs | 13 | ||||
| -rw-r--r-- | src/bin/ngit/main.rs | 19 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/issue_list.rs | 75 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/list.rs | 93 |
4 files changed, 140 insertions, 60 deletions
diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs index 0599b51..5ee9165 100644 --- a/src/bin/ngit/cli.rs +++ b/src/bin/ngit/cli.rs | |||
| @@ -177,6 +177,10 @@ pub enum PrCommands { | |||
| 177 | /// Filter by status (comma-separated: open,draft,closed,applied) | 177 | /// Filter by status (comma-separated: open,draft,closed,applied) |
| 178 | #[arg(long, default_value = "open,draft")] | 178 | #[arg(long, default_value = "open,draft")] |
| 179 | status: String, | 179 | status: String, |
| 180 | /// Filter by label (repeatable, OR logic: --label bug --label | ||
| 181 | /// help-wanted) | ||
| 182 | #[arg(long = "label", value_name = "LABEL")] | ||
| 183 | labels: Vec<String>, | ||
| 180 | /// Output as JSON | 184 | /// Output as JSON |
| 181 | #[arg(long)] | 185 | #[arg(long)] |
| 182 | json: bool, | 186 | json: bool, |
| @@ -311,9 +315,10 @@ pub enum IssueCommands { | |||
| 311 | /// Filter by status (comma-separated: open,draft,closed,applied) | 315 | /// Filter by status (comma-separated: open,draft,closed,applied) |
| 312 | #[arg(long, default_value = "open")] | 316 | #[arg(long, default_value = "open")] |
| 313 | status: String, | 317 | status: String, |
| 314 | /// Filter by hashtag/label (comma-separated) | 318 | /// Filter by label (repeatable, OR logic: --label bug --label |
| 315 | #[arg(long)] | 319 | /// help-wanted) |
| 316 | hashtag: Option<String>, | 320 | #[arg(long = "label", value_name = "LABEL")] |
| 321 | labels: Vec<String>, | ||
| 317 | /// Output as JSON | 322 | /// Output as JSON |
| 318 | #[arg(long)] | 323 | #[arg(long)] |
| 319 | json: bool, | 324 | json: bool, |
| @@ -347,7 +352,7 @@ pub enum IssueCommands { | |||
| 347 | /// Issue body / description | 352 | /// Issue body / description |
| 348 | #[arg(long)] | 353 | #[arg(long)] |
| 349 | body: Option<String>, | 354 | body: Option<String>, |
| 350 | /// Hashtag labels (repeatable: --label bug --label help-wanted) | 355 | /// Labels to apply (repeatable: --label bug --label help-wanted) |
| 351 | #[arg(long = "label", value_name = "LABEL")] | 356 | #[arg(long = "label", value_name = "LABEL")] |
| 352 | labels: Vec<String>, | 357 | labels: Vec<String>, |
| 353 | }, | 358 | }, |
diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs index 4c1aa75..28bf1da 100644 --- a/src/bin/ngit/main.rs +++ b/src/bin/ngit/main.rs | |||
| @@ -57,12 +57,20 @@ async fn main() { | |||
| 57 | Commands::Pr(args) => match &args.pr_command { | 57 | Commands::Pr(args) => match &args.pr_command { |
| 58 | PrCommands::List { | 58 | PrCommands::List { |
| 59 | status, | 59 | status, |
| 60 | labels, | ||
| 60 | json, | 61 | json, |
| 61 | id, | 62 | id, |
| 62 | offline, | 63 | offline, |
| 63 | } => { | 64 | } => { |
| 64 | sub_commands::list::launch(status.clone(), *json, false, id.clone(), *offline) | 65 | sub_commands::list::launch( |
| 65 | .await | 66 | status.clone(), |
| 67 | labels.clone(), | ||
| 68 | *json, | ||
| 69 | false, | ||
| 70 | id.clone(), | ||
| 71 | *offline, | ||
| 72 | ) | ||
| 73 | .await | ||
| 66 | } | 74 | } |
| 67 | PrCommands::View { | 75 | PrCommands::View { |
| 68 | id, | 76 | id, |
| @@ -72,6 +80,7 @@ async fn main() { | |||
| 72 | } => { | 80 | } => { |
| 73 | sub_commands::list::launch( | 81 | sub_commands::list::launch( |
| 74 | "open,draft,closed,applied".to_string(), | 82 | "open,draft,closed,applied".to_string(), |
| 83 | vec![], | ||
| 75 | *json, | 84 | *json, |
| 76 | *comments, | 85 | *comments, |
| 77 | Some(id.clone()), | 86 | Some(id.clone()), |
| @@ -122,14 +131,14 @@ async fn main() { | |||
| 122 | Commands::Issue(args) => match &args.issue_command { | 131 | Commands::Issue(args) => match &args.issue_command { |
| 123 | IssueCommands::List { | 132 | IssueCommands::List { |
| 124 | status, | 133 | status, |
| 125 | hashtag, | 134 | labels, |
| 126 | json, | 135 | json, |
| 127 | id, | 136 | id, |
| 128 | offline, | 137 | offline, |
| 129 | } => { | 138 | } => { |
| 130 | sub_commands::issue_list::launch( | 139 | sub_commands::issue_list::launch( |
| 131 | status.clone(), | 140 | status.clone(), |
| 132 | hashtag.clone(), | 141 | labels.clone(), |
| 133 | *json, | 142 | *json, |
| 134 | false, | 143 | false, |
| 135 | id.clone(), | 144 | id.clone(), |
| @@ -145,7 +154,7 @@ async fn main() { | |||
| 145 | } => { | 154 | } => { |
| 146 | sub_commands::issue_list::launch( | 155 | sub_commands::issue_list::launch( |
| 147 | "open,draft,closed,applied".to_string(), | 156 | "open,draft,closed,applied".to_string(), |
| 148 | None, | 157 | vec![], |
| 149 | *json, | 158 | *json, |
| 150 | *comments, | 159 | *comments, |
| 151 | Some(id.clone()), | 160 | Some(id.clone()), |
diff --git a/src/bin/ngit/sub_commands/issue_list.rs b/src/bin/ngit/sub_commands/issue_list.rs index 864cd76..d7c8ac9 100644 --- a/src/bin/ngit/sub_commands/issue_list.rs +++ b/src/bin/ngit/sub_commands/issue_list.rs | |||
| @@ -38,7 +38,7 @@ fn get_issue_title(event: &nostr::Event) -> String { | |||
| 38 | }) | 38 | }) |
| 39 | } | 39 | } |
| 40 | 40 | ||
| 41 | fn get_issue_hashtags(event: &nostr::Event) -> Vec<String> { | 41 | fn get_issue_labels(event: &nostr::Event) -> Vec<String> { |
| 42 | event | 42 | event |
| 43 | .tags | 43 | .tags |
| 44 | .iter() | 44 | .iter() |
| @@ -136,7 +136,7 @@ async fn get_comments_for_issue( | |||
| 136 | #[allow(clippy::too_many_lines)] | 136 | #[allow(clippy::too_many_lines)] |
| 137 | pub async fn launch( | 137 | pub async fn launch( |
| 138 | status: String, | 138 | status: String, |
| 139 | hashtag: Option<String>, | 139 | labels: Vec<String>, |
| 140 | json: bool, | 140 | json: bool, |
| 141 | show_comments: bool, | 141 | show_comments: bool, |
| 142 | id: Option<String>, | 142 | id: Option<String>, |
| @@ -188,11 +188,8 @@ pub async fn launch( | |||
| 188 | 188 | ||
| 189 | let status_filter: HashSet<&str> = status.split(',').map(str::trim).collect(); | 189 | let status_filter: HashSet<&str> = status.split(',').map(str::trim).collect(); |
| 190 | 190 | ||
| 191 | let hashtag_filter: Option<HashSet<String>> = hashtag.map(|h| { | 191 | // OR filter: issue must have at least one of the requested labels. |
| 192 | h.split(',') | 192 | let label_filter: HashSet<String> = labels.iter().map(|l| l.trim().to_lowercase()).collect(); |
| 193 | .map(|s| s.trim().to_lowercase()) | ||
| 194 | .collect::<HashSet<String>>() | ||
| 195 | }); | ||
| 196 | 193 | ||
| 197 | // Use an empty vec as the "all_pr_roots" argument — issues don't have PR | 194 | // Use an empty vec as the "all_pr_roots" argument — issues don't have PR |
| 198 | // revisions, so we pass an empty slice. | 195 | // revisions, so we pass an empty slice. |
| @@ -206,16 +203,16 @@ pub async fn launch( | |||
| 206 | if !status_filter.contains(status_str) && !status_filter.contains("unknown") { | 203 | if !status_filter.contains(status_str) && !status_filter.contains("unknown") { |
| 207 | return None; | 204 | return None; |
| 208 | } | 205 | } |
| 209 | let tags = get_issue_hashtags(issue); | 206 | let issue_labels = get_issue_labels(issue); |
| 210 | if let Some(ref hf) = hashtag_filter { | 207 | if !label_filter.is_empty() { |
| 211 | let issue_tags_lower: HashSet<String> = | 208 | let issue_labels_lower: HashSet<String> = |
| 212 | tags.iter().map(|t| t.to_lowercase()).collect(); | 209 | issue_labels.iter().map(|t| t.to_lowercase()).collect(); |
| 213 | if !hf.iter().any(|h| issue_tags_lower.contains(h)) { | 210 | if !label_filter.iter().any(|l| issue_labels_lower.contains(l)) { |
| 214 | return None; | 211 | return None; |
| 215 | } | 212 | } |
| 216 | } | 213 | } |
| 217 | let comment_count = comment_counts.get(&issue.id).copied().unwrap_or(0); | 214 | let comment_count = comment_counts.get(&issue.id).copied().unwrap_or(0); |
| 218 | Some((issue, status_kind, tags, comment_count)) | 215 | Some((issue, status_kind, issue_labels, comment_count)) |
| 219 | }) | 216 | }) |
| 220 | .collect(); | 217 | .collect(); |
| 221 | 218 | ||
| @@ -254,7 +251,7 @@ pub async fn launch( | |||
| 254 | if json { | 251 | if json { |
| 255 | output_json(&filtered)?; | 252 | output_json(&filtered)?; |
| 256 | } else { | 253 | } else { |
| 257 | output_table(&filtered, &status, hashtag_filter.as_ref()); | 254 | output_table(&filtered, &status, &label_filter); |
| 258 | } | 255 | } |
| 259 | 256 | ||
| 260 | Ok(()) | 257 | Ok(()) |
| @@ -305,7 +302,7 @@ fn show_issue_details( | |||
| 305 | nostr::EventId::from_hex(event_id_or_nevent).context("failed to parse event id")? | 302 | nostr::EventId::from_hex(event_id_or_nevent).context("failed to parse event id")? |
| 306 | }; | 303 | }; |
| 307 | 304 | ||
| 308 | let (issue, status_kind, tags, comment_count) = issues | 305 | let (issue, status_kind, labels, comment_count) = issues |
| 309 | .iter() | 306 | .iter() |
| 310 | .find(|(e, _, _, _)| e.id == target_id) | 307 | .find(|(e, _, _, _)| e.id == target_id) |
| 311 | .context("issue not found")?; | 308 | .context("issue not found")?; |
| @@ -333,7 +330,7 @@ fn show_issue_details( | |||
| 333 | "status": status, | 330 | "status": status, |
| 334 | "title": title, | 331 | "title": title, |
| 335 | "author": issue.pubkey.to_bech32().unwrap_or_default(), | 332 | "author": issue.pubkey.to_bech32().unwrap_or_default(), |
| 336 | "hashtags": tags, | 333 | "labels": labels, |
| 337 | "comment_count": comment_count, | 334 | "comment_count": comment_count, |
| 338 | "comments": comments_json, | 335 | "comments": comments_json, |
| 339 | "description": issue.content, | 336 | "description": issue.content, |
| @@ -344,7 +341,7 @@ fn show_issue_details( | |||
| 344 | "status": status, | 341 | "status": status, |
| 345 | "title": title, | 342 | "title": title, |
| 346 | "author": issue.pubkey.to_bech32().unwrap_or_default(), | 343 | "author": issue.pubkey.to_bech32().unwrap_or_default(), |
| 347 | "hashtags": tags, | 344 | "labels": labels, |
| 348 | "comment_count": comment_count, | 345 | "comment_count": comment_count, |
| 349 | "description": issue.content, | 346 | "description": issue.content, |
| 350 | }) | 347 | }) |
| @@ -356,13 +353,13 @@ fn show_issue_details( | |||
| 356 | println!("Title: {title}"); | 353 | println!("Title: {title}"); |
| 357 | println!("Author: {}", issue.pubkey.to_bech32().unwrap_or_default()); | 354 | println!("Author: {}", issue.pubkey.to_bech32().unwrap_or_default()); |
| 358 | println!("Status: {status}"); | 355 | println!("Status: {status}"); |
| 359 | if !tags.is_empty() { | 356 | if !labels.is_empty() { |
| 360 | let tags_str = tags | 357 | let labels_str = labels |
| 361 | .iter() | 358 | .iter() |
| 362 | .map(|t| format!("#{t}")) | 359 | .map(|l| format!("#{l}")) |
| 363 | .collect::<Vec<_>>() | 360 | .collect::<Vec<_>>() |
| 364 | .join(" "); | 361 | .join(" "); |
| 365 | println!("Tags: {tags_str}"); | 362 | println!("Labels: {labels_str}"); |
| 366 | } | 363 | } |
| 367 | 364 | ||
| 368 | if !issue.content.is_empty() { | 365 | if !issue.content.is_empty() { |
| @@ -425,39 +422,35 @@ fn chrono_timestamp(unix_secs: u64) -> String { | |||
| 425 | fn output_table( | 422 | fn output_table( |
| 426 | issues: &[(&nostr::Event, Kind, Vec<String>, usize)], | 423 | issues: &[(&nostr::Event, Kind, Vec<String>, usize)], |
| 427 | status_filter: &str, | 424 | status_filter: &str, |
| 428 | hashtag_filter: Option<&HashSet<String>>, | 425 | label_filter: &HashSet<String>, |
| 429 | ) { | 426 | ) { |
| 430 | println!("{:<66} {:<8} {:<5} TITLE HASHTAGS", "ID", "STATUS", "CMTS"); | 427 | println!("{:<66} {:<8} {:<5} TITLE LABELS", "ID", "STATUS", "CMTS"); |
| 431 | for (issue, status_kind, tags, comment_count) in issues { | 428 | for (issue, status_kind, labels, comment_count) in issues { |
| 432 | let id = issue.id.to_string(); | 429 | let id = issue.id.to_string(); |
| 433 | let status = status_kind_to_str(*status_kind); | 430 | let status = status_kind_to_str(*status_kind); |
| 434 | let title = get_issue_title(issue); | 431 | let title = get_issue_title(issue); |
| 435 | let tags_str = if tags.is_empty() { | 432 | let labels_str = if labels.is_empty() { |
| 436 | String::new() | 433 | String::new() |
| 437 | } else { | 434 | } else { |
| 438 | tags.iter() | 435 | labels |
| 439 | .map(|t| format!("#{t}")) | 436 | .iter() |
| 437 | .map(|l| format!("#{l}")) | ||
| 440 | .collect::<Vec<_>>() | 438 | .collect::<Vec<_>>() |
| 441 | .join(" ") | 439 | .join(" ") |
| 442 | }; | 440 | }; |
| 443 | if tags_str.is_empty() { | 441 | if labels_str.is_empty() { |
| 444 | println!("{id:<66} {status:<8} {comment_count:<5} {title}"); | 442 | println!("{id:<66} {status:<8} {comment_count:<5} {title}"); |
| 445 | } else { | 443 | } else { |
| 446 | println!("{id:<66} {status:<8} {comment_count:<5} {title} {tags_str}"); | 444 | println!("{id:<66} {status:<8} {comment_count:<5} {title} {labels_str}"); |
| 447 | } | 445 | } |
| 448 | } | 446 | } |
| 449 | 447 | ||
| 450 | println!(); | 448 | println!(); |
| 451 | print!("--status {status_filter}"); | 449 | print!("--status {status_filter}"); |
| 452 | if let Some(hf) = hashtag_filter { | 450 | if !label_filter.is_empty() { |
| 453 | let tags: Vec<&String> = hf.iter().collect(); | 451 | for l in label_filter { |
| 454 | print!( | 452 | print!(" --label {l}"); |
| 455 | " --hashtag {}", | 453 | } |
| 456 | tags.iter() | ||
| 457 | .map(|s| s.as_str()) | ||
| 458 | .collect::<Vec<_>>() | ||
| 459 | .join(",") | ||
| 460 | ); | ||
| 461 | } | 454 | } |
| 462 | println!(); | 455 | println!(); |
| 463 | } | 456 | } |
| @@ -465,14 +458,14 @@ fn output_table( | |||
| 465 | fn output_json(issues: &[(&nostr::Event, Kind, Vec<String>, usize)]) -> Result<()> { | 458 | fn output_json(issues: &[(&nostr::Event, Kind, Vec<String>, usize)]) -> Result<()> { |
| 466 | let json_output: Vec<serde_json::Value> = issues | 459 | let json_output: Vec<serde_json::Value> = issues |
| 467 | .iter() | 460 | .iter() |
| 468 | .map(|(issue, status_kind, tags, comment_count)| { | 461 | .map(|(issue, status_kind, labels, comment_count)| { |
| 469 | serde_json::json!({ | 462 | serde_json::json!({ |
| 470 | "id": issue.id.to_string(), | 463 | "id": issue.id.to_string(), |
| 471 | "status": status_kind_to_str(*status_kind), | 464 | "status": status_kind_to_str(*status_kind), |
| 472 | "title": get_issue_title(issue), | 465 | "title": get_issue_title(issue), |
| 473 | "author": issue.pubkey.to_bech32().unwrap_or_default(), | 466 | "author": issue.pubkey.to_bech32().unwrap_or_default(), |
| 474 | "hashtags": tags, | 467 | "labels": labels, |
| 475 | "comments": comment_count, | 468 | "comment_count": comment_count, |
| 476 | }) | 469 | }) |
| 477 | }) | 470 | }) |
| 478 | .collect(); | 471 | .collect(); |
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)] |
| 98 | pub async fn launch( | 98 | pub 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 | ||
| 302 | fn output_table(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef, status_filter: &str) { | 319 | fn 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)] | ||
| 403 | fn show_proposal_details( | 456 | fn 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!(); |