Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions example/iterpagination/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2026 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// iterpagination is an example of how to use pagination with Go-native iterators.
// It's runnable with the following command:
//
// export GITHUB_AUTH_TOKEN=your_token
// export GITHUB_REPOSITORY_OWNER=your_owner
// export GITHUB_REPOSITORY_NAME=your_repo
// export GITHUB_REPOSITORY_ISSUE=your_issue
// go run .
package main

import (
"cmp"
"context"
"fmt"
"log"
"os"
"strconv"

"github.com/google/go-github/v81/github"
)

func main() {
token := os.Getenv("GITHUB_AUTH_TOKEN")
if token == "" {
log.Fatal("Unauthorized: No token present")
}
owner := cmp.Or(os.Getenv("GITHUB_REPOSITORY_OWNER"), "google")
repo := cmp.Or(os.Getenv("GITHUB_REPOSITORY_NAME"), "go-github")
issue, _ := strconv.Atoi(os.Getenv("GITHUB_REPOSITORY_ISSUE"))
issue = cmp.Or(issue, 2618)

ctx := context.Background()
client := github.NewClient(nil).WithAuthToken(token)

opts := github.IssueListCommentsOptions{
Sort: github.Ptr("created"),
ListOptions: github.ListOptions{
Page: 1,
PerPage: 5,
},
}

fmt.Println("Listing comments for issue", issue, "in repository", owner+"/"+repo)

scannedOpts := opts
for c := range github.MustIter(github.Scan2(func(p github.PaginationOption) ([]*github.IssueComment, *github.Response, error) {
return client.Issues.ListComments(ctx, owner, repo, issue, &scannedOpts, p)
})) {
body := c.GetBody()
if len(body) > 50 {
body = body[:50]
}
fmt.Printf("Comment: %q\n", body)
}
}
116 changes: 116 additions & 0 deletions github/examples_pagination_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2026 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package github_test

import (
"context"
"fmt"
"log"
"slices"

"github.com/google/go-github/v81/github"
)

func ExampleIssuesService_ListComments_offset_pagination_scan() {
client := github.NewClient(nil)
ctx := context.Background()
opts := &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
PerPage: 5,
},
}

it, hasErr := github.Scan(func(p github.PaginationOption) ([]*github.IssueComment, *github.Response, error) {
return client.Issues.ListComments(ctx, "google", "go-github", 526, opts, p)
})

comments := slices.Collect(it)
if err := hasErr(); err != nil {
log.Fatalf("Scan iterator returned error: %v", err)
}

fmt.Println("Total comments:", len(comments))
}

func ExampleIssuesService_ListComments_offset_pagination_scan2() {
client := github.NewClient(nil)
ctx := context.Background()
opts := &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
PerPage: 5,
},
}

var comments []*github.IssueComment
for c, err := range github.Scan2(func(p github.PaginationOption) ([]*github.IssueComment, *github.Response, error) {
return client.Issues.ListComments(ctx, "google", "go-github", 526, opts, p)
}) {
if err != nil {
log.Fatalf("Scan2 iterator returned error: %v", err)
}
comments = append(comments, c)
}

fmt.Println("Total comments:", len(comments))
}

func ExampleIssuesService_ListComments_offset_pagination_scanAndCollect() {
client := github.NewClient(nil)
ctx := context.Background()
opts := &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
PerPage: 5,
},
}

comments, err := github.ScanAndCollect(func(p github.PaginationOption) ([]*github.IssueComment, *github.Response, error) {
return client.Issues.ListComments(ctx, "google", "go-github", 526, opts, p)
})
if err != nil {
log.Fatalf("ScanAndCollect returned error: %v", err)
}

fmt.Println("Total comments:", len(comments))
}

func ExampleIssuesService_ListComments_offset_pagination_scan2MustIter() {
client := github.NewClient(nil)
ctx := context.Background()
opts := &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
PerPage: 5,
},
}

var comments []*github.IssueComment
for c := range github.MustIter(github.Scan2(func(p github.PaginationOption) ([]*github.IssueComment, *github.Response, error) {
return client.Issues.ListComments(ctx, "google", "go-github", 526, opts, p)
})) {
comments = append(comments, c)
}

fmt.Println("Total comments:", len(comments))
}

func ExampleSecurityAdvisoriesService_ListRepositorySecurityAdvisoriesForOrg_after_pagination_scan2MustIter() {
client := github.NewClient(nil)
ctx := context.Background()

opts := &github.ListRepositorySecurityAdvisoriesOptions{
ListCursorOptions: github.ListCursorOptions{
PerPage: 1,
},
}

var advisories []*github.SecurityAdvisory
for a := range github.MustIter(github.Scan2(func(p github.PaginationOption) ([]*github.SecurityAdvisory, *github.Response, error) {
return client.SecurityAdvisories.ListRepositorySecurityAdvisoriesForOrg(ctx, "alexandear-org", opts, p)
})) {
advisories = append(advisories, a)
}

fmt.Println("Total advisories:", len(advisories))
}
22 changes: 21 additions & 1 deletion github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,24 @@ func WithVersion(version string) RequestOption {
}
}

