feat: IssueService and PullService with actions
Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
abc8840fa4
commit
f8c6090227
7 changed files with 387 additions and 4 deletions
|
|
@ -100,6 +100,11 @@ func (c *Client) Delete(ctx context.Context, path string) error {
|
|||
return c.do(ctx, http.MethodDelete, path, nil, nil)
|
||||
}
|
||||
|
||||
// DeleteWithBody performs a DELETE request with a JSON body.
|
||||
func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) error {
|
||||
return c.do(ctx, http.MethodDelete, path, body, nil)
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, body, out any) error {
|
||||
url := c.baseURL + path
|
||||
|
||||
|
|
|
|||
4
forge.go
4
forge.go
|
|
@ -28,10 +28,10 @@ func NewForge(url, token string, opts ...Option) *Forge {
|
|||
c := NewClient(url, token, opts...)
|
||||
f := &Forge{client: c}
|
||||
f.Repos = newRepoService(c)
|
||||
f.Issues = newIssueService(c)
|
||||
f.Pulls = newPullService(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{}
|
||||
|
|
|
|||
105
issues.go
Normal file
105
issues.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// IssueService handles issue operations within a repository.
|
||||
type IssueService struct {
|
||||
Resource[types.Issue, types.CreateIssueOption, types.EditIssueOption]
|
||||
}
|
||||
|
||||
func newIssueService(c *Client) *IssueService {
|
||||
return &IssueService{
|
||||
Resource: *NewResource[types.Issue, types.CreateIssueOption, types.EditIssueOption](
|
||||
c, "/api/v1/repos/{owner}/{repo}/issues/{index}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Pin pins an issue.
|
||||
func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", owner, repo, index)
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// Unpin unpins an issue.
|
||||
func (s *IssueService) Unpin(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", owner, repo, index)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// SetDeadline sets or updates the deadline on an issue.
|
||||
func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline string) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/deadline", owner, repo, index)
|
||||
body := map[string]string{"due_date": deadline}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// AddReaction adds a reaction to an issue.
|
||||
func (s *IssueService) AddReaction(ctx context.Context, owner, repo string, index int64, reaction string) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner, repo, index)
|
||||
body := map[string]string{"content": reaction}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// DeleteReaction removes a reaction from an issue.
|
||||
func (s *IssueService) DeleteReaction(ctx context.Context, owner, repo string, index int64, reaction string) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner, repo, index)
|
||||
body := map[string]string{"content": reaction}
|
||||
return s.client.DeleteWithBody(ctx, path, body)
|
||||
}
|
||||
|
||||
// StartStopwatch starts the stopwatch on an issue.
|
||||
func (s *IssueService) StartStopwatch(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/stopwatch/start", owner, repo, index)
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// StopStopwatch stops the stopwatch on an issue.
|
||||
func (s *IssueService) StopStopwatch(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/stopwatch/stop", owner, repo, index)
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// AddLabels adds labels to an issue.
|
||||
func (s *IssueService) AddLabels(ctx context.Context, owner, repo string, index int64, labelIDs []int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner, repo, index)
|
||||
body := types.IssueLabelsOption{Labels: toAnySlice(labelIDs)}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// RemoveLabel removes a single label from an issue.
|
||||
func (s *IssueService) RemoveLabel(ctx context.Context, owner, repo string, index int64, labelID int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels/%d", owner, repo, index, labelID)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListComments returns all comments on an issue.
|
||||
func (s *IssueService) ListComments(ctx context.Context, owner, repo string, index int64) ([]types.Comment, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
|
||||
return ListAll[types.Comment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateComment creates a comment on an issue.
|
||||
func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, index int64, body string) (*types.Comment, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
|
||||
opts := types.CreateIssueCommentOption{Body: body}
|
||||
var out types.Comment
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// toAnySlice converts a slice of int64 to a slice of any for IssueLabelsOption.
|
||||
func toAnySlice(ids []int64) []any {
|
||||
out := make([]any, len(ids))
|
||||
for i, id := range ids {
|
||||
out[i] = id
|
||||
}
|
||||
return out
|
||||
}
|
||||
103
issues_test.go
Normal file
103
issues_test.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestIssueService_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.Issue{
|
||||
{ID: 1, Title: "bug report"},
|
||||
{ID: 2, Title: "feature request"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
result, err := f.Issues.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].Title != "bug report" {
|
||||
t.Errorf("got title=%q, want %q", result.Items[0].Title, "bug report")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_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/issues/1" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Issue{ID: 1, Title: "bug report", Index: 1})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
issue, err := f.Issues.Get(context.Background(), Params{"owner": "core", "repo": "go-forge", "index": "1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if issue.Title != "bug report" {
|
||||
t.Errorf("got title=%q", issue.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Good_Create(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)
|
||||
}
|
||||
var body types.CreateIssueOption
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(types.Issue{ID: 1, Title: body.Title, Index: 1})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
issue, err := f.Issues.Create(context.Background(), Params{"owner": "core", "repo": "go-forge"}, &types.CreateIssueOption{
|
||||
Title: "new issue",
|
||||
Body: "description here",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if issue.Title != "new issue" {
|
||||
t.Errorf("got title=%q", issue.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Good_Pin(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/issues/42/pin" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
err := f.Issues.Pin(context.Background(), "core", "go-forge", 42)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
63
pulls.go
Normal file
63
pulls.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// PullService handles pull request operations within a repository.
|
||||
type PullService struct {
|
||||
Resource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption]
|
||||
}
|
||||
|
||||
func newPullService(c *Client) *PullService {
|
||||
return &PullService{
|
||||
Resource: *NewResource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption](
|
||||
c, "/api/v1/repos/{owner}/{repo}/pulls/{index}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Merge merges a pull request. Method is one of "merge", "rebase", "rebase-merge", "squash", "fast-forward-only", "manually-merged".
|
||||
func (s *PullService) Merge(ctx context.Context, owner, repo string, index int64, method string) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
|
||||
body := map[string]string{"Do": method}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// Update updates a pull request branch with the base branch.
|
||||
func (s *PullService) Update(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/update", owner, repo, index)
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// ListReviews returns all reviews on a pull request.
|
||||
func (s *PullService) ListReviews(ctx context.Context, owner, repo string, index int64) ([]types.PullReview, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
|
||||
return ListAll[types.PullReview](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// SubmitReview creates a new review on a pull request.
|
||||
func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, index int64, review map[string]any) (*types.PullReview, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
|
||||
var out types.PullReview
|
||||
if err := s.client.Post(ctx, path, review, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DismissReview dismisses a pull request review.
|
||||
func (s *PullService) DismissReview(ctx context.Context, owner, repo string, index, reviewID int64, msg string) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals", owner, repo, index, reviewID)
|
||||
body := map[string]string{"message": msg}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// UndismissReview undismisses a pull request review.
|
||||
func (s *PullService) UndismissReview(ctx context.Context, owner, repo string, index, reviewID int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals", owner, repo, index, reviewID)
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
109
pulls_test.go
Normal file
109
pulls_test.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestPullService_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.PullRequest{
|
||||
{ID: 1, Title: "add feature"},
|
||||
{ID: 2, Title: "fix bug"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
result, err := f.Pulls.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].Title != "add feature" {
|
||||
t.Errorf("got title=%q, want %q", result.Items[0].Title, "add feature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullService_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/pulls/1" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.PullRequest{ID: 1, Title: "add feature", Index: 1})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
pr, err := f.Pulls.Get(context.Background(), Params{"owner": "core", "repo": "go-forge", "index": "1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pr.Title != "add feature" {
|
||||
t.Errorf("got title=%q", pr.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullService_Good_Create(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)
|
||||
}
|
||||
var body types.CreatePullRequestOption
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(types.PullRequest{ID: 1, Title: body.Title, Index: 1})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
pr, err := f.Pulls.Create(context.Background(), Params{"owner": "core", "repo": "go-forge"}, &types.CreatePullRequestOption{
|
||||
Title: "new pull request",
|
||||
Head: "feature-branch",
|
||||
Base: "main",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pr.Title != "new pull request" {
|
||||
t.Errorf("got title=%q", pr.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullService_Good_Merge(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/pulls/7/merge" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var body map[string]string
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
if body["Do"] != "merge" {
|
||||
t.Errorf("got Do=%q, want %q", body["Do"], "merge")
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
err := f.Pulls.Merge(context.Background(), "core", "go-forge", 7, "merge")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ 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{}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue