upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-03-05 11:28:36 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-03-05 11:28:36 +0000
commit0e493c455a0345c206dd1c5b0dfb5322b8a4e3e9 (patch)
tree105517fc7a592e469a0f72667f9b364895052287 /src
parent2f2819cc2365be07fedfd35ab3654b3607e29e76 (diff)
feat(labels): fetch and apply NIP-32 kind-1985 label events
- Add KIND_LABEL (kind 1985) constant to git_events.rs - Add get_labels() merging inline t-tags with external kind-1985 events, gating each on author-or-maintainer permission - Extend get_fetch_filters() to request kind-1985 events for all known issue and proposal IDs - Track label event counts in FetchReport (field + Display + consolidation) - Update issue_list.rs and list.rs to fetch label events from cache and pass them through get_labels() instead of reading t-tags inline
Diffstat (limited to 'src')
-rw-r--r--src/bin/ngit/sub_commands/issue_list.rs27
-rw-r--r--src/bin/ngit/sub_commands/list.rs83
-rw-r--r--src/lib/client.rs38
-rw-r--r--src/lib/git_events.rs88
4 files changed, 168 insertions, 68 deletions
diff --git a/src/bin/ngit/sub_commands/issue_list.rs b/src/bin/ngit/sub_commands/issue_list.rs
index d7c8ac9..22b1b8a 100644
--- a/src/bin/ngit/sub_commands/issue_list.rs
+++ b/src/bin/ngit/sub_commands/issue_list.rs
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use ngit::{ 4use ngit::{
5 client::{Params, get_events_from_local_cache, get_issues_from_cache}, 5 client::{Params, get_events_from_local_cache, get_issues_from_cache},
6 git_events::{KIND_COMMENT, get_status, status_kinds, tag_value}, 6 git_events::{KIND_COMMENT, KIND_LABEL, get_labels, get_status, status_kinds, tag_value},
7}; 7};
8use nostr::{ 8use nostr::{
9 FromBech32, ToBech32, 9 FromBech32, ToBech32,
@@ -38,17 +38,7 @@ fn get_issue_title(event: &nostr::Event) -> String {
38 }) 38 })
39} 39}
40 40
41fn get_issue_labels(event: &nostr::Event) -> Vec<String> { 41
42 event
43 .tags
44 .iter()
45 .filter(|t| {
46 let s = t.as_slice();
47 s.len() >= 2 && s[0].eq("t")
48 })
49 .map(|t| t.as_slice()[1].clone())
50 .collect()
51}
52 42
53fn status_kind_to_str(kind: Kind) -> &'static str { 43fn status_kind_to_str(kind: Kind) -> &'static str {
54 match kind { 44 match kind {
@@ -184,6 +174,17 @@ pub async fn launch(
184 statuses 174 statuses
185 }; 175 };
186 176
177 // Fetch NIP-32 kind-1985 label events for all issues.
178 let label_events: Vec<nostr::Event> = get_events_from_local_cache(
179 git_repo_path,
180 vec![
181 nostr::Filter::default()
182 .events(issues.iter().map(|e| e.id))
183 .kind(KIND_LABEL),
184 ],
185 )
186 .await?;
187
187 let comment_counts = get_comment_counts(git_repo_path, &issues).await?; 188 let comment_counts = get_comment_counts(git_repo_path, &issues).await?;
188 189
189 let status_filter: HashSet<&str> = status.split(',').map(str::trim).collect(); 190 let status_filter: HashSet<&str> = status.split(',').map(str::trim).collect();
@@ -203,7 +204,7 @@ pub async fn launch(
203 if !status_filter.contains(status_str) && !status_filter.contains("unknown") { 204 if !status_filter.contains(status_str) && !status_filter.contains("unknown") {
204 return None; 205 return None;
205 } 206 }
206 let issue_labels = get_issue_labels(issue); 207 let issue_labels = get_labels(issue, &repo_ref, &label_events);
207 if !label_filter.is_empty() { 208 if !label_filter.is_empty() {
208 let issue_labels_lower: HashSet<String> = 209 let issue_labels_lower: HashSet<String> =
209 issue_labels.iter().map(|t| t.to_lowercase()).collect(); 210 issue_labels.iter().map(|t| t.to_lowercase()).collect();
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs
index 547c051..404b25e 100644
--- a/src/bin/ngit/sub_commands/list.rs
+++ b/src/bin/ngit/sub_commands/list.rs
@@ -15,7 +15,8 @@ use ngit::{
15 }, 15 },
16 fetch::fetch_from_git_server, 16 fetch::fetch_from_git_server,
17 git_events::{ 17 git_events::{
18 KIND_COMMENT, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch, 18 KIND_COMMENT, KIND_LABEL, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE,
19 get_commit_id_from_patch, get_labels,
19 get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, status_kinds, tag_value, 20 get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, status_kinds, tag_value,
20 }, 21 },
21 repo_ref::{RepoRef, is_grasp_server_in_list}, 22 repo_ref::{RepoRef, is_grasp_server_in_list},
@@ -162,6 +163,17 @@ pub async fn launch(
162 statuses 163 statuses
163 }; 164 };
164 165
166 // Fetch NIP-32 kind-1985 label events for all proposals.
167 let label_events: Vec<nostr::Event> = get_events_from_local_cache(
168 git_repo_path,
169 vec![
170 nostr::Filter::default()
171 .events(proposals_and_revisions.iter().map(|e| e.id))
172 .kind(KIND_LABEL),
173 ],
174 )
175 .await?;
176
165 let mut open_proposals: Vec<&nostr::Event> = vec![]; 177 let mut open_proposals: Vec<&nostr::Event> = vec![];
166 let mut draft_proposals: Vec<&nostr::Event> = vec![]; 178 let mut draft_proposals: Vec<&nostr::Event> = vec![];
167 let mut closed_proposals: Vec<&nostr::Event> = vec![]; 179 let mut closed_proposals: Vec<&nostr::Event> = vec![];
@@ -191,7 +203,7 @@ pub async fn launch(
191 // OR filter: proposal must have at least one of the requested labels. 203 // 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(); 204 let label_filter: HashSet<String> = labels.iter().map(|l| l.trim().to_lowercase()).collect();
193 205
194 let filtered_proposals: Vec<(&nostr::Event, Kind)> = proposals 206 let filtered_proposals: Vec<(&nostr::Event, Kind, Vec<String>)> = proposals
195 .iter() 207 .iter()
196 .filter_map(|p| { 208 .filter_map(|p| {
197 let status_kind = get_status(p, &repo_ref, &statuses, &proposals); 209 let status_kind = get_status(p, &repo_ref, &statuses, &proposals);
@@ -205,21 +217,15 @@ pub async fn launch(
205 if !status_filter.contains(status_str) && !status_filter.contains("unknown") { 217 if !status_filter.contains(status_str) && !status_filter.contains("unknown") {
206 return None; 218 return None;
207 } 219 }
220 let proposal_labels = get_labels(p, &repo_ref, &label_events);
208 if !label_filter.is_empty() { 221 if !label_filter.is_empty() {
209 let proposal_labels: HashSet<String> = p 222 let proposal_labels_lower: HashSet<String> =
210 .tags 223 proposal_labels.iter().map(|l| l.to_lowercase()).collect();
211 .iter() 224 if !label_filter.iter().any(|l| proposal_labels_lower.contains(l)) {
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; 225 return None;
220 } 226 }
221 } 227 }
222 Some((p, status_kind)) 228 Some((p, status_kind, proposal_labels))
223 }) 229 })
224 .collect(); 230 .collect();
225 231
@@ -241,7 +247,6 @@ pub async fn launch(
241 }; 247 };
242 return show_proposal_details( 248 return show_proposal_details(
243 &filtered_proposals, 249 &filtered_proposals,
244 &repo_ref,
245 event_id_or_nevent, 250 event_id_or_nevent,
246 json, 251 json,
247 show_comments, 252 show_comments,
@@ -251,9 +256,9 @@ pub async fn launch(
251 } 256 }
252 257
253 if json { 258 if json {
254 output_json(&filtered_proposals, &repo_ref)?; 259 output_json(&filtered_proposals)?;
255 } else { 260 } else {
256 output_table(&filtered_proposals, &repo_ref, &status, &label_filter); 261 output_table(&filtered_proposals, &status, &label_filter);
257 } 262 }
258 263
259 Ok(()) 264 Ok(())
@@ -317,8 +322,7 @@ fn status_kind_to_str(kind: Kind) -> &'static str {
317} 322}
318 323
319fn output_table( 324fn output_table(
320 proposals: &[(&nostr::Event, Kind)], 325 proposals: &[(&nostr::Event, Kind, Vec<String>)],
321 _repo_ref: &RepoRef,
322 status_filter: &str, 326 status_filter: &str,
323 label_filter: &HashSet<String>, 327 label_filter: &HashSet<String>,
324) { 328) {
@@ -328,7 +332,7 @@ fn output_table(
328 } 332 }
329 333
330 println!("{:<66} {:<8} TITLE LABELS", "ID", "STATUS"); 334 println!("{:<66} {:<8} TITLE LABELS", "ID", "STATUS");
331 for (proposal, status_kind) in proposals { 335 for (proposal, status_kind, proposal_labels) in proposals {
332 let id = proposal.id.to_string(); 336 let id = proposal.id.to_string();
333 let status = status_kind_to_str(*status_kind); 337 let status = status_kind_to_str(*status_kind);
334 let title = if let Ok(cl) = event_to_cover_letter(proposal) { 338 let title = if let Ok(cl) = event_to_cover_letter(proposal) {
@@ -338,14 +342,9 @@ fn output_table(
338 } else { 342 } else {
339 proposal.id.to_string() 343 proposal.id.to_string()
340 }; 344 };
341 let labels_str: String = proposal 345 let labels_str: String = proposal_labels
342 .tags
343 .iter() 346 .iter()
344 .filter(|t| { 347 .map(|l| format!("#{l}"))
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<_>>() 348 .collect::<Vec<_>>()
350 .join(" "); 349 .join(" ");
351 if labels_str.is_empty() { 350 if labels_str.is_empty() {
@@ -377,10 +376,10 @@ fn output_table(
377 ); 376 );
378} 377}
379 378
380fn output_json(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef) -> Result<()> { 379fn output_json(proposals: &[(&nostr::Event, Kind, Vec<String>)]) -> Result<()> {
381 let json_output: Vec<serde_json::Value> = proposals 380 let json_output: Vec<serde_json::Value> = proposals
382 .iter() 381 .iter()
383 .map(|(proposal, status_kind)| { 382 .map(|(proposal, status_kind, proposal_labels)| {
384 let id = proposal.id.to_string(); 383 let id = proposal.id.to_string();
385 let status = status_kind_to_str(*status_kind).to_string(); 384 let status = status_kind_to_str(*status_kind).to_string();
386 let (title, author, branch) = if let Ok(cl) = event_to_cover_letter(proposal) { 385 let (title, author, branch) = if let Ok(cl) = event_to_cover_letter(proposal) {
@@ -401,22 +400,13 @@ fn output_json(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef) -> Resu
401 String::new(), 400 String::new(),
402 ) 401 )
403 }; 402 };
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();
413 serde_json::json!({ 403 serde_json::json!({
414 "id": id, 404 "id": id,
415 "status": status, 405 "status": status,
416 "title": title, 406 "title": title,
417 "author": author, 407 "author": author,
418 "branch": branch, 408 "branch": branch,
419 "labels": labels, 409 "labels": proposal_labels,
420 }) 410 })
421 }) 411 })
422 .collect(); 412 .collect();
@@ -454,8 +444,7 @@ fn comment_reply_to(comment: &nostr::Event) -> Option<nostr::EventId> {
454 444
455#[allow(clippy::too_many_lines)] 445#[allow(clippy::too_many_lines)]
456fn show_proposal_details( 446fn show_proposal_details(
457 proposals: &[(&nostr::Event, Kind)], 447 proposals: &[(&nostr::Event, Kind, Vec<String>)],
458 _repo_ref: &RepoRef,
459 event_id_or_nevent: &str, 448 event_id_or_nevent: &str,
460 json: bool, 449 json: bool,
461 show_comments: bool, 450 show_comments: bool,
@@ -466,24 +455,14 @@ fn show_proposal_details(
466 455
467 let target_id = resolve_event_id(event_id_or_nevent)?; 456 let target_id = resolve_event_id(event_id_or_nevent)?;
468 457
469 let (proposal, status_kind) = proposals 458 let (proposal, status_kind, proposal_labels) = proposals
470 .iter() 459 .iter()
471 .find(|(p, _)| p.id == target_id) 460 .find(|(p, _, _)| p.id == target_id)
472 .context("proposal not found")?; 461 .context("proposal not found")?;
473 462
474 let cover_letter = event_to_cover_letter(proposal) 463 let cover_letter = event_to_cover_letter(proposal)
475 .context("failed to extract proposal details from proposal root event")?; 464 .context("failed to extract proposal details from proposal root event")?;
476 465
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
487 if json { 466 if json {
488 let json_output = if show_comments { 467 let json_output = if show_comments {
489 let comments_json: Vec<serde_json::Value> = comments 468 let comments_json: Vec<serde_json::Value> = comments
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 8501a1f..94a173f 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -56,9 +56,9 @@ use crate::{
56 get_dirs, 56 get_dirs,
57 git::{Repo, RepoActions, get_git_config_item}, 57 git::{Repo, RepoActions, get_git_config_item},
58 git_events::{ 58 git_events::{
59 KIND_COMMENT, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, KIND_USER_GRASP_LIST, 59 KIND_COMMENT, KIND_LABEL, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE,
60 event_is_cover_letter, event_is_patch_set_root, event_is_revision_root, 60 KIND_USER_GRASP_LIST, event_is_cover_letter, event_is_patch_set_root,
61 event_is_valid_pr_or_pr_update, status_kinds, 61 event_is_revision_root, event_is_valid_pr_or_pr_update, status_kinds,
62 }, 62 },
63 login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, 63 login::{get_likely_logged_in_user, user::get_user_ref_from_cache},
64 repo_ref::{RepoRef, normalize_grasp_server_url}, 64 repo_ref::{RepoRef, normalize_grasp_server_url},
@@ -1998,6 +1998,8 @@ async fn process_fetched_events(
1998 } 1998 }
1999 } else if event.kind.eq(&KIND_COMMENT) { 1999 } else if event.kind.eq(&KIND_COMMENT) {
2000 report.comments.insert(event.id); 2000 report.comments.insert(event.id);
2001 } else if event.kind.eq(&KIND_LABEL) {
2002 report.labels.insert(event.id);
2001 } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind) 2003 } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind)
2002 { 2004 {
2003 if request.missing_contributor_profiles.contains(&event.pubkey) { 2005 if request.missing_contributor_profiles.contains(&event.pubkey) {
@@ -2121,6 +2123,9 @@ pub fn consolidate_fetch_reports(reports: Vec<Result<FetchReport>>) -> FetchRepo
2121 for c in relay_report.comments { 2123 for c in relay_report.comments {
2122 report.comments.insert(c); 2124 report.comments.insert(c);
2123 } 2125 }
2126 for c in relay_report.labels {
2127 report.labels.insert(c);
2128 }
2124 report.deletions += relay_report.deletions; 2129 report.deletions += relay_report.deletions;
2125 for c in relay_report.contributor_profiles { 2130 for c in relay_report.contributor_profiles {
2126 report.contributor_profiles.insert(c); 2131 report.contributor_profiles.insert(c);
@@ -2245,6 +2250,24 @@ pub fn get_fetch_filters(
2245 ] 2250 ]
2246 } 2251 }
2247 }, 2252 },
2253 // Fetch NIP-32 kind-1985 label events for issues and proposals.
2254 // Label events reference the target via a lowercase `e` tag.
2255 {
2256 let all_root_ids: HashSet<EventId> = issue_ids
2257 .iter()
2258 .chain(proposal_ids.iter())
2259 .copied()
2260 .collect();
2261 if all_root_ids.is_empty() {
2262 vec![]
2263 } else {
2264 vec![
2265 nostr::Filter::default()
2266 .events(all_root_ids)
2267 .kind(KIND_LABEL),
2268 ]
2269 }
2270 },
2248 // Request kind-5 deletions for state events and repo announcements by 2271 // Request kind-5 deletions for state events and repo announcements by
2249 // their event ID (#e tag), as per NIP-09. The #a-tagged filter above 2272 // their event ID (#e tag), as per NIP-09. The #a-tagged filter above
2250 // covers addressable-event deletions; this covers the specific event IDs 2273 // covers addressable-event deletions; this covers the specific event IDs
@@ -2333,6 +2356,8 @@ pub struct FetchReport {
2333 issue_statuses: HashSet<EventId>, 2356 issue_statuses: HashSet<EventId>,
2334 /// NIP-22 kind-1111 comments against issues, patches, and PRs. 2357 /// NIP-22 kind-1111 comments against issues, patches, and PRs.
2335 comments: HashSet<EventId>, 2358 comments: HashSet<EventId>,
2359 /// NIP-32 kind-1985 label events for issues and proposals.
2360 labels: HashSet<EventId>,
2336 /// Count of kind-5 deletion events received (for display purposes). 2361 /// Count of kind-5 deletion events received (for display purposes).
2337 deletions: u32, 2362 deletions: u32,
2338 contributor_profiles: HashSet<PublicKey>, 2363 contributor_profiles: HashSet<PublicKey>,
@@ -2421,6 +2446,13 @@ impl Display for FetchReport {
2421 if self.comments.len() > 1 { "s" } else { "" }, 2446 if self.comments.len() > 1 { "s" } else { "" },
2422 )); 2447 ));
2423 } 2448 }
2449 if !self.labels.is_empty() {
2450 display_items.push(format!(
2451 "{} label{}",
2452 self.labels.len(),
2453 if self.labels.len() > 1 { "s" } else { "" },
2454 ));
2455 }
2424 if self.deletions > 0 { 2456 if self.deletions > 0 {
2425 display_items.push(format!( 2457 display_items.push(format!(
2426 "{} deletion{}", 2458 "{} deletion{}",
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs
index 7c5dda2..a5aef12 100644
--- a/src/lib/git_events.rs
+++ b/src/lib/git_events.rs
@@ -91,6 +91,9 @@ pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619);
91pub const KIND_USER_GRASP_LIST: Kind = Kind::Custom(10317); 91pub const KIND_USER_GRASP_LIST: Kind = Kind::Custom(10317);
92/// NIP-22 comment (kind 1111) — threaded comments on any event. 92/// NIP-22 comment (kind 1111) — threaded comments on any event.
93pub const KIND_COMMENT: Kind = Kind::Custom(1111); 93pub const KIND_COMMENT: Kind = Kind::Custom(1111);
94/// NIP-32 label event (kind 1985) — applies hashtag labels to an existing
95/// event. Used to add labels to issues, patches and PRs after the fact.
96pub const KIND_LABEL: Kind = Kind::Custom(1985);
94 97
95pub fn event_is_patch_set_root(event: &Event) -> bool { 98pub fn event_is_patch_set_root(event: &Event) -> bool {
96 event.kind.eq(&Kind::GitPatch) 99 event.kind.eq(&Kind::GitPatch)
@@ -975,6 +978,91 @@ pub fn is_event_proposal_root_for_branch(
975 )) 978 ))
976} 979}
977 980
981/// Compute the effective set of labels for `event`.
982///
983/// Labels come from two sources, both subject to the same permission check:
984///
985/// 1. `t` tags on the event itself (self-reported by the event author).
986/// 2. NIP-32 kind-1985 label events in `all_label_events` that reference
987/// `event` via a lowercase `e` tag and carry `["L", "#t"]` +
988/// `["l", "<value>", "#t"]` tags.
989///
990/// A label is only applied when the author of the source event (the original
991/// event for inline `t` tags, or the kind-1985 event for external labels) is
992/// either the author of `event` itself or one of the repository maintainers.
993pub fn get_labels(
994 event: &Event,
995 repo_ref: &RepoRef,
996 all_label_events: &[Event],
997) -> Vec<String> {
998 let is_permitted = |pubkey: &PublicKey| -> bool {
999 pubkey.eq(&event.pubkey) || repo_ref.maintainers.contains(pubkey)
1000 };
1001
1002 // 1. Inline `t` tags on the event itself — only if the event author is
1003 // permitted (they always are, since they authored the event, but we
1004 // keep the check symmetric with the external-label path).
1005 let mut labels: Vec<String> = if is_permitted(&event.pubkey) {
1006 event
1007 .tags
1008 .iter()
1009 .filter(|t| {
1010 let s = t.as_slice();
1011 s.len() >= 2 && s[0].eq("t")
1012 })
1013 .map(|t| t.as_slice()[1].clone())
1014 .collect()
1015 } else {
1016 vec![]
1017 };
1018
1019 // 2. External NIP-32 kind-1985 label events.
1020 //
1021 // A valid label event must:
1022 // - be kind 1985
1023 // - reference `event` via a lowercase `e` tag
1024 // - have `["L", "#t"]` (namespace declaration)
1025 // - have at least one `["l", "<value>", "#t"]` tag
1026 // - be authored by a permitted pubkey
1027 let event_id_str = event.id.to_string();
1028 for label_event in all_label_events {
1029 if !label_event.kind.eq(&KIND_LABEL) {
1030 continue;
1031 }
1032 if !is_permitted(&label_event.pubkey) {
1033 continue;
1034 }
1035 // Must reference our event via a lowercase `e` tag.
1036 let references_event = label_event.tags.iter().any(|t| {
1037 let s = t.as_slice();
1038 s.len() >= 2 && s[0].eq("e") && s[1].eq(&event_id_str)
1039 });
1040 if !references_event {
1041 continue;
1042 }
1043 // Must declare the `#t` namespace.
1044 let has_namespace = label_event.tags.iter().any(|t| {
1045 let s = t.as_slice();
1046 s.len() >= 2 && s[0].eq("L") && s[1].eq("#t")
1047 });
1048 if !has_namespace {
1049 continue;
1050 }
1051 // Collect all `["l", "<value>", "#t"]` labels from this event.
1052 for tag in label_event.tags.iter() {
1053 let s = tag.as_slice();
1054 if s.len() >= 3 && s[0].eq("l") && s[2].eq("#t") && !s[1].is_empty() {
1055 let label = s[1].clone();
1056 if !labels.contains(&label) {
1057 labels.push(label);
1058 }
1059 }
1060 }
1061 }
1062
1063 labels
1064}
1065
978pub fn get_status( 1066pub fn get_status(
979 proposal: &Event, 1067 proposal: &Event,
980 repo_ref: &RepoRef, 1068 repo_ref: &RepoRef,