go-scm/forge/prs.go
Virgil b94caf0a9d
Some checks failed
Security Scan / security (push) Failing after 11s
Test / test (push) Failing after 51s
feat(forge): add PR review iterator
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-01 08:18:36 +00:00

160 lines
4.7 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package forge
import (
"bytes"
"iter"
fmt "dappco.re/go/core/scm/internal/ax/fmtx"
json "dappco.re/go/core/scm/internal/ax/jsonx"
"net/http"
"net/url"
"strconv"
"dappco.re/go/core/log"
"dappco.re/go/core/scm/agentci"
forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// MergePullRequest merges a pull request with the given method ("squash", "rebase", "merge").
// Usage: MergePullRequest(...)
func (c *Client) MergePullRequest(owner, repo string, index int64, method string) error {
style := forgejo.MergeStyleMerge
switch method {
case "squash":
style = forgejo.MergeStyleSquash
case "rebase":
style = forgejo.MergeStyleRebase
}
merged, _, err := c.api.MergePullRequest(owner, repo, index, forgejo.MergePullRequestOption{
Style: style,
DeleteBranchAfterMerge: true,
})
if err != nil {
return log.E("forge.MergePullRequest", "failed to merge pull request", err)
}
if !merged {
return log.E("forge.MergePullRequest", fmt.Sprintf("merge returned false for %s/%s#%d", owner, repo, index), nil)
}
return nil
}
// SetPRDraft sets or clears the draft status on a pull request.
// The Forgejo SDK v2.2.0 doesn't expose the draft field on EditPullRequestOption,
// so we use a raw HTTP PATCH request.
// Usage: SetPRDraft(...)
func (c *Client) SetPRDraft(owner, repo string, index int64, draft bool) error {
safeOwner, err := agentci.ValidatePathElement(owner)
if err != nil {
return log.E("forge.SetPRDraft", "invalid owner", err)
}
safeRepo, err := agentci.ValidatePathElement(repo)
if err != nil {
return log.E("forge.SetPRDraft", "invalid repo", err)
}
payload := map[string]bool{"draft": draft}
body, err := json.Marshal(payload)
if err != nil {
return log.E("forge.SetPRDraft", "marshal payload", err)
}
path, err := url.JoinPath(c.url, "api", "v1", "repos", safeOwner, safeRepo, "pulls", strconv.FormatInt(index, 10))
if err != nil {
return log.E("forge.SetPRDraft", "failed to build request path", err)
}
req, err := http.NewRequest(http.MethodPatch, path, bytes.NewReader(body))
if err != nil {
return log.E("forge.SetPRDraft", "create request", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "token "+c.token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return log.E("forge.SetPRDraft", "failed to update draft status", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return log.E("forge.SetPRDraft", fmt.Sprintf("unexpected status %d", resp.StatusCode), nil)
}
return nil
}
// ListPRReviews returns all reviews for a pull request.
// Usage: ListPRReviews(...)
func (c *Client) ListPRReviews(owner, repo string, index int64) ([]*forgejo.PullReview, error) {
var all []*forgejo.PullReview
page := 1
for {
reviews, resp, err := c.api.ListPullReviews(owner, repo, index, forgejo.ListPullReviewsOptions{
ListOptions: forgejo.ListOptions{Page: page, PageSize: 50},
})
if err != nil {
return nil, log.E("forge.ListPRReviews", "failed to list reviews", err)
}
all = append(all, reviews...)
if resp == nil || page >= resp.LastPage {
break
}
page++
}
return all, nil
}
// ListPRReviewsIter returns an iterator over reviews for a pull request.
// Usage: ListPRReviewsIter(...)
func (c *Client) ListPRReviewsIter(owner, repo string, index int64) iter.Seq2[*forgejo.PullReview, error] {
return func(yield func(*forgejo.PullReview, error) bool) {
page := 1
for {
reviews, resp, err := c.api.ListPullReviews(owner, repo, index, forgejo.ListPullReviewsOptions{
ListOptions: forgejo.ListOptions{Page: page, PageSize: 50},
})
if err != nil {
yield(nil, log.E("forge.ListPRReviews", "failed to list reviews", err))
return
}
for _, review := range reviews {
if !yield(review, nil) {
return
}
}
if resp == nil || page >= resp.LastPage {
break
}
page++
}
}
}
// GetCombinedStatus returns the combined commit status for a ref (SHA or branch).
// Usage: GetCombinedStatus(...)
func (c *Client) GetCombinedStatus(owner, repo string, ref string) (*forgejo.CombinedStatus, error) {
status, _, err := c.api.GetCombinedStatus(owner, repo, ref)
if err != nil {
return nil, log.E("forge.GetCombinedStatus", "failed to get combined status", err)
}
return status, nil
}
// DismissReview dismisses a pull request review by ID.
// Usage: DismissReview(...)
func (c *Client) DismissReview(owner, repo string, index, reviewID int64, message string) error {
_, err := c.api.DismissPullReview(owner, repo, index, reviewID, forgejo.DismissPullReviewOptions{
Message: message,
})
if err != nil {
return log.E("forge.DismissReview", "failed to dismiss review", err)
}
return nil
}