forge_notify: show assignees and reviewers unconditionally (closes #256)

This commit is contained in:
damocles 2026-05-22 16:44:47 +02:00 committed by Mara
parent 9b9277db78
commit 845fafdf1b

View file

@ -199,10 +199,13 @@ fn review_state_label(state: &str) -> Option<&str> {
/// and should be silently discarded (and marked read by the caller).
///
/// Formats:
/// - Comment: `[comment on PR #N repo] title\nurl: ...\n\nauthor: body`
/// - Review: `[PR approved #N repo] title\nurl: ...\n\nreviewer: body`
/// - Comment: `[comment on PR #N repo] title\nurl: ...\n\nauthor: body\nassignee: user`
/// - Review: `[PR approved #N repo] title\nurl: ...\n\nreviewer: body\nassignee: user`
/// - New item: `[new issue #N repo] title\nurl: ...\nassignee: user`
/// - State: `[PR merged #N repo] title\nurl: ...`
/// - State: `[PR merged #N repo] title\nurl: ...\nassignee: user`
///
/// Assignees (and, for PRs, requested_reviewers) are appended unconditionally
/// on all issue/PR notifications (closes #256).
///
/// Number is extracted from `html_url` last path segment before any `#`.
/// Repo slug (`owner/name`) is always included — agents may watch multiple repos.
@ -241,6 +244,49 @@ async fn format_notification(
.as_str()
.unwrap_or("");
// Always fetch subject detail for assignee/reviewer metadata (#256).
// Keeps agents informed of current ownership without a follow-up fetch.
let subject = if !subject_api_url.is_empty() {
fetch_json(client, subject_api_url, token).await
} else {
None
};
let is_pr = matches!(notif_type, "Pull Request" | "Pull");
// Build assignee + reviewer suffix appended to all notification kinds.
let meta_suffix = {
let assignees: Vec<&str> = subject
.as_ref()
.and_then(|s| s["assignees"].as_array())
.map(|arr| arr.iter().filter_map(|a| a["login"].as_str()).collect())
.unwrap_or_default();
let assignee_line = if assignees.is_empty() {
"assignee: unassigned".to_owned()
} else {
format!("assignee: {}", assignees.join(", "))
};
// For PRs, include requested_reviewers when present.
let reviewer_line = if is_pr {
let reviewers: Vec<&str> = subject
.as_ref()
.and_then(|s| s["requested_reviewers"].as_array())
.map(|arr| arr.iter().filter_map(|r| r["login"].as_str()).collect())
.unwrap_or_default();
if reviewers.is_empty() {
None
} else {
Some(format!("reviewer: {}", reviewers.join(", ")))
}
} else {
None
};
match reviewer_line {
Some(r) => format!("\n{assignee_line}\n{r}"),
None => format!("\n{assignee_line}"),
}
};
// Determine whether this notification was triggered by a comment/review or
// by creation/state-change of the subject itself.
let has_comment = !comment_api_url.is_empty() && comment_api_url != subject_api_url;
@ -287,14 +333,19 @@ async fn format_notification(
} else {
out.push_str(&format!("\n\nreviewer: {author}"));
}
out.push_str(&meta_suffix);
Some(out)
} else {
// Regular comment.
let kind = format!("comment on {}{num}{repo}", notif_type_label(notif_type));
let mut out = format!("[{kind}] {title}\nurl: {url}\n\n{author}: {}", truncate(body_text, BODY_TRUNCATE));
let mut out = format!(
"[{kind}] {title}\nurl: {url}\n\n{author}: {}",
truncate(body_text, BODY_TRUNCATE)
);
if out.ends_with('\n') {
out.pop();
}
out.push_str(&meta_suffix);
Some(out)
}
} else {
@ -328,27 +379,7 @@ async fn format_notification(
};
let mut out = format!("[{kind}] {title}\nurl: {html_url}");
// For new (open) items: fetch assignee(s). Skip body — agents can
// run `hive-forge view <n>` for full content. One extra API call
// only on creation events, not state changes.
if is_new {
let subject = fetch_json(client, subject_api_url, token).await;
let assignees: Vec<&str> = subject
.as_ref()
.and_then(|s| s["assignees"].as_array())
.map(|arr| {
arr.iter()
.filter_map(|a| a["login"].as_str())
.collect()
})
.unwrap_or_default();
if assignees.is_empty() {
out.push_str("\nassignee: unassigned");
} else {
out.push_str(&format!("\nassignee: {}", assignees.join(", ")));
}
}
out.push_str(&meta_suffix);
Some(out)
}
}