Files
review-bot/gitea/client.go
T
Rodin e234dca474
CI / test (pull_request) Successful in 13s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 1m51s
CI / review (gpt-5-mini, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 2m0s
feat: full file context + patterns-repo support
Major improvements to review quality:

1. Full file context: fetch complete content of all modified files from
   the PR branch and include as reference. This eliminates false-positive
   "missing import" findings since the model sees the entire file.

2. Patterns repo: new --patterns-repo / PATTERNS_REPO flag fetches
   language idiom files from a separate Gitea repo (e.g. rodin/elixir-patterns)
   and includes them as review criteria.

3. Multi-file patterns: --patterns-files / PATTERNS_FILES accepts
   comma-separated file paths to fetch from the patterns repo.

New API methods:
- GetPullRequestFiles: list changed files in a PR
- GetFileContentRef: fetch file content from a specific branch/ref

Prompt changes:
- BuildSystemPrompt now accepts (conventions, patterns)
- BuildUserPrompt now accepts fileContext parameter
- File context displayed before diff for model reference
- Patterns presented as "review criteria" in system prompt

Composite action updated with patterns-repo and patterns-files inputs.
2026-05-01 12:11:49 -07:00

180 lines
5.1 KiB
Go

package gitea
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
// Client interacts with the Gitea API.
type Client struct {
BaseURL string
Token string
HTTP *http.Client
}
// NewClient creates a new Gitea API client.
func NewClient(baseURL, token string) *Client {
return &Client{
BaseURL: strings.TrimRight(baseURL, "/"),
Token: token,
HTTP: &http.Client{},
}
}
// PullRequest holds relevant PR metadata.
type PullRequest struct {
Title string `json:"title"`
Body string `json:"body"`
Head struct {
Sha string `json:"sha"`
Ref string `json:"ref"`
} `json:"head"`
}
// CommitStatus represents a single CI status entry.
type CommitStatus struct {
Status string `json:"status"`
Context string `json:"context"`
Description string `json:"description"`
TargetURL string `json:"target_url"`
}
// ChangedFile represents a file modified in a PR.
type ChangedFile struct {
Filename string `json:"filename"`
Status string `json:"status"`
}
// GetPullRequest fetches PR metadata.
func (c *Client) GetPullRequest(owner, repo string, number int) (*PullRequest, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d", c.BaseURL, owner, repo, number)
body, err := c.doGet(url)
if err != nil {
return nil, fmt.Errorf("fetch PR: %w", err)
}
var pr PullRequest
if err := json.Unmarshal(body, &pr); err != nil {
return nil, fmt.Errorf("parse PR JSON: %w", err)
}
return &pr, nil
}
// GetPullRequestDiff fetches the unified diff for a PR.
func (c *Client) GetPullRequestDiff(owner, repo string, number int) (string, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d.diff", c.BaseURL, owner, repo, number)
body, err := c.doGet(url)
if err != nil {
return "", fmt.Errorf("fetch diff: %w", err)
}
return string(body), nil
}
// GetPullRequestFiles fetches the list of files changed in a PR.
func (c *Client) GetPullRequestFiles(owner, repo string, number int) ([]ChangedFile, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/files", c.BaseURL, owner, repo, number)
body, err := c.doGet(url)
if err != nil {
return nil, fmt.Errorf("fetch PR files: %w", err)
}
var files []ChangedFile
if err := json.Unmarshal(body, &files); err != nil {
return nil, fmt.Errorf("parse PR files JSON: %w", err)
}
return files, nil
}
// GetCommitStatuses fetches CI statuses for a commit SHA.
func (c *Client) GetCommitStatuses(owner, repo, sha string) ([]CommitStatus, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/commits/%s/statuses", c.BaseURL, owner, repo, sha)
body, err := c.doGet(url)
if err != nil {
return nil, fmt.Errorf("fetch commit statuses: %w", err)
}
var statuses []CommitStatus
if err := json.Unmarshal(body, &statuses); err != nil {
return nil, fmt.Errorf("parse statuses JSON: %w", err)
}
return statuses, nil
}
// GetFileContent fetches a file from the default branch of a repo.
func (c *Client) GetFileContent(owner, repo, filepath string) (string, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/raw/%s", c.BaseURL, owner, repo, filepath)
body, err := c.doGet(url)
if err != nil {
return "", fmt.Errorf("fetch file %s: %w", filepath, err)
}
return string(body), nil
}
// GetFileContentRef fetches a file from a specific ref (branch/tag/sha) in a repo.
func (c *Client) GetFileContentRef(owner, repo, filepath, ref string) (string, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/raw/%s?ref=%s", c.BaseURL, owner, repo, filepath, ref)
body, err := c.doGet(url)
if err != nil {
return "", fmt.Errorf("fetch file %s@%s: %w", filepath, ref, err)
}
return string(body), nil
}
// PostReview submits a review to a PR.
// event should be "APPROVED" or "REQUEST_CHANGES".
func (c *Client) PostReview(owner, repo string, number int, event, body string) error {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/reviews", c.BaseURL, owner, repo, number)
payload := struct {
Body string `json:"body"`
Event string `json:"event"`
}{
Body: body,
Event: event,
}
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal review payload: %w", err)
}
req, err := http.NewRequest("POST", url, strings.NewReader(string(data)))
if err != nil {
return fmt.Errorf("create review request: %w", err)
}
req.Header.Set("Authorization", "token "+c.Token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTP.Do(req)
if err != nil {
return fmt.Errorf("post review: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("post review failed (status %d): %s", resp.StatusCode, string(respBody))
}
return nil
}
func (c *Client) doGet(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "token "+c.Token)
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
return io.ReadAll(resp.Body)
}