feat: BranchService and ReleaseService with protections and assets
Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c6e9d83b08
commit
fd0343e47e
6 changed files with 313 additions and 4 deletions
63
branches.go
Normal file
63
branches.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// BranchService handles branch operations within a repository.
|
||||
type BranchService struct {
|
||||
Resource[types.Branch, types.CreateBranchRepoOption, struct{}]
|
||||
}
|
||||
|
||||
func newBranchService(c *Client) *BranchService {
|
||||
return &BranchService{
|
||||
Resource: *NewResource[types.Branch, types.CreateBranchRepoOption, struct{}](
|
||||
c, "/api/v1/repos/{owner}/{repo}/branches/{branch}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// ListBranchProtections returns all branch protections for a repository.
|
||||
func (s *BranchService) ListBranchProtections(ctx context.Context, owner, repo string) ([]types.BranchProtection, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
|
||||
return ListAll[types.BranchProtection](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetBranchProtection returns a single branch protection by name.
|
||||
func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, name string) (*types.BranchProtection, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
|
||||
var out types.BranchProtection
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreateBranchProtection creates a new branch protection rule.
|
||||
func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo string, opts *types.CreateBranchProtectionOption) (*types.BranchProtection, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
|
||||
var out types.BranchProtection
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// EditBranchProtection updates an existing branch protection rule.
|
||||
func (s *BranchService) EditBranchProtection(ctx context.Context, owner, repo, name string, opts *types.EditBranchProtectionOption) (*types.BranchProtection, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
|
||||
var out types.BranchProtection
|
||||
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DeleteBranchProtection deletes a branch protection rule.
|
||||
func (s *BranchService) DeleteBranchProtection(ctx context.Context, owner, repo, name string) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
102
branches_test.go
Normal file
102
branches_test.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestBranchService_Good_List(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode([]types.Branch{
|
||||
{Name: "main", Protected: true},
|
||||
{Name: "develop", Protected: false},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
result, err := f.Branches.List(context.Background(), Params{"owner": "core", "repo": "go-forge"}, DefaultList)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Items) != 2 {
|
||||
t.Errorf("got %d items, want 2", len(result.Items))
|
||||
}
|
||||
if result.Items[0].Name != "main" {
|
||||
t.Errorf("got name=%q, want %q", result.Items[0].Name, "main")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchService_Good_Get(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/branches/main" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Branch{Name: "main", Protected: true})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
branch, err := f.Branches.Get(context.Background(), Params{"owner": "core", "repo": "go-forge", "branch": "main"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if branch.Name != "main" {
|
||||
t.Errorf("got name=%q, want %q", branch.Name, "main")
|
||||
}
|
||||
if !branch.Protected {
|
||||
t.Error("expected branch to be protected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchService_Good_CreateProtection(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)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/branch_protections" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.CreateBranchProtectionOption
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.RuleName != "main" {
|
||||
t.Errorf("got rule_name=%q, want %q", opts.RuleName, "main")
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.BranchProtection{
|
||||
RuleName: "main",
|
||||
EnablePush: true,
|
||||
RequiredApprovals: 2,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
bp, err := f.Branches.CreateBranchProtection(context.Background(), "core", "go-forge", &types.CreateBranchProtectionOption{
|
||||
RuleName: "main",
|
||||
EnablePush: true,
|
||||
RequiredApprovals: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bp.RuleName != "main" {
|
||||
t.Errorf("got rule_name=%q, want %q", bp.RuleName, "main")
|
||||
}
|
||||
if bp.RequiredApprovals != 2 {
|
||||
t.Errorf("got required_approvals=%d, want 2", bp.RequiredApprovals)
|
||||
}
|
||||
}
|
||||
4
forge.go
4
forge.go
|
|
@ -34,8 +34,8 @@ func NewForge(url, token string, opts ...Option) *Forge {
|
|||
f.Users = newUserService(c)
|
||||
f.Teams = newTeamService(c)
|
||||
f.Admin = newAdminService(c)
|
||||
f.Branches = &BranchService{}
|
||||
f.Releases = &ReleaseService{}
|
||||
f.Branches = newBranchService(c)
|
||||
f.Releases = newReleaseService(c)
|
||||
f.Labels = &LabelService{}
|
||||
f.Webhooks = &WebhookService{}
|
||||
f.Notifications = &NotificationService{}
|
||||
|
|
|
|||
59
releases.go
Normal file
59
releases.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// ReleaseService handles release operations within a repository.
|
||||
type ReleaseService struct {
|
||||
Resource[types.Release, types.CreateReleaseOption, types.EditReleaseOption]
|
||||
}
|
||||
|
||||
func newReleaseService(c *Client) *ReleaseService {
|
||||
return &ReleaseService{
|
||||
Resource: *NewResource[types.Release, types.CreateReleaseOption, types.EditReleaseOption](
|
||||
c, "/api/v1/repos/{owner}/{repo}/releases/{id}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// GetByTag returns a release by its tag name.
|
||||
func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string) (*types.Release, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag)
|
||||
var out types.Release
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DeleteByTag deletes a release by its tag name.
|
||||
func (s *ReleaseService) DeleteByTag(ctx context.Context, owner, repo, tag string) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListAssets returns all assets for a release.
|
||||
func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, releaseID int64) ([]types.Attachment, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
|
||||
return ListAll[types.Attachment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetAsset returns a single asset for a release.
|
||||
func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) (*types.Attachment, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
|
||||
var out types.Attachment
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DeleteAsset deletes a single asset from a release.
|
||||
func (s *ReleaseService) DeleteAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
87
releases_test.go
Normal file
87
releases_test.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestReleaseService_Good_List(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode([]types.Release{
|
||||
{ID: 1, TagName: "v1.0.0", Title: "Release 1.0"},
|
||||
{ID: 2, TagName: "v2.0.0", Title: "Release 2.0"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
result, err := f.Releases.List(context.Background(), Params{"owner": "core", "repo": "go-forge"}, DefaultList)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Items) != 2 {
|
||||
t.Errorf("got %d items, want 2", len(result.Items))
|
||||
}
|
||||
if result.Items[0].TagName != "v1.0.0" {
|
||||
t.Errorf("got tag=%q, want %q", result.Items[0].TagName, "v1.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseService_Good_Get(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/releases/1" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Release{ID: 1, TagName: "v1.0.0", Title: "Release 1.0"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
release, err := f.Releases.Get(context.Background(), Params{"owner": "core", "repo": "go-forge", "id": "1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if release.TagName != "v1.0.0" {
|
||||
t.Errorf("got tag=%q, want %q", release.TagName, "v1.0.0")
|
||||
}
|
||||
if release.Title != "Release 1.0" {
|
||||
t.Errorf("got title=%q, want %q", release.Title, "Release 1.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseService_Good_GetByTag(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/releases/tags/v1.0.0" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Release{ID: 1, TagName: "v1.0.0", Title: "Release 1.0"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
release, err := f.Releases.GetByTag(context.Background(), "core", "go-forge", "v1.0.0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if release.TagName != "v1.0.0" {
|
||||
t.Errorf("got tag=%q, want %q", release.TagName, "v1.0.0")
|
||||
}
|
||||
if release.ID != 1 {
|
||||
t.Errorf("got id=%d, want 1", release.ID)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ package forge
|
|||
|
||||
// Stub service types — replaced as each service is implemented.
|
||||
|
||||
type BranchService struct{}
|
||||
type ReleaseService struct{}
|
||||
type LabelService struct{}
|
||||
type WebhookService struct{}
|
||||
type NotificationService struct{}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue