Some checks failed
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Build and Release / Build Docker Image (push) Blocked by required conditions
Build and Release / Create Release (push) Blocked by required conditions
Build and Release / Lint and Test (push) Successful in 9m45s
Build and Release / Build Binaries (amd64, linux) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin) (push) Has been cancelled
- Replace fmt.Errorf with errors.New where no formatting needed
- Use slices.Sort instead of sort.Slice
- Use slices.Contains instead of manual loops
- Use strings.Cut/bytes.Cut instead of Index functions
- Use min() builtin instead of if statements
- Use range over int for iteration
- Replace interface{} with any
- Use strconv.FormatInt instead of fmt.Sprintf
- Fix gofumpt formatting (extra rules)
- Add SDK exclusions to .golangci.yml for standalone SDK package
- Check errors on ctx.Resp.Write calls
- Remove unused struct fields
- Remove unused function parameters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
284 lines
7.3 KiB
Go
284 lines
7.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package gitea provides a Go SDK for the Gitea API.
|
|
package gitea
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Client is a Gitea API client
|
|
type Client struct {
|
|
baseURL string
|
|
token string
|
|
httpClient *http.Client
|
|
userAgent string
|
|
}
|
|
|
|
// ClientOption is a function that configures the client
|
|
type ClientOption func(*Client)
|
|
|
|
// NewClient creates a new Gitea API client
|
|
func NewClient(baseURL string, opts ...ClientOption) (*Client, error) {
|
|
// Normalize URL
|
|
baseURL = strings.TrimSuffix(baseURL, "/")
|
|
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
|
baseURL = "https://" + baseURL
|
|
}
|
|
|
|
c := &Client{
|
|
baseURL: baseURL,
|
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
|
userAgent: "gitea-sdk-go/1.0",
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(c)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// SetToken sets the API token
|
|
func SetToken(token string) ClientOption {
|
|
return func(c *Client) {
|
|
c.token = token
|
|
}
|
|
}
|
|
|
|
// SetHTTPClient sets a custom HTTP client
|
|
func SetHTTPClient(client *http.Client) ClientOption {
|
|
return func(c *Client) {
|
|
c.httpClient = client
|
|
}
|
|
}
|
|
|
|
// SetUserAgent sets a custom user agent
|
|
func SetUserAgent(ua string) ClientOption {
|
|
return func(c *Client) {
|
|
c.userAgent = ua
|
|
}
|
|
}
|
|
|
|
// APIError represents an API error response
|
|
type APIError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Status int `json:"status"`
|
|
Details map[string]any `json:"details,omitempty"`
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
if e.Code != "" {
|
|
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
|
}
|
|
return e.Message
|
|
}
|
|
|
|
// doRequest performs an HTTP request
|
|
func (c *Client) doRequest(ctx context.Context, method, path string, body any, result any) error {
|
|
fullURL := c.baseURL + path
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
jsonBody, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal body: %w", err)
|
|
}
|
|
bodyReader = bytes.NewReader(jsonBody)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
if c.token != "" {
|
|
req.Header.Set("Authorization", "token "+c.token)
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
var apiErr APIError
|
|
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return &APIError{
|
|
Status: resp.StatusCode,
|
|
Message: string(body),
|
|
}
|
|
}
|
|
apiErr.Status = resp.StatusCode
|
|
return &apiErr
|
|
}
|
|
|
|
if result != nil {
|
|
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
|
return fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// doRequestRaw performs an HTTP request with raw body
|
|
func (c *Client) doRequestRaw(ctx context.Context, method, path string, body io.Reader, contentType string, result any) error {
|
|
fullURL := c.baseURL + path
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
if c.token != "" {
|
|
req.Header.Set("Authorization", "token "+c.token)
|
|
}
|
|
if contentType != "" {
|
|
req.Header.Set("Content-Type", contentType)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
var apiErr APIError
|
|
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return &APIError{
|
|
Status: resp.StatusCode,
|
|
Message: string(body),
|
|
}
|
|
}
|
|
apiErr.Status = resp.StatusCode
|
|
return &apiErr
|
|
}
|
|
|
|
if result != nil {
|
|
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
|
return fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetVersion returns the Gitea server version
|
|
func (c *Client) GetVersion(ctx context.Context) (string, error) {
|
|
var result struct {
|
|
Version string `json:"version"`
|
|
}
|
|
if err := c.doRequest(ctx, "GET", "/api/v1/version", nil, &result); err != nil {
|
|
return "", err
|
|
}
|
|
return result.Version, nil
|
|
}
|
|
|
|
// User represents a Gitea user
|
|
type User struct {
|
|
ID int64 `json:"id"`
|
|
Login string `json:"login"`
|
|
FullName string `json:"full_name"`
|
|
Email string `json:"email"`
|
|
AvatarURL string `json:"avatar_url"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
}
|
|
|
|
// GetCurrentUser returns the authenticated user
|
|
func (c *Client) GetCurrentUser(ctx context.Context) (*User, error) {
|
|
var user User
|
|
if err := c.doRequest(ctx, "GET", "/api/v1/user", nil, &user); err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// Repository represents a Gitea repository
|
|
type Repository struct {
|
|
ID int64 `json:"id"`
|
|
Owner *User `json:"owner"`
|
|
Name string `json:"name"`
|
|
FullName string `json:"full_name"`
|
|
Description string `json:"description"`
|
|
Private bool `json:"private"`
|
|
Fork bool `json:"fork"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
Stars int `json:"stars_count"`
|
|
Forks int `json:"forks_count"`
|
|
CloneURL string `json:"clone_url"`
|
|
HTMLURL string `json:"html_url"`
|
|
}
|
|
|
|
// GetRepository returns a repository by owner and name
|
|
func (c *Client) GetRepository(ctx context.Context, owner, repo string) (*Repository, error) {
|
|
var repository Repository
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s", url.PathEscape(owner), url.PathEscape(repo))
|
|
if err := c.doRequest(ctx, "GET", path, nil, &repository); err != nil {
|
|
return nil, err
|
|
}
|
|
return &repository, nil
|
|
}
|
|
|
|
// Release represents a Gitea release
|
|
type Release struct {
|
|
ID int64 `json:"id"`
|
|
TagName string `json:"tag_name"`
|
|
Name string `json:"name"`
|
|
Body string `json:"body"`
|
|
Draft bool `json:"draft"`
|
|
Prerelease bool `json:"prerelease"`
|
|
PublishedAt time.Time `json:"published_at"`
|
|
Assets []Attachment `json:"assets"`
|
|
}
|
|
|
|
// Attachment represents a release asset
|
|
type Attachment struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
DownloadCount int64 `json:"download_count"`
|
|
DownloadURL string `json:"browser_download_url"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// GetRelease returns a release by tag name
|
|
func (c *Client) GetRelease(ctx context.Context, owner, repo, tag string) (*Release, error) {
|
|
var release Release
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s",
|
|
url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(tag))
|
|
if err := c.doRequest(ctx, "GET", path, nil, &release); err != nil {
|
|
return nil, err
|
|
}
|
|
return &release, nil
|
|
}
|
|
|
|
// ListReleases returns all releases for a repository
|
|
func (c *Client) ListReleases(ctx context.Context, owner, repo string) ([]*Release, error) {
|
|
var releases []*Release
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases", url.PathEscape(owner), url.PathEscape(repo))
|
|
if err := c.doRequest(ctx, "GET", path, nil, &releases); err != nil {
|
|
return nil, err
|
|
}
|
|
return releases, nil
|
|
}
|