diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index 2a97547a..5f76608c 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -105,18 +105,6 @@ async fn fetch_json( resp.json().await.ok() } -/// Map a Forgejo notification `subject.type` to a human-readable label. -/// Known values: "Pull", "Issue", "Commit", "Repository". Any unknown -/// type is passed through as-is so new Forgejo types degrade gracefully -/// rather than silently collapsing into a generic label. -fn notif_type_label(t: &str) -> &str { - match t { - "Pull" => "PR", - "Issue" => "issue", - other => other, - } -} - /// Truncate a string to `max` bytes at a char boundary, appending `…` if cut. fn truncate(s: &str, max: usize) -> String { if s.len() <= max { @@ -170,7 +158,11 @@ async fn format_notification( .unwrap_or("") .trim(); - let kind = format!("comment on {}", notif_type_label(notif_type)); + let kind = match notif_type { + "Pull Request" => "comment on PR", + "Issue" => "comment on issue", + _ => "comment", + }; let mut out = format!( "[{kind}] {title}\nrepo: {repo}\nurl: {}\n\n{author}: {}\n", if comment_html_url.is_empty() { html_url } else { comment_html_url }, @@ -182,41 +174,40 @@ async fn format_notification( out } else { // Notification triggered by creation or state change of the subject. - // - // Classification uses notif["subject"]["state"] directly — Forgejo - // returns "open" / "closed" / "merged" here. We do NOT rely on - // fetching the PR/issue detail for `merged`: - // - `subject.url` points to the *issues* endpoint, which returns - // `pull_request.merged`, not top-level `merged`. - // - Forgejo API type is "Pull" / "Issue", never "Pull Request". - let notif_state = notif["subject"]["state"].as_str().unwrap_or(""); + let subject = fetch_json(client, subject_api_url, token).await; + let author = subject + .as_ref() + .and_then(|s| s["user"]["login"].as_str()) + .unwrap_or("?"); + let body = subject + .as_ref() + .and_then(|s| s["body"].as_str()) + .unwrap_or("") + .trim(); + let state = subject + .as_ref() + .and_then(|s| s["state"].as_str()) + .unwrap_or(""); + let merged = subject + .as_ref() + .and_then(|s| s["merged"].as_bool()) + .unwrap_or(false); - let label = notif_type_label(notif_type); - let kind = match notif_state { - "merged" => format!("{label} merged"), - "closed" => format!("{label} closed"), - "open" | "" => format!("new {label}"), - other => format!("{label}: {other}"), + let kind = match (notif_type, state, merged) { + ("Pull Request", "closed", true) => "PR merged".to_owned(), + ("Pull Request", "closed", false) => "PR closed".to_owned(), + ("Pull Request", _, _) => "new PR".to_owned(), + ("Issue", "closed", _) => "issue closed".to_owned(), + ("Issue", _, _) => "new issue".to_owned(), + _ => format!("new {notif_type}"), }; - // Fetch subject only for body/author on new (open) items — not - // worth an extra HTTP round-trip for already-closed/merged ones. - let is_open = notif_state == "open" || notif_state.is_empty(); let mut out = format!("[{kind}] {title}\nrepo: {repo}\nurl: {html_url}"); - if is_open { - let subject = fetch_json(client, subject_api_url, token).await; - let author = subject - .as_ref() - .and_then(|s| s["user"]["login"].as_str()) - .unwrap_or("?"); - let body = subject - .as_ref() - .and_then(|s| s["body"].as_str()) - .unwrap_or("") - .trim(); - if !body.is_empty() { - out.push_str(&format!("\n\n{author}: {}", truncate(body, BODY_TRUNCATE))); - } + if !body.is_empty() && !state.contains("closed") && !merged { + out.push_str(&format!( + "\n\n{author}: {}", + truncate(body, BODY_TRUNCATE) + )); } out }