azure_migrate/internal/forgejo/client.go

196 lines
4.5 KiB
Go

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
}