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"` } `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"` } // 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 } // 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 } // 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) }