refactor(hive-forge): port CLI verbs to forgejo-api

This commit is contained in:
müde 2026-07-07 09:24:53 +02:00
commit 4636987469
36 changed files with 1463 additions and 1153 deletions

View file

@ -13,12 +13,12 @@
//! `forge cli: agents keep commenting without reading prev comments`.)
use anyhow::Result;
use serde_json::Value;
use forgejo_api::structs::{NotifyGetRepoListQuery, NotifyReadThreadQuery};
use crate::client::Client;
use crate::client::{Client, index, split_repo};
/// Forgejo's per-page notification cap.
const PAGE_SIZE: u64 = 50;
const PAGE_SIZE: u32 = 50;
/// How many pages of unread notifications to scan for the thread.
/// Notifications come newest-first and a thread the caller is about to
/// comment on was just active, so it sits near the top; this cap keeps
@ -26,7 +26,7 @@ const PAGE_SIZE: u64 = 50;
/// firehose), at the cost of not detecting a match buried past
/// `MAX_PAGES * PAGE_SIZE` unread items (degrade-open — acceptable for
/// a courtesy guard).
const MAX_PAGES: u64 = 5;
const MAX_PAGES: u32 = 5;
/// The notification thread id of an UNREAD notification on
/// `<repo>#<number>`, or `None` when the thread has no unread
@ -34,24 +34,30 @@ const MAX_PAGES: u64 = 5;
/// number can't false-match another repo. Errors only on transport /
/// non-2xx — callers degrade open on `Err`.
pub fn unread_thread_id(client: &Client, repo: &str, number: u64) -> Result<Option<u64>> {
let (owner, name) = split_repo(repo)?;
for page in 1..=MAX_PAGES {
let v = client.get_json(&format!(
"/repos/{repo}/notifications?all=false&page={page}&limit={PAGE_SIZE}"
))?;
let arr = v.as_array().cloned().unwrap_or_default();
let len = arr.len() as u64;
for n in &arr {
let query = NotifyGetRepoListQuery {
all: Some(false),
..Default::default()
};
let (_, threads) = client
.api()
.notify_get_repo_list(owner, name, query)
.page(page)
.page_size(PAGE_SIZE)
.send()?;
for n in &threads {
let subject_url = n
.get("subject")
.and_then(|s| s.get("url"))
.and_then(Value::as_str)
.unwrap_or("");
.subject
.as_ref()
.and_then(|s| s.url.as_ref())
.map_or("", url::Url::as_str);
if subject_matches(subject_url, number) {
return Ok(n.get("id").and_then(Value::as_u64));
return Ok(n.id.and_then(|id| u64::try_from(id).ok()));
}
}
// Last (short) page reached — stop.
if len < PAGE_SIZE {
if threads.len() < PAGE_SIZE as usize {
break;
}
}
@ -62,7 +68,11 @@ pub fn unread_thread_id(client: &Client, repo: &str, number: u64) -> Result<Opti
/// result). Clears the unread signal so a subsequent comment isn't
/// blocked after the agent has read the thread.
pub fn mark_thread_read(client: &Client, thread_id: u64) -> Result<()> {
client.patch_no_content(&format!("/notifications/threads/{thread_id}"))
client
.api()
.notify_read_thread(index(thread_id)?, NotifyReadThreadQuery::default())
.send()?;
Ok(())
}
/// Reading a thread (`comments` / `view`) is the "I've seen it" signal: