feat: Forge client + RepoService with CRUD and actions

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-02-21 15:46:10 +00:00
parent b1862445a0
commit abc8840fa4
4 changed files with 200 additions and 1 deletions

View file

@ -3,10 +3,51 @@ package forge
// Forge is the top-level client for the Forgejo API.
type Forge struct {
client *Client
Repos *RepoService
Issues *IssueService
Pulls *PullService
Orgs *OrgService
Users *UserService
Teams *TeamService
Admin *AdminService
Branches *BranchService
Releases *ReleaseService
Labels *LabelService
Webhooks *WebhookService
Notifications *NotificationService
Packages *PackageService
Actions *ActionsService
Contents *ContentService
Wiki *WikiService
Misc *MiscService
}
// NewForge creates a new Forge client.
func NewForge(url, token string, opts ...Option) *Forge {
c := NewClient(url, token, opts...)
return &Forge{client: c}
f := &Forge{client: c}
f.Repos = newRepoService(c)
// Other services initialised in their respective tasks.
// Stub them here so tests compile:
f.Issues = &IssueService{}
f.Pulls = &PullService{}
f.Orgs = &OrgService{}
f.Users = &UserService{}
f.Teams = &TeamService{}
f.Admin = &AdminService{}
f.Branches = &BranchService{}
f.Releases = &ReleaseService{}
f.Labels = &LabelService{}
f.Webhooks = &WebhookService{}
f.Notifications = &NotificationService{}
f.Packages = &PackageService{}
f.Actions = &ActionsService{}
f.Contents = &ContentService{}
f.Wiki = &WikiService{}
f.Misc = &MiscService{}
return f
}
// Client returns the underlying HTTP client.
func (f *Forge) Client() *Client { return f.client }

74
forge_test.go Normal file
View file

@ -0,0 +1,74 @@
package forge
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"forge.lthn.ai/core/go-forge/types"
)
func TestForge_Good_NewForge(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "tok")
if f.Repos == nil {
t.Fatal("Repos service is nil")
}
if f.Issues == nil {
t.Fatal("Issues service is nil")
}
}
func TestRepoService_Good_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Repository{{Name: "go-forge"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
result, err := f.Repos.List(context.Background(), Params{"org": "core"}, DefaultList)
if err != nil {
t.Fatal(err)
}
if len(result.Items) != 1 || result.Items[0].Name != "go-forge" {
t.Errorf("unexpected result: %+v", result)
}
}
func TestRepoService_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(types.Repository{Name: "go-forge", FullName: "core/go-forge"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
repo, err := f.Repos.Get(context.Background(), Params{"owner": "core", "repo": "go-forge"})
if err != nil {
t.Fatal(err)
}
if repo.Name != "go-forge" {
t.Errorf("got name=%q", repo.Name)
}
}
func TestRepoService_Good_Fork(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(types.Repository{Name: "go-forge", Fork: true})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
repo, err := f.Repos.Fork(context.Background(), "core", "go-forge", "my-org")
if err != nil {
t.Fatal(err)
}
if !repo.Fork {
t.Error("expected fork=true")
}
}

64
repos.go Normal file
View file

@ -0,0 +1,64 @@
package forge
import (
"context"
"forge.lthn.ai/core/go-forge/types"
)
// RepoService handles repository operations.
type RepoService struct {
Resource[types.Repository, types.CreateRepoOption, types.EditRepoOption]
}
func newRepoService(c *Client) *RepoService {
return &RepoService{
Resource: *NewResource[types.Repository, types.CreateRepoOption, types.EditRepoOption](
c, "/api/v1/repos/{owner}/{repo}",
),
}
}
// ListOrgRepos returns all repositories for an organisation.
func (s *RepoService) ListOrgRepos(ctx context.Context, org string) ([]types.Repository, error) {
return ListAll[types.Repository](ctx, s.client, "/api/v1/orgs/"+org+"/repos", nil)
}
// ListUserRepos returns all repositories for the authenticated user.
func (s *RepoService) ListUserRepos(ctx context.Context) ([]types.Repository, error) {
return ListAll[types.Repository](ctx, s.client, "/api/v1/user/repos", nil)
}
// Fork forks a repository. If org is non-empty, forks into that organisation.
func (s *RepoService) Fork(ctx context.Context, owner, repo, org string) (*types.Repository, error) {
body := map[string]string{}
if org != "" {
body["organization"] = org
}
var out types.Repository
err := s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/forks", body, &out)
if err != nil {
return nil, err
}
return &out, nil
}
// Transfer initiates a repository transfer.
func (s *RepoService) Transfer(ctx context.Context, owner, repo string, opts map[string]any) error {
return s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/transfer", opts, nil)
}
// AcceptTransfer accepts a pending repository transfer.
func (s *RepoService) AcceptTransfer(ctx context.Context, owner, repo string) error {
return s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/transfer/accept", nil, nil)
}
// RejectTransfer rejects a pending repository transfer.
func (s *RepoService) RejectTransfer(ctx context.Context, owner, repo string) error {
return s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/transfer/reject", nil, nil)
}
// MirrorSync triggers a mirror sync.
func (s *RepoService) MirrorSync(ctx context.Context, owner, repo string) error {
return s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/mirror-sync", nil, nil)
}

20
services_stub.go Normal file
View file

@ -0,0 +1,20 @@
package forge
// Stub service types — replaced as each service is implemented.
type IssueService struct{}
type PullService struct{}
type OrgService struct{}
type UserService struct{}
type TeamService struct{}
type AdminService struct{}
type BranchService struct{}
type ReleaseService struct{}
type LabelService struct{}
type WebhookService struct{}
type NotificationService struct{}
type PackageService struct{}
type ActionsService struct{}
type ContentService struct{}
type WikiService struct{}
type MiscService struct{}