package forgejo import ( "encoding/base64" "fmt" "net/http" "time" "github.com/go-resty/resty/v2" ) type Client struct { client *resty.Client token string baseURL string } type Secret struct { Name string `json:"name"` Data string `json:"data"` } type MigrateRequest struct { CloneAddr string `json:"clone_addr"` AuthUsername string `json:"auth_username"` AuthPassword string `json:"auth_password"` UID int `json:"uid"` RepoName string `json:"repo_name"` RepoOwner string `json:"repo_owner"` Mirror bool `json:"mirror"` LFS bool `json:"lfs"` Interval string `json:"interval"` Description string `json:"description,omitempty"` Private bool `json:"private"` } type Repository struct { ID int64 `json:"id"` Name string `json:"name"` FullName string `json:"full_name"` HTMLURL string `json:"html_url"` CloneURL string `json:"clone_url"` Mirror bool `json:"mirror"` } type User struct { ID int64 `json:"id"` UserName string `json:"login"` } type APIError struct { Message string `json:"message"` URL string `json:"url"` } func NewClient(baseURL, token string) *Client { c := resty.New() c.SetTimeout(60 * time.Second) // Longer timeout for migration c.SetHeader("Authorization", "token "+token) c.SetHeader("Content-Type", "application/json") c.SetBaseURL(baseURL + "/api/v1") return &Client{ client: c, token: token, baseURL: baseURL, } } func (c *Client) GetUser(username string) (*User, error) { var user User resp, err := c.client.R(). SetResult(&user). Get("/users/" + username) if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } if resp.StatusCode() != http.StatusOK { return nil, fmt.Errorf("failed to get user: status %d - %s", resp.StatusCode(), resp.String()) } return &user, nil } func (c *Client) RepositoryExists(owner, name string) (bool, error) { resp, err := c.client.R(). Head(fmt.Sprintf("/repos/%s/%s", owner, name)) if err != nil { return false, err } if resp.StatusCode() == http.StatusOK { return true, nil } else if resp.StatusCode() == http.StatusNotFound { return false, nil } return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) } func (c *Client) CreateMirror(req MigrateRequest) (*Repository, error) { var repo Repository var apiErr APIError resp, err := c.client.R(). SetBody(req). SetResult(&repo). SetError(&apiErr). Post("/repos/migrate") if err != nil { return nil, fmt.Errorf("API request failed: %w", err) } if resp.StatusCode() != http.StatusCreated && resp.StatusCode() != http.StatusOK { if apiErr.Message != "" { return nil, fmt.Errorf("Forgejo API error: %s (status %d)", apiErr.Message, resp.StatusCode()) } return nil, fmt.Errorf("Forgejo API returned status %d: %s", resp.StatusCode(), resp.String()) } return &repo, nil } func (c *Client) DeleteRepository(owner, name string) error { resp, err := c.client.R(). Delete(fmt.Sprintf("/repos/%s/%s", owner, name)) if err != nil { return err } if resp.StatusCode() != http.StatusNoContent && resp.StatusCode() != http.StatusOK { return fmt.Errorf("failed to delete repository: status %d", resp.StatusCode()) } return nil } // CreateSecret creates a repository secret for Actions func (c *Client) CreateSecret(owner, repo, name, data string) error { // Forgejo/Gitea API for secrets url := fmt.Sprintf("/repos/%s/%s/actions/secrets/%s", owner, repo, name) body := map[string]string{ "data": data, } resp, err := c.client.R(). SetBody(body). Put(url) if err != nil { return fmt.Errorf("failed to create secret: %w", err) } if resp.StatusCode() != 201 && resp.StatusCode() != 204 { return fmt.Errorf("failed to create secret, status: %d", resp.StatusCode()) } return nil } // CreateFile creates a file in the repository (for workflow files) func (c *Client) CreateFile(owner, repo, path, content, message string) error { // Content needs to be base64 encoded encoded := base64.StdEncoding.EncodeToString([]byte(content)) body := map[string]interface{}{ "content": encoded, "message": message, } url := fmt.Sprintf("/repos/%s/%s/contents/%s", owner, repo, path) resp, err := c.client.R(). SetBody(body). Post(url) if err != nil { return err } // 201 = created, 422 = already exists (should handle update in production) if resp.StatusCode() != 201 { if resp.StatusCode() == 422 { return fmt.Errorf("file %s already exists", path) } return fmt.Errorf("failed to create file, status %d: %s", resp.StatusCode(), resp.String()) } return nil }