feat(forge): add repository list filters
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
0e8bc8918e
commit
09d01bee96
10 changed files with 593 additions and 20 deletions
|
|
@ -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"},
|
||||
|
|
|
|||
82
commits.go
82
commits.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
96
issues.go
96
issues.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
138
pulls.go
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
62
releases.go
62
releases.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue