138 lines
4.4 KiB
Go
138 lines
4.4 KiB
Go
// Package gitea is a minimal Gitea REST client for the three calls spx needs:
|
|
// create issue, get/update issue body, create issue comment.
|
|
package gitea
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Client talks to a Gitea instance using a personal access token.
|
|
type Client struct {
|
|
BaseURL string // host without trailing slash, e.g. "https://gitea.example.com"
|
|
Token string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// New builds a Client with sane defaults.
|
|
func New(baseURL, token string) *Client {
|
|
return &Client{
|
|
BaseURL: strings.TrimRight(baseURL, "/"),
|
|
Token: token,
|
|
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// Issue is the subset of Gitea's Issue object spx cares about.
|
|
type Issue struct {
|
|
Number int `json:"number"`
|
|
Body string `json:"body"`
|
|
HTMLURL string `json:"html_url"`
|
|
}
|
|
|
|
// Comment is the subset of Gitea's Comment object spx cares about.
|
|
type Comment struct {
|
|
ID int64 `json:"id"`
|
|
HTMLURL string `json:"html_url"`
|
|
Body string `json:"body"`
|
|
}
|
|
|
|
// do executes an HTTP request with auth + JSON content type and decodes the
|
|
// response into out (if non-nil and the body is non-empty).
|
|
func (c *Client) do(method, path string, payload any, out any) error {
|
|
var body io.Reader
|
|
if payload != nil {
|
|
buf, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("编码请求体失败: %w", err)
|
|
}
|
|
body = bytes.NewReader(buf)
|
|
}
|
|
url := c.BaseURL + path
|
|
req, err := http.NewRequest(method, url, body)
|
|
if err != nil {
|
|
return fmt.Errorf("构造请求失败: %w", err)
|
|
}
|
|
req.Header.Set("Authorization", "token "+c.Token)
|
|
req.Header.Set("Accept", "application/json")
|
|
if payload != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("%s %s 请求失败: %w", method, url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
respBytes, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("%s %s 返回 %d: %s", method, url, resp.StatusCode, strings.TrimSpace(string(respBytes)))
|
|
}
|
|
if out == nil || len(respBytes) == 0 {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(respBytes, out); err != nil {
|
|
return fmt.Errorf("解析响应失败 (%s %s): %w; body=%s", method, url, err, string(respBytes))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateIssue creates a new issue and returns the created object.
|
|
func (c *Client) CreateIssue(owner, repo, title, body string) (*Issue, error) {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner, repo)
|
|
payload := map[string]any{"title": title, "body": body}
|
|
var out Issue
|
|
if err := c.do(http.MethodPost, path, payload, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// GetIssue fetches an existing issue (or PR — Gitea treats PRs as issues for
|
|
// metadata purposes).
|
|
func (c *Client) GetIssue(owner, repo string, number int) (*Issue, error) {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner, repo, number)
|
|
var out Issue
|
|
if err := c.do(http.MethodGet, path, nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// UpdateIssueBody patches an issue's body field.
|
|
func (c *Client) UpdateIssueBody(owner, repo string, number int, body string) error {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner, repo, number)
|
|
payload := map[string]any{"body": body}
|
|
return c.do(http.MethodPatch, path, payload, nil)
|
|
}
|
|
|
|
// ListIssueComments returns the issue/PR comments in Gitea's default order
|
|
// (ascending by creation time). Used by `spx issue state get|merge` to find
|
|
// the last comment, which is where the state JSON blob lives.
|
|
func (c *Client) ListIssueComments(owner, repo string, number int) ([]Comment, error) {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, number)
|
|
var out []Comment
|
|
if err := c.do(http.MethodGet, path, nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// CreateIssueComment posts a comment on an issue or PR.
|
|
//
|
|
// In Gitea, PR comments (not review threads) live under the same endpoint as
|
|
// issue comments — pass the PR number as the issue number.
|
|
func (c *Client) CreateIssueComment(owner, repo string, number int, body string) (*Comment, error) {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, number)
|
|
payload := map[string]any{"body": body}
|
|
var out Comment
|
|
if err := c.do(http.MethodPost, path, payload, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|