refactor(hive-c0re): port forge module + knowledge hooks to forgejo-api

This commit is contained in:
müde 2026-07-07 09:11:15 +02:00
commit b8a3927c43
5 changed files with 651 additions and 426 deletions

View file

@ -4,10 +4,12 @@
//! boundary; moved verbatim from the `forge` module root.
use anyhow::Context;
use forgejo_api::ForgejoError;
use forgejo_api::structs::{MergePullRequestOption, MergePullRequestOptionDo};
use crate::coordinator::Coordinator;
use super::{CONFIG_ORG, FORGE_HTTP, core_token, forge_http};
use super::{CONFIG_ORG, api, core_token};
// ---------------------------------------------------------------------------
// PR-based config-flow merge primitives (part of the
@ -242,35 +244,57 @@ pub async fn ff_push_to_main(repo: &str, sha: &str) -> Result<(), ForgeMergeErro
}
/// Mark PR `pr` as **manually merged** at `sha` (Forgejo
/// `POST …/pulls/{pr}/merge` with `Do=manually-merged`, `MergeCommitID=sha`).
/// `repo_merge_pull_request` with `Do=manually-merged`, `MergeCommitID=sha`).
/// `ff_push_to_main` must have already set `main` to `sha` (Forgejo requires
/// the branch already be at the merge commit). On a non-2xx, re-reads the PR
/// head to distinguish drift (`HeadDrift`) from a generic failure (`Other`) —
/// best-effort, since the handler's pre-merge head re-read is the real gate.
/// the branch already be at the merge commit). On an API rejection, re-reads
/// the PR head to distinguish drift (`HeadDrift`) from a generic failure
/// (`Other`) — best-effort, since the handler's pre-merge head re-read is
/// the real gate. A transport failure skips the drift re-read (it couldn't
/// reach the forge either) and surfaces as `Other` directly.
///
/// # Errors
/// `HeadDrift` if the PR head no longer matches `sha`; `Other` otherwise.
pub async fn mark_pr_merged(repo: &str, pr: u64, sha: &str) -> Result<(), ForgeMergeError> {
let token = core_token()
.ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?;
let url = format!("{FORGE_HTTP}/api/v1/repos/{repo}/pulls/{pr}/merge");
let body = format!(r#"{{"Do":"manually-merged","MergeCommitID":"{sha}"}}"#);
let (status, _) = forge_http(reqwest::Method::POST, &url, &token, &body)
let (owner, name) = repo.split_once('/').ok_or_else(|| {
ForgeMergeError::Other(anyhow::anyhow!("forge repo `{repo}` is not owner/name"))
})?;
let index = i64::try_from(pr)
.map_err(|_| ForgeMergeError::Other(anyhow::anyhow!("PR index {pr} overflows i64")))?;
let body = MergePullRequestOption {
r#do: MergePullRequestOptionDo::ManuallyMerged,
merge_commit_id: Some(sha.to_owned()),
merge_message_field: None,
merge_title_field: None,
delete_branch_after_merge: None,
force_merge: None,
head_commit_id: None,
merge_when_checks_succeed: None,
};
let client = api(&token).map_err(ForgeMergeError::Other)?;
match client
.repo_merge_pull_request(owner, name, index, body)
.await
.context("POST pulls/<pr>/merge (manually-merged)")?;
if status.is_success() {
return Ok(());
}
// Best-effort drift detection: if the live head no longer matches `sha`,
// that's a head-drift race; otherwise surface as a hard failure.
match pr_head_sha(repo, pr).await {
Ok(actual) if actual != sha => Err(ForgeMergeError::HeadDrift {
expected: sha.to_string(),
actual,
}),
_ => Err(ForgeMergeError::Other(anyhow::anyhow!(
"mark PR #{pr} in {repo} manually-merged at {sha} failed: HTTP {status}"
))),
{
Ok(()) => Ok(()),
Err(ForgejoError::ReqwestError(e)) => Err(ForgeMergeError::Other(
anyhow::Error::from(e).context("POST pulls/<pr>/merge (manually-merged)"),
)),
Err(e) => {
// Best-effort drift detection: if the live head no longer matches
// `sha`, that's a head-drift race; otherwise surface as a hard
// failure.
match pr_head_sha(repo, pr).await {
Ok(actual) if actual != sha => Err(ForgeMergeError::HeadDrift {
expected: sha.to_string(),
actual,
}),
_ => Err(ForgeMergeError::Other(anyhow::Error::from(e).context(
format!("mark PR #{pr} in {repo} manually-merged at {sha}"),
))),
}
}
}
}