f19b271642
Rebuild the Gitea Codex review bot from the product contract with a Go HTTP service, durable SQL queue, typed Gitea client, isolated runner, and deployment updates. Co-Authored-By: Claude <noreply@anthropic.com>
197 lines
5.8 KiB
Go
197 lines
5.8 KiB
Go
package gitea
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea-codex-bot/internal/config"
|
|
"gitea-codex-bot/internal/domain"
|
|
)
|
|
|
|
type Client struct {
|
|
baseURL, token string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewClient(settings config.Settings) *Client {
|
|
return &Client{baseURL: strings.TrimRight(settings.GiteaBaseURL, "/"), token: settings.GiteaToken, httpClient: &http.Client{Timeout: 20 * time.Second}}
|
|
}
|
|
|
|
func (c *Client) request(ctx context.Context, method, path string, body any) ([]byte, int, error) {
|
|
var reader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
reader = strings.NewReader(string(data))
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reader)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "token "+c.token)
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
data, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
|
if err != nil {
|
|
return nil, resp.StatusCode, err
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return data, resp.StatusCode, fmt.Errorf("gitea returned HTTP %d", resp.StatusCode)
|
|
}
|
|
return data, resp.StatusCode, nil
|
|
}
|
|
|
|
func splitRepo(repo string) (string, string, error) {
|
|
owner, name, ok := strings.Cut(repo, "/")
|
|
if !ok || owner == "" || name == "" {
|
|
return "", "", fmt.Errorf("invalid repository %q", repo)
|
|
}
|
|
return url.PathEscape(owner), url.PathEscape(name), nil
|
|
}
|
|
|
|
func (c *Client) GetPullRequest(ctx context.Context, repo string, number int) (domain.PullRequestContext, error) {
|
|
owner, name, err := splitRepo(repo)
|
|
if err != nil {
|
|
return domain.PullRequestContext{}, err
|
|
}
|
|
data, _, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner, name, number), nil)
|
|
if err != nil {
|
|
return domain.PullRequestContext{}, err
|
|
}
|
|
var p struct {
|
|
HTMLURL string `json:"html_url"`
|
|
Base struct {
|
|
Ref, SHA string
|
|
Repo struct {
|
|
CloneURL string `json:"clone_url"`
|
|
FullName string `json:"full_name"`
|
|
} `json:"repo"`
|
|
} `json:"base"`
|
|
Head struct {
|
|
Ref, SHA string
|
|
Repo struct {
|
|
CloneURL string `json:"clone_url"`
|
|
FullName string `json:"full_name"`
|
|
} `json:"repo"`
|
|
} `json:"head"`
|
|
}
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
return domain.PullRequestContext{}, err
|
|
}
|
|
if p.Base.SHA == "" || p.Head.SHA == "" || p.Base.Repo.CloneURL == "" || p.Head.Repo.CloneURL == "" {
|
|
return domain.PullRequestContext{}, fmt.Errorf("gitea pull request response missing required fields")
|
|
}
|
|
return domain.PullRequestContext{Repo: repo, PRNumber: number, BaseRef: p.Base.Ref, BaseSHA: p.Base.SHA, HeadRef: p.Head.Ref, HeadSHA: p.Head.SHA, CloneURL: p.Head.Repo.CloneURL, BaseCloneURL: p.Base.Repo.CloneURL, HeadCloneURL: p.Head.Repo.CloneURL, HTMLURL: p.HTMLURL, IsFork: p.Base.Repo.FullName != p.Head.Repo.FullName}, nil
|
|
}
|
|
|
|
func (c *Client) GetFileContent(ctx context.Context, repo, path, ref string) (string, bool, error) {
|
|
owner, name, err := splitRepo(repo)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
data, status, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?ref=%s", owner, name, url.PathEscape(path), url.QueryEscape(ref)), nil)
|
|
if err != nil && status == http.StatusNotFound {
|
|
return "", false, nil
|
|
}
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
var p struct {
|
|
Content string `json:"content"`
|
|
Encoding string `json:"encoding"`
|
|
}
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
return "", false, err
|
|
}
|
|
if p.Encoding != "base64" || p.Content == "" {
|
|
return "", false, nil
|
|
}
|
|
decoded, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(p.Content, "\n", ""))
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
return string(decoded), true, nil
|
|
}
|
|
|
|
func (c *Client) PostIssueComment(ctx context.Context, repo string, number int, body string) (int64, error) {
|
|
owner, name, err := splitRepo(repo)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
data, _, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, name, number), map[string]string{"body": body})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var p struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
return 0, err
|
|
}
|
|
return p.ID, nil
|
|
}
|
|
func (c *Client) EditIssueComment(ctx context.Context, repo string, commentID int64, body string) (int64, error) {
|
|
owner, name, err := splitRepo(repo)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
data, _, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", owner, name, commentID), map[string]string{"body": body})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var p struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
return 0, err
|
|
}
|
|
return p.ID, nil
|
|
}
|
|
func (c *Client) GetIssueComments(ctx context.Context, repo string, number int) ([]map[string]any, error) {
|
|
owner, name, err := splitRepo(repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, _, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, name, number), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var p []map[string]any
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
func (c *Client) GetIssueComment(ctx context.Context, repo string, commentID int64) (map[string]any, error) {
|
|
owner, name, err := splitRepo(repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, _, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", owner, name, commentID), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var p map[string]any
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
var _ = strconv.Itoa
|