feat(forge): add repository list filters
All checks were successful
Security Scan / security (push) Successful in 15s
Test / test (push) Successful in 2m11s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 08:33:11 +00:00
parent 0e8bc8918e
commit 09d01bee96
10 changed files with 593 additions and 20 deletions

View file

@ -109,6 +109,38 @@ func TestOption_Stringers_Good(t *testing.T) {
got: SearchIssuesOptions{State: "open", PriorityRepoID: 99, Assigned: true, Query: "build"},
want: `forge.SearchIssuesOptions{state="open", q="build", priority_repo_id=99, assigned=true}`,
},
{
name: "IssueListOptions",
got: IssueListOptions{State: "open", Labels: "bug", Query: "panic", CreatedBy: "alice"},
want: `forge.IssueListOptions{state="open", labels="bug", q="panic", created_by="alice"}`,
},
{
name: "PullListOptions",
got: PullListOptions{State: "open", Sort: "priority", Milestone: 7, Labels: []int64{1, 2}, Poster: "alice"},
want: `forge.PullListOptions{state="open", sort="priority", milestone=7, labels=[]int64{1, 2}, poster="alice"}`,
},
{
name: "ReleaseListOptions",
got: ReleaseListOptions{Draft: true, PreRelease: true, Query: "1.0"},
want: `forge.ReleaseListOptions{draft=true, pre-release=true, q="1.0"}`,
},
{
name: "CommitListOptions",
got: func() CommitListOptions {
stat := false
verification := false
files := false
return CommitListOptions{
Sha: "main",
Path: "docs",
Stat: &stat,
Verification: &verification,
Files: &files,
Not: "deadbeef",
}
}(),
want: `forge.CommitListOptions{sha="main", path="docs", stat=false, verification=false, files=false, not="deadbeef"}`,
},
{
name: "ReleaseAttachmentUploadOptions",
got: ReleaseAttachmentUploadOptions{Name: "release.zip"},

View file

@ -3,6 +3,7 @@ package forge
import (
"context"
"iter"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -20,6 +21,62 @@ type CommitService struct {
client *Client
}
// CommitListOptions controls filtering for repository commit listings.
//
// Usage:
//
// stat := false
// opts := forge.CommitListOptions{Sha: "main", Stat: &stat}
type CommitListOptions struct {
Sha string
Path string
Stat *bool
Verification *bool
Files *bool
Not string
}
// String returns a safe summary of the commit list filters.
func (o CommitListOptions) String() string {
return optionString("forge.CommitListOptions",
"sha", o.Sha,
"path", o.Path,
"stat", o.Stat,
"verification", o.Verification,
"files", o.Files,
"not", o.Not,
)
}
// GoString returns a safe Go-syntax summary of the commit list filters.
func (o CommitListOptions) GoString() string { return o.String() }
func (o CommitListOptions) queryParams() map[string]string {
query := make(map[string]string, 6)
if o.Sha != "" {
query["sha"] = o.Sha
}
if o.Path != "" {
query["path"] = o.Path
}
if o.Stat != nil {
query["stat"] = strconv.FormatBool(*o.Stat)
}
if o.Verification != nil {
query["verification"] = strconv.FormatBool(*o.Verification)
}
if o.Files != nil {
query["files"] = strconv.FormatBool(*o.Files)
}
if o.Not != "" {
query["not"] = o.Not
}
if len(query) == 0 {
return nil
}
return query
}
const (
commitCollectionPath = "/api/v1/repos/{owner}/{repo}/commits"
commitItemPath = "/api/v1/repos/{owner}/{repo}/git/commits/{sha}"
@ -30,18 +87,18 @@ func newCommitService(c *Client) *CommitService {
}
// List returns a single page of commits for a repository.
func (s *CommitService) List(ctx context.Context, params Params, opts ListOptions) (*PagedResult[types.Commit], error) {
return ListPage[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil, opts)
func (s *CommitService) List(ctx context.Context, params Params, opts ListOptions, filters ...CommitListOptions) (*PagedResult[types.Commit], error) {
return ListPage[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), commitListQuery(filters...), opts)
}
// ListAll returns all commits for a repository.
func (s *CommitService) ListAll(ctx context.Context, params Params) ([]types.Commit, error) {
return ListAll[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil)
func (s *CommitService) ListAll(ctx context.Context, params Params, filters ...CommitListOptions) ([]types.Commit, error) {
return ListAll[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), commitListQuery(filters...))
}
// Iter returns an iterator over all commits for a repository.
func (s *CommitService) Iter(ctx context.Context, params Params) iter.Seq2[types.Commit, error] {
return ListIter[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil)
func (s *CommitService) Iter(ctx context.Context, params Params, filters ...CommitListOptions) iter.Seq2[types.Commit, error] {
return ListIter[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), commitListQuery(filters...))
}
// Get returns a single commit by SHA or ref.
@ -155,3 +212,16 @@ func (s *CommitService) DeleteNote(ctx context.Context, owner, repo, sha string)
path := ResolvePath("/api/v1/repos/{owner}/{repo}/git/notes/{sha}", pathParams("owner", owner, "repo", repo, "sha", sha))
return s.client.Delete(ctx, path)
}
func commitListQuery(filters ...CommitListOptions) map[string]string {
query := make(map[string]string, len(filters))
for _, filter := range filters {
for key, value := range filter.queryParams() {
query[key] = value
}
}
if len(query) == 0 {
return nil
}
return query
}

View file

@ -62,6 +62,55 @@ func TestCommitService_List_Good(t *testing.T) {
}
}
func TestCommitService_ListFiltered_Good(t *testing.T) {
stat := false
verification := false
files := false
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/commits" {
t.Errorf("wrong path: %s", r.URL.Path)
}
want := map[string]string{
"sha": "main",
"path": "docs",
"stat": "false",
"verification": "false",
"files": "false",
"not": "deadbeef",
"page": "1",
"limit": "50",
}
for key, wantValue := range want {
if got := r.URL.Query().Get(key); got != wantValue {
t.Errorf("got %s=%q, want %q", key, got, wantValue)
}
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Commit{{SHA: "abc123"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
commits, err := f.Commits.ListAll(context.Background(), Params{"owner": "core", "repo": "go-forge"}, CommitListOptions{
Sha: "main",
Path: "docs",
Stat: &stat,
Verification: &verification,
Files: &files,
Not: "deadbeef",
})
if err != nil {
t.Fatal(err)
}
if len(commits) != 1 || commits[0].SHA != "abc123" {
t.Fatalf("got %#v", commits)
}
}
func TestCommitService_Get_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {

View file

@ -69,6 +69,8 @@ func isZeroOptionValue(v any) bool {
return len(x) == 0
case *time.Time:
return x == nil
case *bool:
return x == nil
case time.Time:
return x.IsZero()
default:
@ -93,6 +95,11 @@ func formatOptionValue(v any) string {
return "<nil>"
}
return strconv.Quote(x.Format(time.RFC3339))
case *bool:
if x == nil {
return "<nil>"
}
return strconv.FormatBool(*x)
case time.Time:
return strconv.Quote(x.Format(time.RFC3339))
default:

View file

@ -21,6 +21,81 @@ type IssueService struct {
Resource[types.Issue, types.CreateIssueOption, types.EditIssueOption]
}
// IssueListOptions controls filtering for repository issue listings.
//
// Usage:
//
// opts := forge.IssueListOptions{State: "open", Labels: "bug"}
type IssueListOptions struct {
State string
Labels string
Query string
Type string
Milestones string
Since *time.Time
Before *time.Time
CreatedBy string
AssignedBy string
MentionedBy string
}
// String returns a safe summary of the issue list filters.
func (o IssueListOptions) String() string {
return optionString("forge.IssueListOptions",
"state", o.State,
"labels", o.Labels,
"q", o.Query,
"type", o.Type,
"milestones", o.Milestones,
"since", o.Since,
"before", o.Before,
"created_by", o.CreatedBy,
"assigned_by", o.AssignedBy,
"mentioned_by", o.MentionedBy,
)
}
// GoString returns a safe Go-syntax summary of the issue list filters.
func (o IssueListOptions) GoString() string { return o.String() }
func (o IssueListOptions) queryParams() map[string]string {
query := make(map[string]string, 10)
if o.State != "" {
query["state"] = o.State
}
if o.Labels != "" {
query["labels"] = o.Labels
}
if o.Query != "" {
query["q"] = o.Query
}
if o.Type != "" {
query["type"] = o.Type
}
if o.Milestones != "" {
query["milestones"] = o.Milestones
}
if o.Since != nil {
query["since"] = o.Since.Format(time.RFC3339)
}
if o.Before != nil {
query["before"] = o.Before.Format(time.RFC3339)
}
if o.CreatedBy != "" {
query["created_by"] = o.CreatedBy
}
if o.AssignedBy != "" {
query["assigned_by"] = o.AssignedBy
}
if o.MentionedBy != "" {
query["mentioned_by"] = o.MentionedBy
}
if len(query) == 0 {
return nil
}
return query
}
// AttachmentUploadOptions controls metadata sent when uploading an attachment.
//
// Usage:
@ -201,15 +276,15 @@ func (s *IssueService) IterSearchIssues(ctx context.Context, opts SearchIssuesOp
}
// ListIssues returns all issues in a repository.
func (s *IssueService) ListIssues(ctx context.Context, owner, repo string) ([]types.Issue, error) {
func (s *IssueService) ListIssues(ctx context.Context, owner, repo string, filters ...IssueListOptions) ([]types.Issue, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues", pathParams("owner", owner, "repo", repo))
return ListAll[types.Issue](ctx, s.client, path, nil)
return ListAll[types.Issue](ctx, s.client, path, issueListQuery(filters...))
}
// IterIssues returns an iterator over all issues in a repository.
func (s *IssueService) IterIssues(ctx context.Context, owner, repo string) iter.Seq2[types.Issue, error] {
func (s *IssueService) IterIssues(ctx context.Context, owner, repo string, filters ...IssueListOptions) iter.Seq2[types.Issue, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues", pathParams("owner", owner, "repo", repo))
return ListIter[types.Issue](ctx, s.client, path, nil)
return ListIter[types.Issue](ctx, s.client, path, issueListQuery(filters...))
}
// CreateIssue creates a new issue in a repository.
@ -454,6 +529,19 @@ func (s *IssueService) DeleteCommentReaction(ctx context.Context, owner, repo st
return s.client.DeleteWithBody(ctx, path, types.EditReactionOption{Reaction: reaction})
}
func issueListQuery(filters ...IssueListOptions) map[string]string {
query := make(map[string]string, len(filters))
for _, filter := range filters {
for key, value := range filter.queryParams() {
query[key] = value
}
}
if len(query) == 0 {
return nil
}
return query
}
func attachmentUploadQuery(opts *AttachmentUploadOptions) map[string]string {
if opts == nil {
return nil

View file

@ -78,6 +78,64 @@ func TestIssueService_List_Good(t *testing.T) {
}
}
func TestIssueService_ListFiltered_Good(t *testing.T) {
since := time.Date(2026, time.March, 1, 12, 30, 0, 0, time.UTC)
before := time.Date(2026, time.March, 2, 12, 30, 0, 0, time.UTC)
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" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
want := map[string]string{
"state": "open",
"labels": "bug,help wanted",
"q": "panic",
"type": "issues",
"milestones": "v1.0",
"since": since.Format(time.RFC3339),
"before": before.Format(time.RFC3339),
"created_by": "alice",
"assigned_by": "bob",
"mentioned_by": "carol",
"page": "1",
"limit": "50",
}
for key, wantValue := range want {
if got := r.URL.Query().Get(key); got != wantValue {
t.Errorf("got %s=%q, want %q", key, got, wantValue)
}
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Issue{{ID: 1, Title: "panic in parser"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
issues, err := f.Issues.ListIssues(context.Background(), "core", "go-forge", IssueListOptions{
State: "open",
Labels: "bug,help wanted",
Query: "panic",
Type: "issues",
Milestones: "v1.0",
Since: &since,
Before: &before,
CreatedBy: "alice",
AssignedBy: "bob",
MentionedBy: "carol",
})
if err != nil {
t.Fatal(err)
}
if len(issues) != 1 || issues[0].Title != "panic in parser" {
t.Fatalf("got %#v", issues)
}
}
func TestIssueService_Get_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {

138
pulls.go
View file

@ -3,6 +3,8 @@ package forge
import (
"context"
"iter"
"net/url"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -17,6 +19,53 @@ type PullService struct {
Resource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption]
}
// PullListOptions controls filtering for repository pull request listings.
//
// Usage:
//
// opts := forge.PullListOptions{State: "open", Labels: []int64{1, 2}}
type PullListOptions struct {
State string
Sort string
Milestone int64
Labels []int64
Poster string
}
// String returns a safe summary of the pull request list filters.
func (o PullListOptions) String() string {
return optionString("forge.PullListOptions",
"state", o.State,
"sort", o.Sort,
"milestone", o.Milestone,
"labels", o.Labels,
"poster", o.Poster,
)
}
// GoString returns a safe Go-syntax summary of the pull request list filters.
func (o PullListOptions) GoString() string { return o.String() }
func (o PullListOptions) addQuery(values url.Values) {
if o.State != "" {
values.Set("state", o.State)
}
if o.Sort != "" {
values.Set("sort", o.Sort)
}
if o.Milestone != 0 {
values.Set("milestone", strconv.FormatInt(o.Milestone, 10))
}
for _, label := range o.Labels {
if label != 0 {
values.Add("labels", strconv.FormatInt(label, 10))
}
}
if o.Poster != "" {
values.Set("poster", o.Poster)
}
}
func newPullService(c *Client) *PullService {
return &PullService{
Resource: *NewResource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption](
@ -26,15 +75,13 @@ func newPullService(c *Client) *PullService {
}
// ListPullRequests returns all pull requests in a repository.
func (s *PullService) ListPullRequests(ctx context.Context, owner, repo string) ([]types.PullRequest, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls", pathParams("owner", owner, "repo", repo))
return ListAll[types.PullRequest](ctx, s.client, path, nil)
func (s *PullService) ListPullRequests(ctx context.Context, owner, repo string, filters ...PullListOptions) ([]types.PullRequest, error) {
return s.listAll(ctx, owner, repo, filters...)
}
// IterPullRequests returns an iterator over all pull requests in a repository.
func (s *PullService) IterPullRequests(ctx context.Context, owner, repo string) iter.Seq2[types.PullRequest, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls", pathParams("owner", owner, "repo", repo))
return ListIter[types.PullRequest](ctx, s.client, path, nil)
func (s *PullService) IterPullRequests(ctx context.Context, owner, repo string, filters ...PullListOptions) iter.Seq2[types.PullRequest, error] {
return s.listIter(ctx, owner, repo, filters...)
}
// CreatePullRequest creates a pull request in a repository.
@ -176,6 +223,85 @@ func (s *PullService) DeleteReview(ctx context.Context, owner, repo string, inde
return s.client.Delete(ctx, path)
}
func (s *PullService) listPage(ctx context.Context, owner, repo string, opts ListOptions, filters ...PullListOptions) (*PagedResult[types.PullRequest], error) {
if opts.Page < 1 {
opts.Page = 1
}
if opts.Limit < 1 {
opts.Limit = defaultPageLimit
}
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls", pathParams("owner", owner, "repo", repo))
u, err := url.Parse(path)
if err != nil {
return nil, err
}
values := u.Query()
values.Set("page", strconv.Itoa(opts.Page))
values.Set("limit", strconv.Itoa(opts.Limit))
for _, filter := range filters {
filter.addQuery(values)
}
u.RawQuery = values.Encode()
var items []types.PullRequest
resp, err := s.client.doJSON(ctx, "GET", u.String(), nil, &items)
if err != nil {
return nil, err
}
totalCount, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
return &PagedResult[types.PullRequest]{
Items: items,
TotalCount: totalCount,
Page: opts.Page,
HasMore: (totalCount > 0 && (opts.Page-1)*opts.Limit+len(items) < totalCount) ||
(totalCount == 0 && len(items) >= opts.Limit),
}, nil
}
func (s *PullService) listAll(ctx context.Context, owner, repo string, filters ...PullListOptions) ([]types.PullRequest, error) {
var all []types.PullRequest
page := 1
for {
result, err := s.listPage(ctx, owner, repo, ListOptions{Page: page, Limit: defaultPageLimit}, filters...)
if err != nil {
return nil, err
}
all = append(all, result.Items...)
if !result.HasMore {
break
}
page++
}
return all, nil
}
func (s *PullService) listIter(ctx context.Context, owner, repo string, filters ...PullListOptions) iter.Seq2[types.PullRequest, error] {
return func(yield func(types.PullRequest, error) bool) {
page := 1
for {
result, err := s.listPage(ctx, owner, repo, ListOptions{Page: page, Limit: defaultPageLimit}, filters...)
if err != nil {
yield(*new(types.PullRequest), err)
return
}
for _, item := range result.Items {
if !yield(item, nil) {
return
}
}
if !result.HasMore {
break
}
page++
}
}
}
// ListReviewComments returns all comments on a pull request review.
func (s *PullService) ListReviewComments(ctx context.Context, owner, repo string, index, reviewID int64) ([]types.PullReviewComment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID)))

View file

@ -5,6 +5,7 @@ import (
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"dappco.re/go/core/forge/types"
@ -41,6 +42,53 @@ func TestPullService_List_Good(t *testing.T) {
}
}
func TestPullService_ListFiltered_Good(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" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
want := map[string]string{
"state": "open",
"sort": "priority",
"milestone": "7",
"poster": "alice",
"page": "1",
"limit": "50",
}
for key, wantValue := range want {
if got := r.URL.Query().Get(key); got != wantValue {
t.Errorf("got %s=%q, want %q", key, got, wantValue)
}
}
if got := r.URL.Query()["labels"]; !reflect.DeepEqual(got, []string{"1", "2"}) {
t.Errorf("got labels=%v, want %v", got, []string{"1", "2"})
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.PullRequest{{ID: 1, Title: "add feature"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
prs, err := f.Pulls.ListPullRequests(context.Background(), "core", "go-forge", PullListOptions{
State: "open",
Sort: "priority",
Milestone: 7,
Labels: []int64{1, 2},
Poster: "alice",
})
if err != nil {
t.Fatal(err)
}
if len(prs) != 1 || prs[0].Title != "add feature" {
t.Fatalf("got %#v", prs)
}
}
func TestPullService_Get_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {

View file

@ -3,6 +3,7 @@ package forge
import (
"context"
"iter"
"strconv"
goio "io"
@ -19,6 +20,46 @@ type ReleaseService struct {
Resource[types.Release, types.CreateReleaseOption, types.EditReleaseOption]
}
// ReleaseListOptions controls filtering for repository release listings.
//
// Usage:
//
// opts := forge.ReleaseListOptions{Draft: true, Query: "1.0"}
type ReleaseListOptions struct {
Draft bool
PreRelease bool
Query string
}
// String returns a safe summary of the release list filters.
func (o ReleaseListOptions) String() string {
return optionString("forge.ReleaseListOptions",
"draft", o.Draft,
"pre-release", o.PreRelease,
"q", o.Query,
)
}
// GoString returns a safe Go-syntax summary of the release list filters.
func (o ReleaseListOptions) GoString() string { return o.String() }
func (o ReleaseListOptions) queryParams() map[string]string {
query := make(map[string]string, 3)
if o.Draft {
query["draft"] = strconv.FormatBool(true)
}
if o.PreRelease {
query["pre-release"] = strconv.FormatBool(true)
}
if o.Query != "" {
query["q"] = o.Query
}
if len(query) == 0 {
return nil
}
return query
}
// ReleaseAttachmentUploadOptions controls metadata sent when uploading a release attachment.
//
// Usage:
@ -60,15 +101,15 @@ func newReleaseService(c *Client) *ReleaseService {
}
// ListReleases returns all releases in a repository.
func (s *ReleaseService) ListReleases(ctx context.Context, owner, repo string) ([]types.Release, error) {
func (s *ReleaseService) ListReleases(ctx context.Context, owner, repo string, filters ...ReleaseListOptions) ([]types.Release, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases", pathParams("owner", owner, "repo", repo))
return ListAll[types.Release](ctx, s.client, path, nil)
return ListAll[types.Release](ctx, s.client, path, releaseListQuery(filters...))
}
// IterReleases returns an iterator over all releases in a repository.
func (s *ReleaseService) IterReleases(ctx context.Context, owner, repo string) iter.Seq2[types.Release, error] {
func (s *ReleaseService) IterReleases(ctx context.Context, owner, repo string, filters ...ReleaseListOptions) iter.Seq2[types.Release, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases", pathParams("owner", owner, "repo", repo))
return ListIter[types.Release](ctx, s.client, path, nil)
return ListIter[types.Release](ctx, s.client, path, releaseListQuery(filters...))
}
// CreateRelease creates a release in a repository.
@ -174,3 +215,16 @@ func (s *ReleaseService) DeleteAsset(ctx context.Context, owner, repo string, re
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets/{assetID}", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID), "assetID", int64String(assetID)))
return s.client.Delete(ctx, path)
}
func releaseListQuery(filters ...ReleaseListOptions) map[string]string {
query := make(map[string]string, len(filters))
for _, filter := range filters {
for key, value := range filter.queryParams() {
query[key] = value
}
}
if len(query) == 0 {
return nil
}
return query
}

View file

@ -84,6 +84,47 @@ func TestReleaseService_List_Good(t *testing.T) {
}
}
func TestReleaseService_ListFiltered_Good(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" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
want := map[string]string{
"draft": "true",
"pre-release": "true",
"q": "1.0",
"page": "1",
"limit": "50",
}
for key, wantValue := range want {
if got := r.URL.Query().Get(key); got != wantValue {
t.Errorf("got %s=%q, want %q", key, got, wantValue)
}
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Release{{ID: 1, TagName: "v1.0.0", Title: "Release 1.0"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
releases, err := f.Releases.ListReleases(context.Background(), "core", "go-forge", ReleaseListOptions{
Draft: true,
PreRelease: true,
Query: "1.0",
})
if err != nil {
t.Fatal(err)
}
if len(releases) != 1 || releases[0].TagName != "v1.0.0" {
t.Fatalf("got %#v", releases)
}
}
func TestReleaseService_Get_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {