// Package gitea provides a Gitea API adapter implementing port.GitRepository. // // Context Propagation Note: // The Gitea Go SDK (code.gitea.io/sdk/gitea) does not natively support context // propagation for HTTP requests. Methods accept context.Context for interface // compatibility and future-proofing, but the underlying SDK calls do not use it // for cancellation or timeouts. If cancellation is critical, consider using a // context-aware HTTP transport or wrapping calls with context deadline checks. package gitea import ( "context" "fmt" "code.gitea.io/sdk/gitea" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" ) // Ensure Client implements GitRepository. var _ port.GitRepository = (*Client)(nil) // Client is a Gitea API client adapter. type Client struct { client *gitea.Client defaultOwner string // default organization/user for new repos } // SDKClient returns the underlying Gitea SDK client. // Used by the template provider to create files in repos. func (c *Client) SDKClient() *gitea.Client { return c.client } // NewClient creates a new Gitea client. // url is the Gitea server URL (e.g., https://git.threesix.ai) // token is an API access token with repo permissions // defaultOwner is the organization or user to create repos under func NewClient(url, token, defaultOwner string) (*Client, error) { client, err := gitea.NewClient(url, gitea.SetToken(token)) if err != nil { return nil, fmt.Errorf("failed to create gitea client: %w", err) } return &Client{ client: client, defaultOwner: defaultOwner, }, nil } // CreateRepo creates a new git repository under the default owner. func (c *Client) CreateRepo(ctx context.Context, name, description string, private bool) (*domain.Repo, error) { opts := gitea.CreateRepoOption{ Name: name, Description: description, Private: private, AutoInit: false, // Empty repo - template seeding will create all files DefaultBranch: "main", } var repo *gitea.Repository var err error // Try to create as org repo first, fall back to user repo repo, _, err = c.client.CreateOrgRepo(c.defaultOwner, opts) if err != nil { // May not be an org, try as user repo repo, _, err = c.client.CreateRepo(opts) if err != nil { return nil, fmt.Errorf("failed to create repo: %w", err) } } return repoFromGitea(repo), nil } // DeleteRepo deletes a repository. func (c *Client) DeleteRepo(ctx context.Context, owner, name string) error { _, err := c.client.DeleteRepo(owner, name) if err != nil { return fmt.Errorf("failed to delete repo %s/%s: %w", owner, name, err) } return nil } // ListRepos returns all repositories for an owner. func (c *Client) ListRepos(ctx context.Context, owner string) ([]*domain.Repo, error) { // Try as organization first repos, _, err := c.client.ListOrgRepos(owner, gitea.ListOrgReposOptions{ ListOptions: gitea.ListOptions{PageSize: 100}, }) if err != nil { // Try as user repos, _, err = c.client.ListUserRepos(owner, gitea.ListReposOptions{ ListOptions: gitea.ListOptions{PageSize: 100}, }) if err != nil { return nil, fmt.Errorf("failed to list repos for %s: %w", owner, err) } } result := make([]*domain.Repo, len(repos)) for i, r := range repos { result[i] = repoFromGitea(r) } return result, nil } // GetRepo returns a single repository. func (c *Client) GetRepo(ctx context.Context, owner, name string) (*domain.Repo, error) { repo, _, err := c.client.GetRepo(owner, name) if err != nil { return nil, fmt.Errorf("failed to get repo %s/%s: %w", owner, name, err) } return repoFromGitea(repo), nil } // AddCollaborator adds a user as collaborator to a repo. func (c *Client) AddCollaborator(ctx context.Context, owner, repo, username string, permission string) error { var accessMode gitea.AccessMode switch permission { case "read": accessMode = gitea.AccessModeRead case "write": accessMode = gitea.AccessModeWrite case "admin": accessMode = gitea.AccessModeAdmin default: accessMode = gitea.AccessModeRead } _, err := c.client.AddCollaborator(owner, repo, username, gitea.AddCollaboratorOption{ Permission: &accessMode, }) if err != nil { return fmt.Errorf("failed to add collaborator %s to %s/%s: %w", username, owner, repo, err) } return nil } // RemoveCollaborator removes a collaborator from a repo. func (c *Client) RemoveCollaborator(ctx context.Context, owner, repo, username string) error { _, err := c.client.DeleteCollaborator(owner, repo, username) if err != nil { return fmt.Errorf("failed to remove collaborator %s from %s/%s: %w", username, owner, repo, err) } return nil } // AddDeployKey adds a deploy key to a repo. func (c *Client) AddDeployKey(ctx context.Context, owner, repo, title, publicKey string, readOnly bool) (*domain.DeployKey, error) { key, _, err := c.client.CreateDeployKey(owner, repo, gitea.CreateKeyOption{ Title: title, Key: publicKey, ReadOnly: readOnly, }) if err != nil { return nil, fmt.Errorf("failed to add deploy key to %s/%s: %w", owner, repo, err) } return &domain.DeployKey{ ID: key.ID, Title: key.Title, PublicKey: key.Key, ReadOnly: key.ReadOnly, CreatedAt: key.Created, }, nil } // DeleteDeployKey removes a deploy key from a repo. func (c *Client) DeleteDeployKey(ctx context.Context, owner, repo string, keyID int64) error { _, err := c.client.DeleteDeployKey(owner, repo, keyID) if err != nil { return fmt.Errorf("failed to delete deploy key %d from %s/%s: %w", keyID, owner, repo, err) } return nil } // CreateWebhook creates a webhook on a repository. func (c *Client) CreateWebhook(ctx context.Context, owner, repo, url, secret string, events []string) (*domain.RepoWebhook, error) { hook, _, err := c.client.CreateRepoHook(owner, repo, gitea.CreateHookOption{ Type: gitea.HookTypeGitea, Config: map[string]string{ "url": url, "content_type": "json", "secret": secret, }, Events: events, Active: true, }) if err != nil { return nil, fmt.Errorf("failed to create webhook on %s/%s: %w", owner, repo, err) } return &domain.RepoWebhook{ ID: hook.ID, URL: hook.Config["url"], Secret: secret, Events: hook.Events, Active: hook.Active, HookType: string(hook.Type), }, nil } // DeleteWebhook removes a webhook from a repo. func (c *Client) DeleteWebhook(ctx context.Context, owner, repo string, webhookID int64) error { _, err := c.client.DeleteRepoHook(owner, repo, webhookID) if err != nil { return fmt.Errorf("failed to delete webhook %d from %s/%s: %w", webhookID, owner, repo, err) } return nil } // repoFromGitea converts a gitea.Repository to domain.Repo. func repoFromGitea(r *gitea.Repository) *domain.Repo { return &domain.Repo{ ID: r.ID, Owner: r.Owner.UserName, Name: r.Name, FullName: r.FullName, Description: r.Description, Private: r.Private, CloneSSH: r.SSHURL, CloneHTTP: r.CloneURL, HTMLURL: r.HTMLURL, CreatedAt: r.Created, UpdatedAt: r.Updated, } }