Watch
0
0
Fork
You've already forked hyperhive
0

comment once per pr and agent while red, clear the marker on a green build

This commit is contained in:
atlas 2026-10-26 22:10:07 +01:00 • committed by mara
commit 9fc0e3bf90

View file

@ -1,28 +1,17 @@
//! Comments on a pull request, posted once per marker.
use anyhow::{Context, Result};
use forgejo_api::structs::{CreateIssueCommentOption, IssueGetCommentsQuery};
use forgejo_api::structs::{
Comment, CreateIssueCommentOption, EditIssueCommentOption, IssueGetCommentsQuery,
};
use super::Client;
impl Client {
/// Comment `body` on pull request `pr` of `owner/repo` unless a comment
/// there already contains `marker`. Whether a comment was posted.
///
/// The marker is what the comment carries, so the check holds across a
/// restart and across every caller. It reads the comments first, so two
/// concurrent calls with one marker can both post.
pub(crate) async fn comment_once(
&self,
owner: &str,
repo: &str,
pr: u64,
marker: &str,
body: &str,
) -> Result<bool> {
/// Every comment on pull request `pr` of `owner/repo`.
async fn comments(&self, owner: &str, repo: &str, pr: u64) -> Result<Vec<Comment>> {
let index = i64::try_from(pr).context("PR number out of range")?;
let existing = self
.api
self.api
.call(|api| {
Box::pin(
api.issue_get_comments(
@ -38,7 +27,25 @@ impl Client {
)
})
.await
.with_context(|| format!("listing the comments on {owner}/{repo}#{pr}"))?;
.with_context(|| format!("listing the comments on {owner}/{repo}#{pr}"))
}
/// Comment `body` on pull request `pr` of `owner/repo` unless a comment
/// there already contains `marker`. Whether a comment was posted.
///
/// The marker is what the comment carries, so the check holds across a
/// restart and across every caller. It reads the comments first, so two
/// concurrent calls with one marker can both post.
pub(crate) async fn comment_once(
&self,
owner: &str,
repo: &str,
pr: u64,
marker: &str,
body: &str,
) -> Result<bool> {
let index = i64::try_from(pr).context("PR number out of range")?;
let existing = self.comments(owner, repo, pr).await?;
if existing
.iter()
.any(|c| c.body.as_deref().is_some_and(|b| b.contains(marker)))
@ -58,6 +65,42 @@ impl Client {
.with_context(|| format!("commenting on {owner}/{repo}#{pr}"))?;
Ok(true)
}
/// Rewrite every comment on pull request `pr` of `owner/repo` that
/// contains `marker` so it contains `cleared` instead, which lets
/// [`Self::comment_once`] post that marker again. How many comments were
/// rewritten.
pub(crate) async fn clear_marker(
&self,
owner: &str,
repo: &str,
pr: u64,
marker: &str,
cleared: &str,
) -> Result<usize> {
let mut rewritten = 0;
for comment in self.comments(owner, repo, pr).await? {
let (Some(id), Some(body)) = (comment.id, comment.body.as_deref()) else {
continue;
};
if !body.contains(marker) {
continue;
}
let option = EditIssueCommentOption {
body: body.replace(marker, cleared),
updated_at: None,
};
self.api
.call(|api| {
api.issue_edit_comment(owner, repo, id, option.clone())
.into_future()
})
.await
.with_context(|| format!("clearing comment {id} on {owner}/{repo}#{pr}"))?;
rewritten += 1;
}
Ok(rewritten)
}
}
#[cfg(test)]
@ -87,8 +130,8 @@ pub(crate) mod tests {
"GET" => {
let list: Vec<_> = held
.iter()
.filter(|(p, _)| *p == path)
.enumerate()
.filter(|(_, (p, _))| *p == path)
.map(
|(id, (_, body))| serde_json::json!({ "id": id, "body": body }),
)
@ -100,6 +143,16 @@ pub(crate) mod tests {
serde_json::Value::from(list).to_string(),
)
}
"PATCH" => {
let sent: serde_json::Value = serde_json::from_slice(&body).unwrap();
let id: usize = path.rsplit('/').next().unwrap().parse().unwrap();
held[id].1 = sent["body"].as_str().unwrap().to_owned();
(
StatusCode::OK,
[("x-total-count", String::new())],
serde_json::json!({ "id": id, "body": held[id].1 }).to_string(),
)
}
"POST" => {
let sent: serde_json::Value = serde_json::from_slice(&body).unwrap();
let text = sent["body"].as_str().unwrap().to_owned();
@ -107,7 +160,8 @@ pub(crate) mod tests {
(
StatusCode::CREATED,
[("x-total-count", String::new())],
serde_json::json!({ "id": held.len(), "body": text }).to_string(),
serde_json::json!({ "id": held.len() - 1, "body": text })
.to_string(),
)
}
_ => (
@ -147,4 +201,39 @@ pub(crate) mod tests {
format!("/api/v1/repos/o/r/{}/7/comments", "issues")
);
}
#[tokio::test]
async fn a_cleared_marker_can_be_posted_again() {
let (client, held) = stand_in().await;
let (marker, cleared) = ("<!-- a -->", "<!-- a cleared -->");
assert!(
client
.comment_once("o", "r", 7, marker, marker)
.await
.unwrap()
);
assert_eq!(
client
.clear_marker("o", "r", 7, marker, cleared)
.await
.unwrap(),
1
);
assert_eq!(
client
.clear_marker("o", "r", 7, marker, cleared)
.await
.unwrap(),
0
);
assert!(
client
.comment_once("o", "r", 7, marker, marker)
.await
.unwrap()
);
let held = held.lock().unwrap();
assert_eq!(held[0].1, cleared);
assert_eq!(held[1].1, marker);
}
}