// WithOffsetPagination adds page query parameter to the request.
func WithOffsetPagination(page int) RequestOption {
return func(req *http.Request) {
q := req.URL.Query()
q.Set("page", strconv.Itoa(page))
req.URL.RawQuery = q.Encode()
}
}

// WithAfterPagination adds cursor pagination parameters to the request.
func WithAfterPagination(after string) RequestOption {
return func(req *http.Request) {
q := req.URL.Query()
q.Set("after", after)
req.URL.RawQuery = q.Encode()
}
}

// NewRequest creates an API request. A relative URL can be provided in urlStr,
// in which case it is resolved relative to the BaseURL of the Client.
// Relative URLs should always be specified without a preceding slash. If
Expand Down Expand Up @@ -581,7 +599,9 @@ func (c *Client) NewRequest(method, urlStr string, body any, opts ...RequestOpti
req.Header.Set(headerAPIVersion, defaultAPIVersion)

for _, opt := range opts {
opt(req)
if opt != nil {
opt(req)
}
}

return req, nil
Expand Down
4 changes: 2 additions & 2 deletions github/issues_comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type IssueListCommentsOptions struct {
//
//meta:operation GET /repos/{owner}/{repo}/issues/comments
//meta:operation GET /repos/{owner}/{repo}/issues/{issue_number}/comments
func (s *IssuesService) ListComments(ctx context.Context, owner, repo string, number int, opts *IssueListCommentsOptions) ([]*IssueComment, *Response, error) {
func (s *IssuesService) ListComments(ctx context.Context, owner, repo string, number int, opts *IssueListCommentsOptions, reqOpts ...RequestOption) ([]*IssueComment, *Response, error) {
var u string
if number == 0 {
u = fmt.Sprintf("repos/%v/%v/issues/comments", owner, repo)
Expand All @@ -72,7 +72,7 @@ func (s *IssuesService) ListComments(ctx context.Context, owner, repo string, nu
return nil, nil, err
}

req, err := s.client.NewRequest("GET", u, nil)
req, err := s.client.NewRequest("GET", u, nil, reqOpts...)
if err != nil {
return nil, nil, err
}
Expand Down
106 changes: 106 additions & 0 deletions github/pagination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2026 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package github

import (
"fmt"
"iter"
"slices"
)

// PaginationOption represents a pagination option for requests.
type PaginationOption = RequestOption

// Scan scans all pages for the given request function f and returns individual items in an iterator.
// If an error happens during pagination, the iterator stops immediately.
// The caller must consume the returned error function to retrieve potential errors.
func Scan[T any](f func(PaginationOption) ([]T, *Response, error)) (iter.Seq[T], func() error) {
exhausted := false
var e error
it := func(yield func(T) bool) {
defer func() {
exhausted = true
}()
for t, err := range Scan2(f) {
if err != nil {
e = err
return
}

if !yield(t) {
return
}
}
}
hasErr := func() error {
if !exhausted {
panic("called error function of Scan iterator before iterator was exhausted")
}
return e
}
return it, hasErr
}

// Scan2 scans all pages for the given request function f and returns individual items and potential errors in an iterator.
// The caller must consume the error element of the iterator during each iteration
// to ensure that no errors happened.
func Scan2[T any](f func(PaginationOption) ([]T, *Response, error)) iter.Seq2[T, error] {
return func(yield func(T, error) bool) {
var nextOpt PaginationOption

Pagination:
for {
ts, resp, err := f(nextOpt)
if err != nil {
var t T
yield(t, err)
return
}

for _, t := range ts {
if !yield(t, nil) {
return
}
}

// the f request function was either configured for offset- or cursor-based pagination.
switch {
case resp.NextPage != 0:
nextOpt = WithOffsetPagination(resp.NextPage)
case resp.After != "":
nextOpt = WithAfterPagination(resp.After)
default:
// no more pages
break Pagination
}
}
}
}

// MustIter provides a single item iterator for the provided two item iterator and panics if an error happens.
func MustIter[T any](it iter.Seq2[T, error]) iter.Seq[T] {
return func(yield func(T) bool) {
for x, err := range it {
if err != nil {
panic(fmt.Errorf("iterator produced an error: %w", err))
}

if !yield(x) {
return
}
}
}
}

// ScanAndCollect is a convenience function that collects all results and returns them as slice as well as an error if one happens.
func ScanAndCollect[T any](f func(p PaginationOption) ([]T, *Response, error)) ([]T, error) {
it, hasErr := Scan(f)
allItems := slices.Collect(it)
if err := hasErr(); err != nil {
return nil, err
}
return allItems, nil
}
Loading
Loading