gitea/sdk/go/client.go
logikonline 4d1d81e8b3
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
fix: resolve all remaining golangci-lint errors
- 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>
2026-01-09 18:00:44 -05:00

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
}