diff --git a/.golangci.yml b/.golangci.yml index 105b33e71c7..9b802bb20b1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -268,10 +268,10 @@ linters: - GetProvisionedSCIMGroupEnterpriseOptions.ExcludedAttributes - GistListOptions.Since # TODO: Gists - IssueEvent.Action # TODO: Issues + - IssueListByOrgOptions.Since - IssueListByRepoOptions.Since # TODO: Issues - IssueListCommentsOptions.Direction - IssueListCommentsOptions.Sort - - IssueListOptions.Since # TODO: Issues - IssueRequest.Assignees # TODO: Issues - IssueRequest.Labels # TODO: Issues - License.Conditions # TODO: Licenses @@ -284,6 +284,7 @@ linters: - ListAlertsOptions.Severity - ListAlertsOptions.Sort - ListAlertsOptions.State + - ListAllIssuesOptions.Since - ListArtifactsOptions.Name - ListCheckRunsOptions.AppID - ListCheckRunsOptions.CheckName @@ -318,6 +319,7 @@ linters: - ListSCIMProvisionedIdentitiesOptions.Count - ListSCIMProvisionedIdentitiesOptions.Filter - ListSCIMProvisionedIdentitiesOptions.StartIndex + - ListUserIssuesOptions.Since - LockIssueOptions.LockReason # TODO: Issues - MarketplacePlan.Bullets # TODO: Marketplaces - NodeQueryOptions.ClusterRoles diff --git a/github/github-iterators.go b/github/github-iterators.go index 8d4cced8c6c..cdff22217bb 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -3307,18 +3307,18 @@ func (s *GistsService) ListStarredIter(ctx context.Context, opts *GistListOption } } -// ListIter returns an iterator that paginates through all results of List. -func (s *IssuesService) ListIter(ctx context.Context, all bool, opts *IssueListOptions) iter.Seq2[*Issue, error] { +// ListAllIssuesIter returns an iterator that paginates through all results of ListAllIssues. +func (s *IssuesService) ListAllIssuesIter(ctx context.Context, opts *ListAllIssuesOptions) iter.Seq2[*Issue, error] { return func(yield func(*Issue, error) bool) { // Create a copy of opts to avoid mutating the caller's struct if opts == nil { - opts = &IssueListOptions{} + opts = &ListAllIssuesOptions{} } else { opts = Ptr(*opts) } for { - results, resp, err := s.List(ctx, all, opts) + results, resp, err := s.ListAllIssues(ctx, opts) if err != nil { yield(nil, err) return @@ -3370,11 +3370,11 @@ func (s *IssuesService) ListAssigneesIter(ctx context.Context, owner string, rep } // ListByOrgIter returns an iterator that paginates through all results of ListByOrg. -func (s *IssuesService) ListByOrgIter(ctx context.Context, org string, opts *IssueListOptions) iter.Seq2[*Issue, error] { +func (s *IssuesService) ListByOrgIter(ctx context.Context, org string, opts *IssueListByOrgOptions) iter.Seq2[*Issue, error] { return func(yield func(*Issue, error) bool) { // Create a copy of opts to avoid mutating the caller's struct if opts == nil { - opts = &IssueListOptions{} + opts = &IssueListByOrgOptions{} } else { opts = Ptr(*opts) } @@ -3680,6 +3680,37 @@ func (s *IssuesService) ListRepositoryEventsIter(ctx context.Context, owner stri } } +// ListUserIssuesIter returns an iterator that paginates through all results of ListUserIssues. +func (s *IssuesService) ListUserIssuesIter(ctx context.Context, opts *ListUserIssuesOptions) iter.Seq2[*Issue, error] { + return func(yield func(*Issue, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &ListUserIssuesOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListUserIssues(ctx, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.ListOptions.Page = resp.NextPage + } + } +} + // ListIter returns an iterator that paginates through all results of List. func (s *LicensesService) ListIter(ctx context.Context, opts *ListLicensesOptions) iter.Seq2[*License, error] { return func(yield func(*License, error) bool) { @@ -6386,11 +6417,11 @@ func (s *SecurityAdvisoriesService) ListRepositorySecurityAdvisoriesForOrgIter(c } // ListByIssueIter returns an iterator that paginates through all results of ListByIssue. -func (s *SubIssueService) ListByIssueIter(ctx context.Context, owner string, repo string, issueNumber int64, opts *IssueListOptions) iter.Seq2[*SubIssue, error] { +func (s *SubIssueService) ListByIssueIter(ctx context.Context, owner string, repo string, issueNumber int64, opts *ListOptions) iter.Seq2[*SubIssue, error] { return func(yield func(*SubIssue, error) bool) { // Create a copy of opts to avoid mutating the caller's struct if opts == nil { - opts = &IssueListOptions{} + opts = &ListOptions{} } else { opts = Ptr(*opts) } @@ -6411,7 +6442,7 @@ func (s *SubIssueService) ListByIssueIter(ctx context.Context, owner string, rep if resp.NextPage == 0 { break } - opts.ListOptions.Page = resp.NextPage + opts.Page = resp.NextPage } } } diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index 9a074b5e814..fc299ce307a 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -7143,7 +7143,7 @@ func TestGistsService_ListStarredIter(t *testing.T) { } } -func TestIssuesService_ListIter(t *testing.T) { +func TestIssuesService_ListAllIssuesIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) var callNum int @@ -7164,7 +7164,7 @@ func TestIssuesService_ListIter(t *testing.T) { } }) - iter := client.Issues.ListIter(t.Context(), false, nil) + iter := client.Issues.ListAllIssuesIter(t.Context(), nil) var gotItems int for _, err := range iter { gotItems++ @@ -7173,11 +7173,11 @@ func TestIssuesService_ListIter(t *testing.T) { } } if want := 7; gotItems != want { - t.Errorf("client.Issues.ListIter call 1 got %v items; want %v", gotItems, want) + t.Errorf("client.Issues.ListAllIssuesIter call 1 got %v items; want %v", gotItems, want) } - opts := &IssueListOptions{} - iter = client.Issues.ListIter(t.Context(), false, opts) + opts := &ListAllIssuesOptions{} + iter = client.Issues.ListAllIssuesIter(t.Context(), opts) gotItems = 0 for _, err := range iter { gotItems++ @@ -7186,10 +7186,10 @@ func TestIssuesService_ListIter(t *testing.T) { } } if want := 2; gotItems != want { - t.Errorf("client.Issues.ListIter call 2 got %v items; want %v", gotItems, want) + t.Errorf("client.Issues.ListAllIssuesIter call 2 got %v items; want %v", gotItems, want) } - iter = client.Issues.ListIter(t.Context(), false, nil) + iter = client.Issues.ListAllIssuesIter(t.Context(), nil) gotItems = 0 for _, err := range iter { gotItems++ @@ -7198,10 +7198,10 @@ func TestIssuesService_ListIter(t *testing.T) { } } if gotItems != 1 { - t.Errorf("client.Issues.ListIter call 3 got %v items; want 1 (an error)", gotItems) + t.Errorf("client.Issues.ListAllIssuesIter call 3 got %v items; want 1 (an error)", gotItems) } - iter = client.Issues.ListIter(t.Context(), false, nil) + iter = client.Issues.ListAllIssuesIter(t.Context(), nil) gotItems = 0 iter(func(item *Issue, err error) bool { gotItems++ @@ -7211,7 +7211,7 @@ func TestIssuesService_ListIter(t *testing.T) { return false }) if gotItems != 1 { - t.Errorf("client.Issues.ListIter call 4 got %v items; want 1 (an error)", gotItems) + t.Errorf("client.Issues.ListAllIssuesIter call 4 got %v items; want 1 (an error)", gotItems) } } @@ -7320,7 +7320,7 @@ func TestIssuesService_ListByOrgIter(t *testing.T) { t.Errorf("client.Issues.ListByOrgIter call 1 got %v items; want %v", gotItems, want) } - opts := &IssueListOptions{} + opts := &IssueListByOrgOptions{} iter = client.Issues.ListByOrgIter(t.Context(), "", opts) gotItems = 0 for _, err := range iter { @@ -8007,6 +8007,78 @@ func TestIssuesService_ListRepositoryEventsIter(t *testing.T) { } } +func TestIssuesService_ListUserIssuesIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `[{},{},{}]`) + case 2: + fmt.Fprint(w, `[{},{},{},{}]`) + case 3: + fmt.Fprint(w, `[{},{}]`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `[{},{}]`) + } + }) + + iter := client.Issues.ListUserIssuesIter(t.Context(), nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.Issues.ListUserIssuesIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListUserIssuesOptions{} + iter = client.Issues.ListUserIssuesIter(t.Context(), opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.Issues.ListUserIssuesIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Issues.ListUserIssuesIter(t.Context(), nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Issues.ListUserIssuesIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Issues.ListUserIssuesIter(t.Context(), nil) + gotItems = 0 + iter(func(item *Issue, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Issues.ListUserIssuesIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestLicensesService_ListIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -14232,7 +14304,7 @@ func TestSubIssueService_ListByIssueIter(t *testing.T) { t.Errorf("client.SubIssue.ListByIssueIter call 1 got %v items; want %v", gotItems, want) } - opts := &IssueListOptions{} + opts := &ListOptions{} iter = client.SubIssue.ListByIssueIter(t.Context(), "", "", 0, opts) gotItems = 0 for _, err := range iter { diff --git a/github/issues.go b/github/issues.go index 95cb13582a4..c31bebe459c 100644 --- a/github/issues.go +++ b/github/issues.go @@ -100,11 +100,33 @@ type IssueRequest struct { Type *string `json:"type,omitempty"` } -// IssueListOptions specifies the optional parameters to the IssuesService.List -// and IssuesService.ListByOrg methods. -type IssueListOptions struct { +// PullRequestLinks object is added to the Issue object when it's an issue included +// in the IssueCommentEvent webhook payload, if the webhook is fired by a comment on a PR. +type PullRequestLinks struct { + URL *string `json:"url,omitempty"` + HTMLURL *string `json:"html_url,omitempty"` + DiffURL *string `json:"diff_url,omitempty"` + PatchURL *string `json:"patch_url,omitempty"` + MergedAt *Timestamp `json:"merged_at,omitempty"` +} + +// IssueType represents the type of issue. +// For now it shows up when receiving an Issue event. +type IssueType struct { + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Color *string `json:"color,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` +} + +// ListAllIssuesOptions specifies the optional parameters to the +// IssuesService.ListAllIssues method. +type ListAllIssuesOptions struct { // Filter specifies which issues to list. Possible values are: assigned, - // created, mentioned, subscribed, all. Default is "assigned". + // created, mentioned, subscribed, repos, all. Default is "assigned". Filter string `url:"filter,omitempty"` // State filters issues based on their state. Possible values are: open, @@ -125,51 +147,129 @@ type IssueListOptions struct { // Since filters issues by time. Since time.Time `url:"since,omitempty"` - // Add ListOptions so offset pagination with integer type "page" query parameter is accepted + Collab bool `url:"collab,omitempty"` + Orgs bool `url:"orgs,omitempty"` + Owned bool `url:"owned,omitempty"` + Pulls bool `url:"pulls,omitempty"` + ListOptions } -// PullRequestLinks object is added to the Issue object when it's an issue included -// in the IssueCommentEvent webhook payload, if the webhook is fired by a comment on a PR. -type PullRequestLinks struct { - URL *string `json:"url,omitempty"` - HTMLURL *string `json:"html_url,omitempty"` - DiffURL *string `json:"diff_url,omitempty"` - PatchURL *string `json:"patch_url,omitempty"` - MergedAt *Timestamp `json:"merged_at,omitempty"` +// ListAllIssues gets issues assigned to the authenticated user across all visible repositories including owned repositories, +// member repositories, and organization repositories. +// You can use the filter query parameter to fetch issues that are not necessarily assigned to you. +// +// GitHub API docs: https://docs.github.com/rest/issues/issues#list-issues-assigned-to-the-authenticated-user +// +//meta:operation GET /issues +func (s *IssuesService) ListAllIssues(ctx context.Context, opts *ListAllIssuesOptions) ([]*Issue, *Response, error) { + u := "issues" + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + req.Header.Set("Accept", mediaTypeReactionsPreview) + + var issues []*Issue + resp, err := s.client.Do(ctx, req, &issues) + if err != nil { + return nil, resp, err + } + + return issues, resp, nil } -// IssueType represents the type of issue. -// For now it shows up when receiving an Issue event. -type IssueType struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - Color *string `json:"color,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` +// ListUserIssuesOptions specifies the optional parameters to the +// IssuesService.ListUserIssues method. +type ListUserIssuesOptions struct { + // Filter specifies which issues to list. Possible values are: assigned, + // created, mentioned, subscribed, repos, all. Default is "assigned". + Filter string `url:"filter,omitempty"` + + // State filters issues based on their state. Possible values are: open, + // closed, all. Default is "open". + State string `url:"state,omitempty"` + + // Labels filters issues based on their label. + Labels []string `url:"labels,comma,omitempty"` + + // Sort specifies how to sort issues. Possible values are: created, updated, + // and comments. Default value is "created". + Sort string `url:"sort,omitempty"` + + // Direction in which to sort issues. Possible values are: asc, desc. + // Default is "desc". + Direction string `url:"direction,omitempty"` + + // Since filters issues by time. + Since time.Time `url:"since,omitempty"` + + ListOptions } -// List the issues for the authenticated user. If all is true, list issues -// across all the user's visible repositories including owned, member, and -// organization repositories; if false, list only owned and member -// repositories. -// -// GitHub API docs: https://docs.github.com/rest/issues/issues#list-issues-assigned-to-the-authenticated-user +// ListUserIssues gets issues across owned and member repositories assigned to the authenticated user. // // GitHub API docs: https://docs.github.com/rest/issues/issues#list-user-account-issues-assigned-to-the-authenticated-user // -//meta:operation GET /issues //meta:operation GET /user/issues -func (s *IssuesService) List(ctx context.Context, all bool, opts *IssueListOptions) ([]*Issue, *Response, error) { - var u string - if all { - u = "issues" - } else { - u = "user/issues" +func (s *IssuesService) ListUserIssues(ctx context.Context, opts *ListUserIssuesOptions) ([]*Issue, *Response, error) { + u := "user/issues" + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + req.Header.Set("Accept", mediaTypeReactionsPreview) + + var issues []*Issue + resp, err := s.client.Do(ctx, req, &issues) + if err != nil { + return nil, resp, err } - return s.listIssues(ctx, u, opts) + + return issues, resp, nil +} + +// IssueListByOrgOptions specifies the optional parameters to the +// IssuesService.ListByOrg method. +type IssueListByOrgOptions struct { + // Filter specifies which issues to list. Possible values are: assigned, + // created, mentioned, subscribed, repos, all. Default is "assigned". + Filter string `url:"filter,omitempty"` + + // State filters issues based on their state. Possible values are: open, + // closed, all. Default is "open". + State string `url:"state,omitempty"` + + // Labels filters issues based on their label. + Labels []string `url:"labels,comma,omitempty"` + + // Type can be the name of an issue type. + Type string `url:"type,omitempty"` + + // Sort specifies how to sort issues. Possible values are: created, updated, + // and comments. Default value is "created". + Sort string `url:"sort,omitempty"` + + // Direction in which to sort issues. Possible values are: asc, desc. + // Default is "desc". + Direction string `url:"direction,omitempty"` + + // Since filters issues by time. + Since time.Time `url:"since,omitempty"` + + ListOptions } // ListByOrg fetches the issues in the specified organization for the @@ -178,12 +278,8 @@ func (s *IssuesService) List(ctx context.Context, all bool, opts *IssueListOptio // GitHub API docs: https://docs.github.com/rest/issues/issues#list-organization-issues-assigned-to-the-authenticated-user // //meta:operation GET /orgs/{org}/issues -func (s *IssuesService) ListByOrg(ctx context.Context, org string, opts *IssueListOptions) ([]*Issue, *Response, error) { +func (s *IssuesService) ListByOrg(ctx context.Context, org string, opts *IssueListByOrgOptions) ([]*Issue, *Response, error) { u := fmt.Sprintf("orgs/%v/issues", org) - return s.listIssues(ctx, u, opts) -} - -func (s *IssuesService) listIssues(ctx context.Context, u string, opts *IssueListOptions) ([]*Issue, *Response, error) { u, err := addOptions(u, opts) if err != nil { return nil, nil, err @@ -222,6 +318,11 @@ type IssueListByRepoOptions struct { // any assigned user. Assignee string `url:"assignee,omitempty"` + // Type can be the name of an issue type. + // If the string * is passed, issues with any type are accepted. + // If the string none is passed, issues without type are returned. + Type string `url:"type,omitempty"` + // Creator filters issues based on their creator. Creator string `url:"creator,omitempty"` diff --git a/github/issues_test.go b/github/issues_test.go index 04cdb1c05ad..ff5b7377300 100644 --- a/github/issues_test.go +++ b/github/issues_test.go @@ -15,7 +15,7 @@ import ( "github.com/google/go-cmp/cmp" ) -func TestIssuesService_List_all(t *testing.T) { +func TestIssuesService_ListAllIssues(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -28,36 +28,44 @@ func TestIssuesService_List_all(t *testing.T) { "labels": "a,b", "sort": "updated", "direction": "asc", - "since": referenceTime.Format(time.RFC3339), - "page": "1", - "per_page": "2", + "since": "2026-01-02T00:00:00Z", + "collab": "true", + "orgs": "true", + "owned": "true", + "pulls": "true", + "page": "5", + "per_page": "10", }) fmt.Fprint(w, `[{"number":1}]`) }) - opt := &IssueListOptions{ + opt := &ListAllIssuesOptions{ Filter: "all", State: "closed", Labels: []string{"a", "b"}, Sort: "updated", Direction: "asc", - Since: referenceTime, - ListOptions: ListOptions{Page: 1, PerPage: 2}, + Since: time.Date(2026, time.January, 2, 0, 0, 0, 0, time.UTC), + Collab: true, + Orgs: true, + Owned: true, + Pulls: true, + ListOptions: ListOptions{Page: 5, PerPage: 10}, } ctx := t.Context() - issues, _, err := client.Issues.List(ctx, true, opt) + issues, _, err := client.Issues.ListAllIssues(ctx, opt) if err != nil { - t.Errorf("Issues.List returned error: %v", err) + t.Errorf("Issues.ListAllIssues returned error: %v", err) } want := []*Issue{{Number: Ptr(1)}} if !cmp.Equal(issues, want) { - t.Errorf("Issues.List returned %+v, want %+v", issues, want) + t.Errorf("Issues.ListAllIssues = %+v, want %+v", issues, want) } - const methodName = "List" + const methodName = "ListAllIssues" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Issues.List(ctx, true, opt) + got, resp, err := client.Issues.ListAllIssues(ctx, nil) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -65,26 +73,54 @@ func TestIssuesService_List_all(t *testing.T) { }) } -func TestIssuesService_List_owned(t *testing.T) { +func TestIssuesService_ListUserIssues(t *testing.T) { t.Parallel() client, mux, _ := setup(t) mux.HandleFunc("/user/issues", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") testHeader(t, r, "Accept", mediaTypeReactionsPreview) + testFormValues(t, r, values{ + "filter": "all", + "state": "closed", + "labels": "a,b", + "sort": "updated", + "direction": "asc", + "since": "2026-01-02T00:00:00Z", + "per_page": "4", + "page": "2", + }) fmt.Fprint(w, `[{"number":1}]`) }) ctx := t.Context() - issues, _, err := client.Issues.List(ctx, false, nil) + opts := &ListUserIssuesOptions{ + Filter: "all", + State: "closed", + Labels: []string{"a", "b"}, + Sort: "updated", + Direction: "asc", + Since: time.Date(2026, time.January, 2, 0, 0, 0, 0, time.UTC), + ListOptions: ListOptions{Page: 2, PerPage: 4}, + } + issues, _, err := client.Issues.ListUserIssues(ctx, opts) if err != nil { - t.Errorf("Issues.List returned error: %v", err) + t.Errorf("Issues.ListUserIssues returned error: %v", err) } want := []*Issue{{Number: Ptr(1)}} if !cmp.Equal(issues, want) { - t.Errorf("Issues.List returned %+v, want %+v", issues, want) + t.Errorf("Issues.ListUserIssues = %+v, want %+v", issues, want) } + + const methodName = "ListUserIssues" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Issues.ListUserIssues(ctx, nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, nil) + } + return resp, err + }) } func TestIssuesService_ListByOrg(t *testing.T) { @@ -93,24 +129,45 @@ func TestIssuesService_ListByOrg(t *testing.T) { mux.HandleFunc("/orgs/o/issues", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") + testFormValues(t, r, values{ + "filter": "all", + "state": "closed", + "labels": "a,b", + "type": "bug", + "sort": "updated", + "direction": "asc", + "since": "2026-01-02T00:00:00Z", + "per_page": "4", + "page": "2", + }) testHeader(t, r, "Accept", mediaTypeReactionsPreview) fmt.Fprint(w, `[{"number":1}]`) }) ctx := t.Context() - issues, _, err := client.Issues.ListByOrg(ctx, "o", nil) + opts := &IssueListByOrgOptions{ + Filter: "all", + State: "closed", + Labels: []string{"a", "b"}, + Type: "bug", + Sort: "updated", + Direction: "asc", + Since: time.Date(2026, time.January, 2, 0, 0, 0, 0, time.UTC), + ListOptions: ListOptions{Page: 2, PerPage: 4}, + } + issues, _, err := client.Issues.ListByOrg(ctx, "o", opts) if err != nil { t.Errorf("Issues.ListByOrg returned error: %v", err) } want := []*Issue{{Number: Ptr(1)}} if !cmp.Equal(issues, want) { - t.Errorf("Issues.List returned %+v, want %+v", issues, want) + t.Errorf("Issues.ListByOrg returned %+v, want %+v", issues, want) } const methodName = "ListByOrg" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Issues.ListByOrg(ctx, "\n", nil) + _, _, err = client.Issues.ListByOrg(ctx, "\n", opts) return err }) @@ -123,24 +180,6 @@ func TestIssuesService_ListByOrg(t *testing.T) { }) } -func TestIssuesService_ListByOrg_invalidOrg(t *testing.T) { - t.Parallel() - client, _, _ := setup(t) - - ctx := t.Context() - _, _, err := client.Issues.ListByOrg(ctx, "%", nil) - testURLParseError(t, err) -} - -func TestIssuesService_ListByOrg_badOrg(t *testing.T) { - t.Parallel() - client, _, _ := setup(t) - - ctx := t.Context() - _, _, err := client.Issues.ListByOrg(ctx, "\n", nil) - testURLParseError(t, err) -} - func TestIssuesService_ListByRepo(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -180,12 +219,12 @@ func TestIssuesService_ListByRepo(t *testing.T) { ctx := t.Context() issues, _, err := client.Issues.ListByRepo(ctx, "o", "r", opt) if err != nil { - t.Errorf("Issues.ListByOrg returned error: %v", err) + t.Errorf("Issues.ListByRepo returned error: %v", err) } want := []*Issue{{Number: Ptr(1)}} if !cmp.Equal(issues, want) { - t.Errorf("Issues.List returned %+v, want %+v", issues, want) + t.Errorf("Issues.ListByRepo returned %+v, want %+v", issues, want) } const methodName = "ListByRepo" @@ -195,7 +234,7 @@ func TestIssuesService_ListByRepo(t *testing.T) { }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Issues.ListByRepo(ctx, "o", "r", opt) + got, resp, err := client.Issues.ListByRepo(ctx, "o", "r", nil) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -203,15 +242,6 @@ func TestIssuesService_ListByRepo(t *testing.T) { }) } -func TestIssuesService_ListByRepo_invalidOwner(t *testing.T) { - t.Parallel() - client, _, _ := setup(t) - - ctx := t.Context() - _, _, err := client.Issues.ListByRepo(ctx, "%", "r", nil) - testURLParseError(t, err) -} - func TestIssuesService_Get(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/sub_issue.go b/github/sub_issue.go index 2effcc5afb0..02f7f505c2e 100644 --- a/github/sub_issue.go +++ b/github/sub_issue.go @@ -71,7 +71,7 @@ func (s *SubIssueService) Remove(ctx context.Context, owner, repo string, issueN // GitHub API docs: https://docs.github.com/rest/issues/sub-issues#list-sub-issues // //meta:operation GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues -func (s *SubIssueService) ListByIssue(ctx context.Context, owner, repo string, issueNumber int64, opts *IssueListOptions) ([]*SubIssue, *Response, error) { +func (s *SubIssueService) ListByIssue(ctx context.Context, owner, repo string, issueNumber int64, opts *ListOptions) ([]*SubIssue, *Response, error) { u := fmt.Sprintf("repos/%v/%v/issues/%v/sub_issues", owner, repo, issueNumber) u, err := addOptions(u, opts) if err != nil { diff --git a/github/sub_issue_test.go b/github/sub_issue_test.go index 1820ef625ef..5d179161fbd 100644 --- a/github/sub_issue_test.go +++ b/github/sub_issue_test.go @@ -59,12 +59,19 @@ func TestSubIssuesService_ListByIssue(t *testing.T) { mux.HandleFunc("/repos/o/r/issues/1/sub_issues", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") + testFormValues(t, r, values{ + "page": "2", + "per_page": "50", + }) fmt.Fprint(w, `[{"id":1},{"id":2}]`) }) ctx := t.Context() - opt := &IssueListOptions{} + opt := &ListOptions{ + Page: 2, + PerPage: 50, + } issues, _, err := client.SubIssue.ListByIssue(ctx, "o", "r", 1, opt) if err != nil { t.Errorf("SubIssues.ListByIssue returned error: %v", err)